Skip to content

Commit

Permalink
feat: Add chat agent with online help contextual hints
Browse files Browse the repository at this point in the history
Gordon Smith <[email protected]>
  • Loading branch information
GordonSmith committed Aug 22, 2024
1 parent a12cc97 commit 9cbf29a
Show file tree
Hide file tree
Showing 16 changed files with 806 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ The following ECL specific commands are available. Note: These commands will *
| Submit (No Archive) | ctrl/cmd+F5 | Submit raw ECL without creating an archive |
| Verify ECL Signature | | Verify ECL Digital Signature |
| Language Reference Lookup | shift+F1 | For the currently selected text, search the online ECL language reference |
| Insert Record Definition | ctrl/cmd+I R | Fetches record definition for given logical file |
| Insert Record Definition | ctrl/cmd+R | Fetches record definition for given logical file |

#### Within the ECL Code Editor Tab Context Menu:

Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"@types/react": "17.0.80",
"@types/react-dom": "17.0.25",
"@types/tmp": "0.2.6",
"@types/vscode": "1.76.0",
"@types/vscode": "1.92.0",
"@types/vscode-notebook-renderer": "1.72.3",
"@typescript-eslint/eslint-plugin": "8.2.0",
"@typescript-eslint/parser": "8.2.0",
Expand Down Expand Up @@ -116,7 +116,7 @@
"url": "https://github.com/hpcc-systems/vscode-ecl.git"
},
"engines": {
"vscode": "^1.76.0"
"vscode": "^1.92.0"
},
"galleryBanner": {
"color": "#CFB69A",
Expand Down Expand Up @@ -205,6 +205,25 @@
"path": "./snippets/kel.json"
}
],
"chatParticipants": [
{
"id": "chat.ecl",
"fullName": "ECL",
"name": "ecl",
"description": "HPCC-Platform Assistant to help with ECL development",
"isSticky": true,
"commands": [
{
"name": "create",
"description": "Create a new ECL file"
},
{
"name": "docs",
"description": "Information about the ECL language"
}
]
}
],
"viewsWelcome": [],
"commands": [
{
Expand Down
53 changes: 53 additions & 0 deletions src/ecl/chat/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as vscode from "vscode";

export const ECL_COMMAND_ID = "ecl";
export const PROCESS_COPILOT_CREATE_CMD = "ecl.createFiles";
export const PROCESS_COPILOT_CREATE_CMD_TITLE = "Create ECL file";
export const COPILOT_CREATE_CMD = "ECL file";

export const OWNER = "hpcc-systems";
export const REPO = "HPCC-Platform";
export const BRANCH = "master";
export const SAMPLE_COLLECTION_URL = `https://cdn.jsdelivr.net/gh/${OWNER}/${REPO}@${BRANCH}/`;

export const MODEL_VENDOR: string = "copilot";

enum LANGUAGE_MODEL_ID {
GPT_3 = "gpt-3.5-turbo",
GPT_4 = "gpt-4",
GPT_4o = "gpt-4o"
}

export const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: MODEL_VENDOR, family: LANGUAGE_MODEL_ID.GPT_4o };

export const FETCH_ISSUE_DETAIL_CMD = "Fetch Issue Details Command";

export enum commands {
DOCS = "docs",
ISSUES = "issues",
}

const GREETINGS = [
"Let me think how I can assist you... 🤔",
"Just a moment, I'm pondering... 💭",
"Give me a second, I'm working on it... ⏳",
"Hold on, let me figure this out... 🧐",
"One moment, I'm processing your request... ⏲️",
"Checking inside Gavins brain... 💭",
"Dans the man for this... 🧐",
"Working on your request... 🚀",
"Lets see what schmoo can do... 🕵️‍♂️",
"Let's see what we can do... 🕵️‍♂️",
"Let's get this sorted... 🗂️",
"Calling Jake for an answer... 💭",
"Hang tight, I'm on the case... 🕵️‍♀️",
"Analyzing the situation... 📊",
"Preparing the solution... 🛠️",
"Searching for the answer... 🔍",
"Maybe Mark knows... 🤔",
"Investigating the problem... 🕵️‍♂️"
];

export const getRandomGreeting = () => {
return GREETINGS[Math.floor(Math.random() * GREETINGS.length)];
};
138 changes: 138 additions & 0 deletions src/ecl/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as vscode from "vscode";
import { commands, getRandomGreeting } from "./constants";
import localize from "../../util/localize";
import { handleDocsCommand } from "./prompts/docs";
import { handleIssueManagement } from "./prompts/issues";

const ECL_PARTICIPANT_ID = "chat.ecl";

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

function handleError(logger: vscode.TelemetryLogger, err: any, stream: vscode.ChatResponseStream): void {
// making the chat request might fail because
// - model does not exist
// - user consent not given
// - quote limits exceeded
logger.logError(err);

if (err instanceof vscode.LanguageModelError) {
console.log(err.message, err.code, err.cause);
if (err.cause instanceof Error && err.cause.message.includes("off_topic")) {
stream.markdown(localize("I'm sorry, I can only explain computer science concepts."));
}
} else {
// re-throw other errors so they show up in the UI
throw err;
}
}

// Get a random topic that the cat has not taught in the chat history yet
function getTopic(history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>): string {
const topics = ["linked list", "recursion", "stack", "queue", "pointers"];
// Filter the chat history to get only the responses from the cat
const previousCatResponses = history.filter(h => {
return h instanceof vscode.ChatResponseTurn && h.participant === ECL_PARTICIPANT_ID;
}) as vscode.ChatResponseTurn[];
// Filter the topics to get only the topics that have not been taught by the cat yet
const topicsNoRepetition = topics.filter(topic => {
return !previousCatResponses.some(catResponse => {
return catResponse.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. Meow!";
}

let eclChat: ECLChat;

export class ECLChat {
protected constructor(ctx: vscode.ExtensionContext) {
const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, ctx: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<IECLChatResult> => {
let cmdResult: any;
stream.progress(localize(getRandomGreeting()));
try {
if (request.command === commands.ISSUES) {
cmdResult = await handleIssueManagement(request, stream, token);
logger.logUsage("request", { kind: commands.ISSUES });
} else {
cmdResult = await handleDocsCommand(request, stream, token);
}
} catch (err) {
handleError(logger, err, stream);
}

return {
metadata: {
command: request.command || "",
},
};

};

const chatParticipant = vscode.chat.createChatParticipant(ECL_PARTICIPANT_ID, handler);
chatParticipant.iconPath = vscode.Uri.joinPath(ctx.extensionUri, "resources/hpcc-icon.png");

chatParticipant.followupProvider = {
provideFollowups(result: IECLChatResult, context: vscode.ChatContext, token: vscode.CancellationToken) {
return [
// {
// prompt: "Sample Followup?",
// label: localize("Do the followup."),
// command: commands.DOCS
// } satisfies vscode.ChatFollowup
];
}
};

const logger = vscode.env.createTelemetryLogger({
sendEventData(eventName, data) {
// Capture event telemetry
console.log(`Event: ${eventName}`);
console.log(`Data: ${JSON.stringify(data)}`);
},
sendErrorData(error, data) {
// Capture error telemetry
console.error(`Error: ${error}`);
console.error(`Data: ${JSON.stringify(data)}`);
}
});

ctx.subscriptions.push(chatParticipant.onDidReceiveFeedback((feedback: vscode.ChatResultFeedback) => {
// Log chat result feedback to be able to compute the success matric of the participant
// unhelpful / totalRequests is a good success metric
logger.logUsage("chatResultFeedback", {
kind: feedback.kind
});
}));

// TODO: Chat Commands ---
// ctx.subscriptions.push(
// chatParticipant,
// vscode.commands.registerCommand(
// "ecl.todo",
// async (filesToCreate) => {
// await createFolderAndFiles(filesToCreate);
// }
// ),
// vscode.commands.registerCommand(FETCH_ISSUE_DETAIL_CMD, async (githubIssue) => {
// vscode.commands.executeCommand(`workbench.action.chat.open`, `@${AEM_COMMAND_ID} /${commands.ISSUES} fetch me details of issue #${githubIssue.number}`);
// })
// );
}

static attach(ctx: vscode.ExtensionContext): ECLChat {
if (!eclChat) {
eclChat = new ECLChat(ctx);
}
return eclChat;
}
}

export function deactivate() { }

65 changes: 65 additions & 0 deletions src/ecl/chat/prompts/docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as vscode from "vscode";
import { AssistantMessage, BasePromptElementProps, PromptElement, PromptSizing, TextChunk, UserMessage, } from "@vscode/prompt-tsx";
import { commands, MODEL_SELECTOR } from "../constants";
import { getChatResponse } from "../utils/index";
import { fetchContext, fetchIndexes, Hit, matchTopics } from "../../docs";
import * as prompts from "./templates/default";

export interface PromptProps extends BasePromptElementProps {
userQuery: string;
}

export interface DocsPromptProps extends PromptProps {
hits: Hit[]
}

export class DocsPrompt extends PromptElement<DocsPromptProps, any> {

render(state: void, sizing: PromptSizing) {
return (
<>
<AssistantMessage priority={1000}>{prompts.SYSTEM_MESSAGE}</AssistantMessage>
<UserMessage priority={500}>
{this.props.hits.map((hit, idx) => (
<TextChunk breakOn=' '>
{JSON.stringify(hit)}
</TextChunk>
))}
</UserMessage>
<UserMessage priority={1000}>{this.props.userQuery}</UserMessage>
</>
);
}
}

export async function handleDocsCommand(request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<{ metadata: { command: string, hits: Hit[] } }> {
const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);
if (model) {
const hits = await fetchContext(request.prompt);
let promptProps: DocsPromptProps;
if (!hits.length) {
promptProps = {
userQuery: `Suggest several (more 3 or more) web links that exist in the previous html content above that might help with the following question "${request.prompt}". The user can not see the above content. Explain why they might be helpful`,
hits: await fetchIndexes()
};
} else {
promptProps = {
userQuery: request.prompt,
hits,
};
}

const chatResponse = await getChatResponse(DocsPrompt, promptProps, token);
for await (const fragment of chatResponse.text) {
stream.markdown(fragment);
}

return {
metadata: {
command: commands.DOCS,
hits: promptProps.hits,
},
};
}
}

Loading

0 comments on commit 9cbf29a

Please sign in to comment.