Skip to content

Commit

Permalink
feat: add ECL agent chat
Browse files Browse the repository at this point in the history
Signed-off-by: David de Hilster <[email protected]>
  • Loading branch information
dehilsterlexis committed May 3, 2024
1 parent 6a0f82d commit 01c19cd
Show file tree
Hide file tree
Showing 9 changed files with 2,507 additions and 4,725 deletions.
6,208 changes: 1,495 additions & 4,713 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 31 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"@types/react": "17.0.65",
"@types/react-dom": "17.0.20",
"@types/tmp": "0.2.4",
"@types/vscode": "1.76.0",
"@types/vscode": "1.82.0",
"@types/vscode-notebook-renderer": "1.72.0",
"@typescript-eslint/eslint-plugin": "6.7.0",
"@typescript-eslint/parser": "6.7.0",
Expand Down Expand Up @@ -125,15 +125,15 @@
"url": "https://github.com/hpcc-systems/vscode-ecl.git"
},
"engines": {
"vscode": "^1.76.0",
"node": ">=16.0.0"
"vscode": "^1.76.0"
},
"galleryBanner": {
"color": "#CFB69A",
"theme": "light"
},
"extensionDependencies": [
"GordonSmith.observable-js"
"GordonSmith.observable-js",
"github.copilot-chat"
],
"activationEvents": [
"onLanguage:ecl",
Expand All @@ -148,6 +148,11 @@
"workspaceContains:*.kel",
"workspaceContains:*.dashy"
],
"enabledApiProposals": [
"chatParticipant",
"chatVariableResolver",
"languageModels"
],
"contributes": {
"languages": [
{
Expand Down Expand Up @@ -221,6 +226,10 @@
],
"viewsWelcome": [],
"commands": [
{
"command": "eclagent.doSomething",
"title": "Do Something"
},
{
"command": "ecl.submit",
"category": "ECL",
Expand Down Expand Up @@ -757,6 +766,24 @@
}
}
],
"chatParticipants": [
{
"id": "chat-ecl.eclagent",
"name": "eclagent",
"description": "I'm conversant in ECL! What can I teach you?",
"isSticky": true,
"commands": [
{
"name": "teach",
"description": "Pick at random ECL concept then explain it"
},
{
"name": "play",
"description": "Enter "
}
]
}
],
"submenus": [
{
"id": "setState",
Expand Down
176 changes: 176 additions & 0 deletions src/chat/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import * as vscode from "vscode";

const ECLAGENT_NAMES_COMMAND_ID = "eclagent.doSomething";
const ECLAGENT_PARTICIPANT_ID = "chat-ecl.eclagent";

interface IECLChatResult extends vscode.ChatResult {
metadata: {
command: string;
}
}

const LANGUAGE_MODEL_ID = "copilot-gpt-3.5-turbo"; // Use faster model. Alternative is 'copilot-gpt-4', which is slower but more powerful

export function activate(context: vscode.ExtensionContext) {

// Define an ECL chat handler.
const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<IECLChatResult> => {
// To talk to an LLM in your subcommand handler implementation, your
// extension can use VS Code's `requestChatAccess` API to access the Copilot API.
// The GitHub Copilot Chat extension implements this provider.
if (request.command == "teach") {
stream.progress("Picking the right topic to teach...");
const topic = getTopic(context.history);
const messages = [
new vscode.LanguageModelChatSystemMessage("You are an ECL expert! Your job is to explain ECL concepts. Always start your response by stating what concept you are explaining. Always include code samples."),
new vscode.LanguageModelChatUserMessage(topic)
];
const chatResponse = await vscode.lm.sendChatRequest(LANGUAGE_MODEL_ID, messages, {}, token);
for await (const fragment of chatResponse.stream) {
stream.markdown(fragment);
}

stream.button({
command: ECLAGENT_NAMES_COMMAND_ID,
title: vscode.l10n.t("Do something"),
});

return { metadata: { command: "teach" } };
} else if (request.command == "play") {
stream.progress("Preparing to look at some ECL code...");
const messages = [
new vscode.LanguageModelChatSystemMessage("You are an ECL expert! You are also very knowledgable about HPCC."),
new vscode.LanguageModelChatUserMessage("Give small random ECL code samples. " + request.prompt)
];
const chatResponse = await vscode.lm.sendChatRequest(LANGUAGE_MODEL_ID, messages, {}, token);
for await (const fragment of chatResponse.stream) {
stream.markdown(fragment);
}
return { metadata: { command: "play" } };
} else {
const messages = [
new vscode.LanguageModelChatSystemMessage(`You are an ECL expert! Think carefully and step by step like an expert who is good at explaining something.
Your job is to explain computer science concepts in fun and entertaining way. Always start your response by stating what concept you are explaining. Always include code samples.`),
new vscode.LanguageModelChatUserMessage(request.prompt)
];
const chatResponse = await vscode.lm.sendChatRequest(LANGUAGE_MODEL_ID, messages, {}, token);
for await (const fragment of chatResponse.stream) {
// Process the output from the language model
stream.markdown(fragment);
}

return { metadata: { command: "" } };
}
};

// Chat participants appear as top-level options in the chat input
// when you type `@`, and can contribute sub-commands in the chat input
// that appear when you type `/`.
const eclagent = vscode.chat.createChatParticipant(ECLAGENT_PARTICIPANT_ID, handler);
eclagent.iconPath = vscode.Uri.joinPath(context.extensionUri, "resources/hpcc-icon.png");
eclagent.followupProvider = {
provideFollowups(result: IECLChatResult, context: vscode.ChatContext, token: vscode.CancellationToken) {
return [{
prompt: "let us learn",
label: vscode.l10n.t("Play with ECL Code"),
command: "play"
} satisfies vscode.ChatFollowup];
}
};

vscode.chat.registerChatVariableResolver("ECLAGENT_context", "Describes the state of mind and version of the ECL agent", {
resolve: (name, context, token) => {
if (name == "ECLAGENT_context") {
const mood = Math.random() > 0.5 ? "happy" : "grumpy";
return [
{
level: vscode.ChatVariableLevel.Short,
value: "version 1.3 " + mood
},
{
level: vscode.ChatVariableLevel.Medium,
value: "I am the ECL agent, version 1.3, and I am " + mood
},
{
level: vscode.ChatVariableLevel.Full,
value: "I am the ECL agent, version 1.3, and I am " + mood
}
];
}
}
});

context.subscriptions.push(
eclagent,
// Register the command handler for the do something followup
vscode.commands.registerTextEditorCommand(ECLAGENT_NAMES_COMMAND_ID, async (textEditor: vscode.TextEditor) => {
const text = textEditor.document.getText();
const messages = [
new vscode.LanguageModelChatSystemMessage(`You are an ECL expert! Think carefully and step by step.
Your job is to be as clear as possible and be encouraging. Be creative. IMPORTANT respond just with code. Do not use markdown!`),
new vscode.LanguageModelChatUserMessage(text)
];

let chatResponse: vscode.LanguageModelChatResponse | undefined;
try {
chatResponse = await vscode.lm.sendChatRequest(LANGUAGE_MODEL_ID, messages, {}, new vscode.CancellationTokenSource().token);

} catch (err) {
// making the chat request might fail because
// - model does not exist
// - user consent not given
// - quote limits exceeded
if (err instanceof vscode.LanguageModelError) {
console.log(err.message, err.code, err.cause);
}
return;
}

// Clear the editor content before inserting new content
await textEditor.edit(edit => {
const start = new vscode.Position(0, 0);
const end = new vscode.Position(textEditor.document.lineCount - 1, textEditor.document.lineAt(textEditor.document.lineCount - 1).text.length);
edit.delete(new vscode.Range(start, end));
});

// Stream the code into the editor as it is coming in from the Language Model
try {
for await (const fragment of chatResponse.stream) {
await textEditor.edit(edit => {
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
edit.insert(position, fragment);
});
}
} catch (err) {
// async response stream may fail, e.g network interruption or server side error
await textEditor.edit(edit => {
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
edit.insert(position, (<Error>err).message);
});
}
}),
);
}

// Get a random topic that the ECL agent has not taught in the chat history yet
function getTopic(history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>): string {
const topics = ["import", "modules", "functions", "transforms", "strings"];
// Filter the chat history to get only the responses from the ecl agent
const previousEclAgentResponses = history.filter(h => {
return h instanceof vscode.ChatResponseTurn && h.participant == ECLAGENT_PARTICIPANT_ID;
}) as vscode.ChatResponseTurn[];
// Filter the topics to get only the topics that have not been taught by the ecl agent yet
const topicsNoRepetition = topics.filter(topic => {
return !previousEclAgentResponses.some(eclAgentResponse => {
return eclAgentResponse.response.some(r => {
return r instanceof vscode.ChatResponseMarkdownPart && r.value.value.includes(topic);
});
});
});

return topicsNoRepetition[Math.floor(Math.random() * topicsNoRepetition.length)] || "I have taught you everything I know.";
}

export function deactivate() { }
7 changes: 4 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ export function activate(context: vscode.ExtensionContext): void {

initialize().then(() => {
return Promise.all([
import("./ecl/main").then(({ activate }) => activate(context)),
import("./kel/main").then(({ activate }) => activate(context)),
import("./dashy/main").then(({ activate }) => activate(context))
import("./ecl/main.js").then(({ activate }) => activate(context)),
import("./kel/main.js").then(({ activate }) => activate(context)),
import("./dashy/main.js").then(({ activate }) => activate(context)),
import("./chat/main.js").then(({ activate }) => activate(context))
]);
}).then(() => {
reporter.sendTelemetryEvent("initialized");
Expand Down
Loading

0 comments on commit 01c19cd

Please sign in to comment.