DEV Community

Cover image for Créer une extension VS Code intégrant de l’IA
Vendomele
Vendomele

Posted on

Créer une extension VS Code intégrant de l’IA

L’IA est incontournable aujourd’hui, comme en témoigne la profusion d’applications qui l’intègrent. Dès lors, en tant que développeur, faut-il devenir un expert IA ou n’est-ce qu’un service comme un autre qui s’utilise via un SDK ou une requête HTTP ?

Cet article est un tuto qui a l'ambition d'expliquer comment implémenter une extension VS Code capable d’utiliser les LLM, et « spoiler alert », c’est un service comme un autre, un développeur ne sera pas perdu, il aura simplement besoin de quelques informations pour utiliser au mieux un tel service.

Un peu de sémantique

Prenons le temps d'être précis sur les termes :

  • Service IA : terme générique, grand public ("j'utilise un service IA")
  • Provider : terme technique qui désigne l'entité qui fournit l'accès au modèle via une API (OpenAI, Anthropic, Groq, ...)
  • Modèle : le réseau de neurones en lui-même (gpt-4o, claude-sonnet, gemini-2-5-flash, ...)
  • LLM (Large Language Model) : une catégorie particulière de modèle, qui a été entraîné sur de vastes quantités de texte afin de comprendre et générer du langage naturel

Un même provider peut exposer plusieurs modèles et un même modèle peut être accessible via plusieurs providers (ex: Llama disponible chez Groq, Nvidia NIM, OpenRouter, ...).

Comment va s'articuler l'extension

L'extension va permettre de sélectionner un provider IA. Les providers disponibles ont été choisis pour deux raisons :

  1. Leur gratuité d'accès : pas besoin de renseigner un numéro de carte bleue, quand on veut faire de la veille technologique, c'est toujours un point positif
  2. Leur diversité technique : certains providers nécessitent une clé API ou un jeton, les en-têtes (header) et le corps (body) de la requête HTTP seront différents

L'extension devra ainsi gérer plusieurs providers, enregistrer une clé API ou un jeton de manière sécurisée, récupérer le contexte selon la sélection faite par l'utilisateur pour le transmettre au provider, puis traiter et afficher la réponse.

Prérequis :

  • Node.js
  • Visual Studio Code

Création du projet pour une extension VS Code

Génération du projet

Pour la création du projet, on va utiliser les outils Yeoman (Yo) et generator-code, le premier permet de créer la structure de base d’un projet, tandis que le second, développé par Microsoft, génère l’ossature d’une extension Visual Studio Code.

La commande npx permet d’exécuter ces outils à la volée sans devoir les installer au préalable.

Ouvrez votre terminal et lancez la commande suivante :

npx -p yo -p generator-code yo code
Enter fullscreen mode Exit fullscreen mode

Un questionnaire sera disponible pour créer le projet

  • le type d'extensions: New Extension (Typescript)
  • le nom du projet (un dossier reprenant ce nom sera créé): hello-ai
  • l'identifiant de l'extension: par défaut, reprend le nom du projet
  • une description: saisir un court texte
  • initialiser un dépôt Git: oui ou non
  • le bundler: esbuild
  • le gestionnaire de paquets: npm, yarn ou pnpm

type d'extension

questionnaire

Configuration du projet

Dans tsconfig.json : Ajouter "types": ["node"] dans compilerOptions. Sans cela, Typescript ne reconnaît pas les types globaux de Node.js.

{
    "compilerOptions": {
        ...
        "types": ["node"],
        ...
Enter fullscreen mode Exit fullscreen mode

Dans .vscode/tasks.json : Pour les script watch:esbuild, modifier la valeur de problemMatcher par $tsc-watch. Le matcher par défaut avec esbuild ne remonte pas correctement les erreurs Typescript, tandis que $tsc-watch parse le format de sortie de tsc et les affiche proprement.

{
    ...
    "tasks": [
        ...
        {
            "type": "npm",
            "script": "watch:esbuild",
            "group": "build",
            "problemMatcher": "$tsc-watch",
            "isBackground": true,
            "label": "npm: watch:esbuild",
            "presentation": {
                "group": "watch",
                "reveal": "never"
            }
        },
        ...
Enter fullscreen mode Exit fullscreen mode

Dans .vscode/launch.json : Supprimez la ligne preLaunchTask. Comme on exécute manuellement le script watch:esbuild en arrière plan et que la compilation est en temps réel, il est inutile de demander à VS Code de relancer une tâche de compilation.

Les alias

Pour pouvoir utiliser les alias dans ce type de projet, en plus du fichier tsconfig.json, il faut modifier le fichier esbuild.js.

/* tsconfig.json */

{
    "compilerOptions": {
        ...
        "paths": {
            "@helpers/*": ["./src/helpers/*"],
            "@providers/*": ["./src/providers/*"],
            "@services/*": ["./src/services/*"],
            "@typings/*": ["./src/typings/*"],
        },
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode
/* esbuild.js */

...
async function main() {
    const ctx = await esbuild.context({
        ...
        alias: {
            '@helpers': "./src/helpers",
            '@providers': "./src/providers",
            '@services': "./src/services",
            '@typings': "./src/typings",
        }
    });
    if (watch) {
        await ctx.watch();
    } else {
        await ctx.rebuild();
        await ctx.dispose();
    }
}
...
Enter fullscreen mode Exit fullscreen mode

Pour lancer l'extension en mode développement

Dans le terminal de VS Code, exécutez la commande suivante :

npm run watch:esbuild
Enter fullscreen mode Exit fullscreen mode

Via le menu de débogage (F5), une nouvelle instance de VS Code en mode Hôte de développement s'ouvrira, permettant ainsi de tester l'extension sans avoir à l'installer et qui prend en compte les modifications, en ayant exécuté un restart (Ctrl+Shift+F5).

Implémentation

Pattern strategy

Comme l'extension permet de choisir un provider qui aura ses propres exigences dans les éléments à appeler ou données à fournir, ce pattern s'impose naturellement.

/* src/typings/aiProvider.ts */

import { ExtensionContext } from 'vscode';

type AIProviderCredential = {
    create: (context: ExtensionContext) => Promise<boolean>;
    read: (context: ExtensionContext) => Promise<string | undefined>;
    delete: (context: ExtensionContext) => Promise<boolean>;
};

export type AIProvider = {
    name: string;
    value: string;
    url: string;
    credential: AIProviderCredential;
    buildHeaders: (context: ExtensionContext) => Promise<Record<string, string> | undefined>;
    buildBody: (prompt: string) => object;
    parseResponse: (data: any) => string;
};
Enter fullscreen mode Exit fullscreen mode
  • name : information à utiliser dans les libellés
  • value : valeur qui permet, entre autres, de sélectionner le provider
  • url : endpoint du provider
  • credential : permet de stocker, lire et supprimer dans les Secrets une information d'identification qu'il s'agisse d'une clé API ou d'un jeton
  • buildHeaders : fournit les en-têtes (header) pour la requête HTTP
  • buildBody : fournit le corps (body) pour la requête HTTP
  • parseResponse : extrait la réponse renvoyée par le provider IA

Oui je sais, j'aurai pu utiliser interface au lieu de type, c'est une préférence et cet article n'a pas pour objet de débattre sur ce point.

Nb: Si vous voulez suivre ce tuto pas à pas, commencez avec un AIProvider et un AIProviderCredential contenant le minimum et ajouter les attributs et méthodes au fur et à mesure qu'ils seront nécessaires dans le code, cela évitera que Typescript s'alarme :

type AIProviderCredential = {
    create: (context: ExtensionContext) => Promise<boolean>;
    // read: (context: ExtensionContext) => Promise<string | undefined>;
    // delete: (context: ExtensionContext) => Promise<boolean>;
};

export type AIProvider = {
    // name: string;
    value: string;
    // url: string;
    // credential: AIProviderCredential;
    // buildHeaders: (context: ExtensionContext) => Promise<Record<string, string> | undefined>;
    // buildBody: (prompt: string) => object;
    // parseResponse: (data: any) => string;
};
Enter fullscreen mode Exit fullscreen mode

L'objet providerManager permet de sélectionner le provider voulu :

/* src/providers/providerManager.ts */

import { AIProvider } from '@typings/aiProvider';

const providers: AIProvider[] = [];

export const providerManager = {
    getProviderByValue: (value: string): AIProvider | undefined => providers.find(p => p.value == value),
};
Enter fullscreen mode Exit fullscreen mode

providerManager compte d'autres méthodes qui seront présentées et expliquées au fur et à mesure.

Pour cet article, on mettra en avant deux providers :

/* src/providers/geminiProvider.ts */

import { AIProvider } from '@typings/aiProvider';

export const geminiProvider: AIProvider = {
    value: 'gemini'
};
Enter fullscreen mode Exit fullscreen mode
/* src/providers/githubProvider.ts */

import { AIProvider } from '@typings/aiProvider';

export const githubProvider: AIProvider = {
    value: 'github'
};
Enter fullscreen mode Exit fullscreen mode
import { geminiProvider } from '@providers/geminiProvider';
import { githubProvider } from '@providers/githubProvider';
import { AIProvider } from '@typings/aiProvider';

const providers: AIProvider[] = [
    geminiProvider,
    githubProvider
];

export const providerManager = {
Enter fullscreen mode Exit fullscreen mode

Package.json & commandes

On va définir les commandes de l'extension :

{
    ...
    "contributes": {
        "commands": [
            {
                "command": "hello-ai.connect",
                "title": "Hello AI: Se connecter à un service IA"
            },
            {
                "command": "hello-ai.disconnect",
                "title": "Hello AI: Se déconnecter du service IA"
            },
            {
                "command": "hello-ai.main",
                "title": "Hello AI"
            }
        ],
        "menus": {
            "commandPalette": [
                {
                    "command": "hello-ai.main",
                    "when": "editor.hasSelection && (editorLangId == typescript || editorLangId == typescriptreact)"
                }
            ],
            "editor/context": [
                {
                    "command": "hello-ai.main",
                    "group": "hello-ai",
                    "when": "editor.hasSelection && (editorLangId == typescript || editorLangId == typescriptreact)"
                }
            ]
        }
    },
    ...
Enter fullscreen mode Exit fullscreen mode

Il convient de noter que parler de Se connecter et Se déconnecter est, dans la plupart des cas, un abus de langage. En réalité, la connexion permet de récupérer l’identifiant (sous forme de clé ou de jeton) auprès du provider sélectionné, afin de le sauvegarder de manière sécurisée. À l’inverse, la déconnexion supprime cet identifiant. Ces termes, plus génériques, ont finalement été retenus pour englober les scénarios spécifiques où la récupération d’un jeton impose une authentification.

La commande main déclenche une requête HTTP vers le provider. Comme cette extension est une simple démo, le périmètre est limité aux fichiers .ts et .tsx pour simplifier le développement et se concentrer sur l’essentiel.

"when": "... (editorLangId == typescript || editorLangId == typescriptreact)"
Enter fullscreen mode Exit fullscreen mode

Dans le fichier extension.ts (généré automatiquement lors de la création du projet), voici comment référencer les commandes définies dans la package.json :

/* src/extension.ts */

import { aiService } from '@services/aiService';
import { commands, ExtensionContext } from 'vscode';

export function activate(context: ExtensionContext) {
    const connectCmd = commands.registerCommand('hello-ai.connect', async () => {
        await aiService.connect(context);
    });

    const disconnectCmd = commands.registerCommand('hello-ai.disconnect', async () => {
        await aiService.disconnect(context);
    });

    const mainCmd = commands.registerCommand('hello-ai.main', async () => {
        await aiService.main(context);
    });

    context.subscriptions.push(connectCmd, disconnectCmd, mainCmd);
}

export function deactivate() { }
Enter fullscreen mode Exit fullscreen mode
/* src/services/aiService.ts */

import { ExtensionContext } from 'vscode';

export const aiService = {
    connect: async (context: ExtensionContext): Promise<void> => {},

    disconnect: async (context: ExtensionContext): Promise<void> => {},

    main: async (context: ExtensionContext): Promise<void> => {}
};
Enter fullscreen mode Exit fullscreen mode

En appuyant sur F1 depuis l'Hôte de développement et en filtrant avec le mot hello, on obtient ceci:

commandes

La commande main libellée « Hello AI » n'apparait pas si aucun texte n'est sélectionné dans un fichier.

"when": "editor.hasSelection ...
Enter fullscreen mode Exit fullscreen mode

Enregistrement de clés ou jetons: aiService.connect

Fournir une interface pour une sélection depuis VS Code

L'API VS Code nous permet d'ouvrir une liste déroulante (ou boîte de sélection rapide) via vscode.window.showQuickPick. Selon la documentation (lien), showQuickPick peut prendre comme paramètre un tableau d'objets dérivant de QuickPickItem. Définissons notre propre QuickPickItem :

/* src/typings/providerQuickPickItem.ts */

import { QuickPickItem } from 'vscode';

export type ProviderQuickPickItem = QuickPickItem & {
    value: string;
};
Enter fullscreen mode Exit fullscreen mode

Mettons à jour les providers :

export const geminiProvider: AIProvider = {
    name: 'Google Gemini',
    value: 'gemini'
};
Enter fullscreen mode Exit fullscreen mode
export const githubProvider: AIProvider = {
    name: 'GitHub models',
    value: 'github'
};
Enter fullscreen mode Exit fullscreen mode

Ajoutons une méthode à providerManager qui retournera un tableau de ProviderQuickPickItem à partir des providers disponibles :

import { geminiProvider } from '@providers/geminiProvider';
import { githubProvider } from '@providers/githubProvider';
import { AIProvider } from '@typings/aiProvider';
import { ProviderQuickPickItem } from '@typings/providerQuickPickItem';

const providers: AIProvider[] = [
    geminiProvider,
    githubProvider
];

export const providerManager = {
    getProviderByValue: (value: string): AIProvider | undefined => providers.find(p => p.value == value),

    getProviderQuickPickItems: (): ProviderQuickPickItem[] => providers.map(p => ({
        label: p.name,
        value: p.value
    })),
};
Enter fullscreen mode Exit fullscreen mode

Commençons à écrire la méthode connect de l'objet aiService :

import { providerManager } from '@providers/providerManager';
import { ProviderQuickPickItem } from '@typings/providerQuickPickItem';
import { ExtensionContext, window } from 'vscode';

export const aiService = {
    connect: async (context: ExtensionContext): Promise<void> => {
        const items: ProviderQuickPickItem[] = providerManager.getProviderQuickPickItems();

        if (!items.length) return;

        const item: ProviderQuickPickItem | undefined = await window.showQuickPick(items, { placeHolder: 'Sélectionnez un service IA' })

        if (!item) return;
    },
Enter fullscreen mode Exit fullscreen mode

liste déroulante

Récupération d'une clé API ou d'un jeton

Pour le provider Gemini, on a besoin d'une clé API disponible sur Google AI Studio. Une fois cette clé récupérée, on l'enregistre dans les Secrets de VS Code. Pour faciliter la tâche de l'utilisateur, l'extension va lui ouvrir le site (vscode.env.openExternal), lui indiquer quoi faire à l'aide d'une boîte de dialogue (vscode.window.showInformationMessage) et fournir une zone de saisie pour coller la valeur de la clé (vscode.window.showInputBox).

import { AIProvider } from '@typings/aiProvider';
import { env, ExtensionContext, Uri, window } from 'vscode';

const OK_LABEL = 'Ok';
const KEY = 'gemini.apiKey';

export const geminiProvider: AIProvider = {
    ...
    credential: {
        create: async (context: ExtensionContext): Promise<boolean> => {
            await env.openExternal(Uri.parse('https://aistudio.google.com/app/apikey'));

            const selection: string | undefined = await window.showInformationMessage(
                'Configuration de la clé API pour Google Gemini',
                {
                    modal: true,
                    detail: "Une page vient de s'ouvrir dans votre navigateur pour Google Gemini.\n" +
                    "Si vous n'êtes pas déjà connecté, connectez-vous d'abord à votre compte.\n" +
                    "Cherchez ensuite l'option permettant de générer une nouvelle clé API, créez-la, puis copiez-la intégralement.\n\n" +
                    'Une fois la clé copiée dans votre presse-papiers, revenez sur cette fenêtre et cliquez sur "OK" pour pouvoir la coller dans le champ qui s\'affichera.'
                },
                OK_LABEL,
            );

            if (selection !== OK_LABEL) return false;

            const secret: string | undefined = await window.showInputBox({
                prompt: 'Collez la clé API récupérée sur le site',
                password: true,
                placeHolder: 'clé API'
            });

            if (!secret) return false;

            await context.secrets.store(KEY, secret);

            return true;
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Pour le provider GitHub models, on a besoin d'un jeton, le code est beaucoup plus simple, on a à utiliser vscode.authentication.getSession qui s'occupe d'ouvrir une page web si nécessaire et sans copier-coller nécessaire de la part de l'utilisateur.

import { AIProvider } from '@typings/aiProvider';
import { authentication, ExtensionContext } from 'vscode';

const KEY = 'github.token';

export const githubProvider: AIProvider = {
    ...
    credential: {
        create: async (context: ExtensionContext): Promise<boolean> => {
            try {
                const session = await authentication.getSession(
                    'github',
                    ['read:user'],
                    { createIfNone: true }
                );

                await context.secrets.store(KEY, session.accessToken);

                return true;
            } catch {
                return false;
            }
        },
    }
};
Enter fullscreen mode Exit fullscreen mode

Reste à finaliser la méthode connect de aiService :

const provider: AIProvider | undefined = providerManager.getProviderByValue(item.value);

if (!provider) return;

const created = await provider.credential.create(context);

if (created)
    window.showInformationMessage(`Connexion à ${provider.name}.`);
else
    window.showErrorMessage('Connexion annulée ou échouée.');
Enter fullscreen mode Exit fullscreen mode

Voyons le résultat de ce code, en sélectionnant Google Gemini :

open

explication

clé gemini

zone de saisie pour coller la clé

Et pour GitHub models :

autorisation pour github

sélection compte github

Si vous êtes déjà connecté à GitHub, vous n'aurez que le message au niveau des notifications de VS Code (vscode.window.showInformationMessage).

connexion réussie

Suppression de clés ou jetons: aiService.disconnect

Mettons à jour nos providers pour permettre la lecture et la suppression d'un secret.

export const geminiProvider: AIProvider = {
    ...
    credential: {
        ...
        read: async (context: ExtensionContext): Promise<string | undefined> => context.secrets.get(KEY),

        delete: async function (context: ExtensionContext): Promise<boolean> {
            const secret = await this.read(context);

            if (secret) {
                await context.secrets.delete(KEY);

                return true;
            }

            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Le code pour githubProvider est identique.

De la même manière que pour la connexion, la suppression d'un secret passera par une liste déroulante. Pour l'alimenter, nous allons ajouter une méthode à l'objet providerManager afin de récupérer un tableau de ProviderQuickPickItem pour les providers ayant un secret enregistré.

...
import { ExtensionContext } from 'vscode';

export const providerManager = {
    ...

    getConnectedProviderQuickPickItems: async (context: ExtensionContext): Promise<ProviderQuickPickItem[]> => {
        const items = await Promise.all(
            providers.map(async (p) => {
                const credential = await p.credential.read(context);
                const item = {
                    label: p.name,
                    value: p.value
                };

                return credential ? item : undefined;
            })
        );

        return items.filter(i => i !== undefined);
    },
};
Enter fullscreen mode Exit fullscreen mode

Il ne reste plus qu'à implémenter la méthode disconnect de aiService :

const items: ProviderQuickPickItem[] = await providerManager.getConnectedProviderQuickPickItems(context);

if (!items.length) {
    window.showInformationMessage("Vous n'êtes connecté à aucun service IA");
    return;
}

const item: ProviderQuickPickItem | undefined = await window.showQuickPick(items, { placeHolder: 'Sélectionnez le service IA à déconnecter' })

if (!item) return;

const provider: AIProvider | undefined = providerManager.getProviderByValue(item.value);

if (!provider) return;

const deleted = await provider.credential.delete(context);

if (deleted)
    window.showInformationMessage(`Déconnecté de ${provider.name}`);
Enter fullscreen mode Exit fullscreen mode

Appel du service

Nous arrivons enfin à la partie la plus importante de ce tuto. Jusqu'à présent, on a mis en place les éléments permettant l'utilisation d'un service IA, nous allons implémenter la requête vers un provider.

Vous avez l'habitude à présent, commençons par mettre à jour les providers:

export const geminiProvider: AIProvider = {
    ...
    url: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
    ...
    buildHeaders: async function (context: ExtensionContext): Promise<Record<string, string> | undefined> {
        const credential = await this.credential.read(context);

        if (credential) {
            return {
                'Content-Type': 'application/json',
                'x-goog-api-key': credential
            };
        }

        return undefined;
    },

    buildBody: (prompt: string): object => ({ contents: [{ parts: [{ text: prompt }] }] }),

    parseResponse: (data: any): string => data.candidates[0].content.parts[0].text
};
Enter fullscreen mode Exit fullscreen mode
export const githubProvider: AIProvider = {
    ...
    url: 'https://models.inference.ai.azure.com/chat/completions',
    ...
    buildHeaders: async function (context: ExtensionContext): Promise<Record<string, string> | undefined> {
        const credential = await this.credential.read(context);

        if (credential) {
            return {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${credential}`
            }
        }

        return undefined;
    },

    buildBody: (prompt: string): object => ({
        model: 'gpt-4o',
        messages: [{ role: 'user', content: prompt }]
    }),

    parseResponse: (data: any): string => data.choices[0].message.content
};
Enter fullscreen mode Exit fullscreen mode

On va construire une classe utilitaire (Helper) qu'on nommera CodeDefinitionHelper pour récupérer les informations qu'on va introduire dans le prompt nécessaire à la requête. Pour cela on va s'appuyer sur une fonctionnalité native de VS Code : Go to Type Definition, accesible via le menu contextuel sur une sélection :

Go to type definition

Via cette instruction vscode.commands.executeCommand et vscode.executeTypeDefinitionProvider en paramètre, on pourra reproduire Go to Type Definition par programmation.

Je ne vais pas commenter tout le code de CodeDefinitionHelper mais son objectif est d'une part récupérer la ligne complète où se trouve la sélection. Imaginons que nous avons ceci :

const context: AppContext = useAppContext();

useEffect(() => {
Enter fullscreen mode Exit fullscreen mode

L'utilisateur sélectionne le mot context, le helper va récupérer const context: AppContext = useAppContext().

CodeDefinitionHelper va ensuite récupérer le bloc correspondant à la définition. Pour résumer, il fait du traitement de fichiers pour en extraire du texte.

/* src/helpers/codeDefinitionHelper.ts */

import * as vscode from 'vscode';

export class CodeDefinitionHelper {
    static getElementsToPrompt = async (): Promise<{ research: string, definitions: Set<string>; } | undefined> => {
        const editor = vscode.window.activeTextEditor;

        if (!editor || editor.selection.isEmpty) return undefined;

        const research = CodeDefinitionHelper.getResearch(editor);
        const definitions = await CodeDefinitionHelper.getDefinitions(editor);

        return definitions ? { research, definitions } : undefined;
    };

    private static getResearch = (editor: vscode.TextEditor): string => {
        const doc = editor.document;
        const startPosition = new vscode.Position(editor.selection.start.line, 0);
        const endPosition = doc.positionAt(doc.getText().length);
        const text = doc.getText(new vscode.Range(startPosition, endPosition));
        const match = text.match(/([^;]*)[;\n]/);

        return match ? match[1].trim() : text.trim();
    };

    private static getDefinitions = async (editor: vscode.TextEditor): Promise<Set<string> | undefined> => {
        const currentUri = editor.document.uri;
        const currentPosition = editor.selection.start;
        const locations = await vscode.commands.executeCommand<vscode.Location[]>(
            'vscode.executeTypeDefinitionProvider',
            currentUri,
            currentPosition
        );

        if (!locations || locations.length == 0) return undefined;

        const result = new Set<string>();

        for (const target of locations) {
            const doc = await vscode.workspace.openTextDocument(target.uri);
            const startPosition = new vscode.Position(target.range.start.line, 0);
            const endPosition = doc.positionAt(doc.getText().length);
            const textFromStart = doc.getText(new vscode.Range(startPosition, endPosition));
            const codeBlock = CodeDefinitionHelper.extractBlock(textFromStart);

            result.add(codeBlock);
        }

        return result;
    };

    private static extractBlock = (code: string): string => {
        let openBrackets = 0;
        let endIndex = 0;
        let foundFirstBracket = false;

        for (let i = 0; i < code.length; i++) {
            if (code[i] == '{') {
                openBrackets++;
                foundFirstBracket = true;
            } else if (code[i] == '}') {
                openBrackets--;
            }

            if (foundFirstBracket && openBrackets == 0) {
                endIndex = i + 1;
                break;
            }
        }

        // Si pas d'accolade ou structure étrange, on renvoie par défaut les 5 premières lignes
        if (endIndex == 0)
            return code.split('\n').slice(0, 5).join('\n');

        return code.substring(0, endIndex);
    };
}
Enter fullscreen mode Exit fullscreen mode

MAJ des imports pour le fichier aiService.ts :

import { CodeDefinitionHelper } from '@helpers/codeDefinitionHelper';
...
import { commands, ExtensionContext, window, workspace } from 'vscode';
Enter fullscreen mode Exit fullscreen mode

Nous avons enfin tous les éléments pour implémenter la méthode main de aiService. Pour commencer nous allons vérifier s'il y a au moins un provider avec un secret d'enregistré, si ce n'est pas le cas on ne va pas plus loin, s'il y en a qu'un, on le prend par défaut sinon on demande à l'utilisateur de faire une sélection parmi les providers « connectés » :

const items: ProviderQuickPickItem[] = await providerManager.getConnectedProviderQuickPickItems(context);

let item = undefined;

if (!items.length)
    window.showErrorMessage("Vous n'êtes connecté à aucun service IA");
else if (items.length == 1)
    item = items[0];
else
    item = await window.showQuickPick(items, { placeHolder: 'Sélectionnez le service IA que vous souhaitez utiliser' })

if (item === undefined) return;

const provider = providerManager.getProviderByValue(item.value);

if (!provider) return;
Enter fullscreen mode Exit fullscreen mode

On construit dynamiquement le prompt :

const dataForPrompt = await CodeDefinitionHelper.getElementsToPrompt();

if (!dataForPrompt) return

const { research, definitions } = dataForPrompt;
const promptDefinitions = [...definitions].join('\n');
const prompt = 'Génère ta réponse directement en texte Markdown brut (titres, listes, gras, ..., blocs de code si pertinent).\n' +
    "INTERDICTION : N'enveloppe JAMAIS ta réponse dans un bloc de code Markdown. Commence directement par le contenu.\n\n" +
    `Que peux-tu dire sur "${research}" dont la/les définition(s) est/sont :\n${promptDefinitions}`

Enter fullscreen mode Exit fullscreen mode

Réalisons la requête HTTP :

const headers = await provider.buildHeaders(context);

if (headers === undefined) return;

const response = await fetch(provider.url, {
    method: 'POST',
    headers,
    body: JSON.stringify(provider.buildBody(prompt))
});
Enter fullscreen mode Exit fullscreen mode

Et pour finir, traitons la réponse, dans le cas où la requête a réussi, on crée un document de type markdown qu'on va ouvrir en preview en écran splité à l'aide de la commande markdown.showPreviewToSide :

if (!response.ok) {
    switch (response.status) {
        case 401:
            window.showErrorMessage('Clé API ou token invalide.');
            break;
        case 429:
            window.showErrorMessage('Quota dépassé — vérifiez votre plan.');
            break;
        case 503:
            window.showErrorMessage('Le service est temporairement indisponible.');
            break;
        default: window.showErrorMessage(`Erreur HTTP ${response.status}`);
    }
} else {
    const data = await response.json();
    const doc = await workspace.openTextDocument({
        content: provider.parseResponse(data) + `\n\n---\n*✨ Réponse générée par **${provider.name}***`,
        language: 'markdown'
    });

    await commands.executeCommand(
        'markdown.showPreviewToSide',
        doc.uri
    );
}
Enter fullscreen mode Exit fullscreen mode

Et voilà, nous avons à présent une extension qui intègre de l'IA :

réponse

Code source

lien du projet : github.com/fvendomele/hello-ai

Remarque : le code présenté dans ce tuto a été simplifié pour rester lisible. La version complète, disponible sur le dépôt, diffère sur plusieurs points :

  • pas de chaînes magiques, utilisation de constantes
  • plus de classes utilitaires utilisées comme façade ou pour de la factorisation
  • des messages supplémentaires à destination de l'utilisateur
  • des providers supplémentaires : Groq (à ne pas confondre avec Grok) et Mistral AI

Au final, rien de nouveau

Pour un développeur qui n'est pas un expert IA, il n'y a rien de fondamentalement nouveau ici. On a l'habitude de travailler avec une base de données, une API, des services accessibles via des endpoints. L'IA, ce n'est jamais qu'un nouveau module de plus dans la boîte à outils, un service comme un autre, avec ses propres contraintes, mais rien qui justifie de s'en sentir exclu si on n'est pas spécialiste du domaine.

Le choix d'une extension VS Code n'est pas anodin. Ce genre de module s'inscrit dans un environnement qui lui permet d'avoir accès au code, au curseur, à la sélection, à l'éditeur. C'est cette proximité qui permet de générer un prompt dynamique. On part de la sélection de l'utilisateur, on remonte jusqu'à la définition et on assemble le tout pour fournir un contexte pertinent pour le provider IA. Une application classique aurait nécessité de constituer des données pour ensuite les exploiter.

Alors bien sûr, j'aurais pu faire plus simple, avoir des prompts préconstruits. La difficulté n'a jamais été du côté de l'appel au provider. Si difficulté il y a, c'est à la construction du prompt, aller chercher la bonne information, au bon endroit, dans le bon format, pour que la réponse du LLM soit réellement exploitable. C'est ce que j'ai voulu démontrer à travers ce tuto.

Dans cette extension qui sert de démo, on utilise uniquement du texte, mais beaucoup de providers sont dits multimodaux, capables d'analyser des images, voire des vidéos. Cette documentation (lien) vers l'API de Gemini nous montre comment utiliser une image et là encore, inutile d'être un spécialiste, quand on parle de base64 à un développeur, il sait à quoi cela sert. Une bonne documentation et n'importe quel développeur peut intégrer de l'IA.

On s'inquiète, à juste titre, pour notre métier de développeur, mais en tant qu'outil, l'IA ouvre des perspectives, un nouveau terrain de jeu à explorer avec tout ce qu'il peut nous proposer, pour ma part, je trouve cela très enthousiasmant, bon à condition de ne pas arriver au cas Skynet.

Top comments (0)