-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add chat agent with online help contextual hints
Gordon Smith <[email protected]>
- Loading branch information
1 parent
a12cc97
commit 9cbf29a
Showing
16 changed files
with
806 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; | ||
} | ||
} | ||
|
Oops, something went wrong.