diff --git a/docs/genaisrc/image-alt-text.genai.js b/docs/genaisrc/image-alt-text.genai.mjs similarity index 94% rename from docs/genaisrc/image-alt-text.genai.js rename to docs/genaisrc/image-alt-text.genai.mjs index 9a06ecfa13..b10dadd16a 100644 --- a/docs/genaisrc/image-alt-text.genai.js +++ b/docs/genaisrc/image-alt-text.genai.mjs @@ -1,7 +1,7 @@ script({ title: "Image Alt Text generator", description: "Generate alt text for images", - model: "openai:gpt-4o", + model: "large", group: "docs", maxTokens: 4000, temperature: 0, diff --git a/docs/src/assets/chat-participant.png b/docs/src/assets/chat-participant.png new file mode 100644 index 0000000000..1524e242db Binary files /dev/null and b/docs/src/assets/chat-participant.png differ diff --git a/docs/src/assets/chat-participant.png.txt b/docs/src/assets/chat-participant.png.txt new file mode 100644 index 0000000000..374bc7423f --- /dev/null +++ b/docs/src/assets/chat-participant.png.txt @@ -0,0 +1 @@ +A screenshot of the chat participant window. \ No newline at end of file diff --git a/docs/src/content/docs/reference/scripts/system.mdx b/docs/src/content/docs/reference/scripts/system.mdx index f61ff569cb..b91538a697 100644 --- a/docs/src/content/docs/reference/scripts/system.mdx +++ b/docs/src/content/docs/reference/scripts/system.mdx @@ -78,84 +78,6 @@ script({ ..., GenAIScript comes with a number of system prompt that support features like creating files, extracting diffs or generating annotations. If unspecified, GenAIScript looks for specific keywords to activate the various system prompts. -### `copilot_chat_participant` - - - - - - - -`````js wrap title="copilot_chat_participant" -script({ - system: [ - // List of system components and tools available for the script - "system", - "system.tools", - "system.files", - "system.diagrams", - "system.annotations", - "system.git_info", - "system.github_info", - "system.safety_harmful_content", - ], - tools: ["agent"], // Tools that the script can use - group: "infrastructure", // Group categorization for the script - parameters: { - question: { - type: "string", // Type of the parameter - description: "the user question", // Description of the parameter - }, - "copilot.editor": { - type: "string", - description: "the content of the opened editor", - default: "", - }, - "copilot.selection": { - type: "string", - description: "the content of the opened editor", - default: "", - }, - }, - flexTokens: 20000, // Flexible token limit for the script -}) - -// Extract the 'question' parameter from the environment variables -const { question } = env.vars -const editor = env.vars["copilot.editor"] -const selection = env.vars["copilot.selection"] - -$`## task - -- make a plan to answer the QUESTION step by step using the information in the Context section -- answer the QUESTION - -## output - -- The final output will be inserted into the Visual Studio Code Copilot Chat window. -- do NOT include the plan in the output - -## guidance: -- use the agent tools to help you -- do NOT be lazy, always finish the tasks -- do NOT skip any steps -` - -// Define a variable QUESTION with the value of 'question' -def("QUESTION", question, { lineNumbers: false }) - -$`## Context` - -// Define a variable FILE with the file data from the environment variables -// The { ignoreEmpty: true, flex: 1 } options specify to ignore empty files and to use flexible token allocation -def("FILE", env.files, { lineNumbers: false, ignoreEmpty: true, flex: 1 }) - -if (editor) writeText(editor, { flex: 4 }) -if (selection) writeText(selection, { flex: 5 }) - -````` - - ### `system` Base system prompt diff --git a/docs/src/content/docs/reference/vscode/github-copilot-chat.mdx b/docs/src/content/docs/reference/vscode/github-copilot-chat.mdx new file mode 100644 index 0000000000..613fa64009 --- /dev/null +++ b/docs/src/content/docs/reference/vscode/github-copilot-chat.mdx @@ -0,0 +1,53 @@ +--- +title: GitHub Copilot Chat +sidebar: + order: 3 +--- + +import { Image } from "astro:assets" +import { Code } from "@astrojs/starlight/components" +import scriptSource from "../../../../../../packages/vscode/genaisrc/copilotchat.genai.mjs?raw" +import src from "../../../../assets/chat-participant.png" +import alt from "../../../../assets/chat-participant.png.txt?raw" + +The `@genaiscript` [chat participant](https://code.visualstudio.com/api/extension-guides/chat#parts-of-the-chat-user-experience) lets your run scripts without the context +of a [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) conversation. +This is useful for leverage existing scripts in an interactive chat session. + +{alt} + +## Choosing which script to run + +The `/run` command expects a script id as the first argument (e.g., `/run poem`). The rest of the query is +passed to the script as the `env.vars.question` variable. + +```sh +@genaiscript /run summarize +``` + +If you omit the `/run` command, GenAIScript will look for a script named `copilotchat`. If it finds one, it will run it. +Otherwise, it will propose you to create a new script. + +```sh +@genaiscript add comments to the current editor +``` + +## Context + +The context selected by the user in Copilot Chat is converted to variables and passed to the script: + +- the prompt content is passed in `env.vars.question`. The script id is removed in the case of `/run`. +- the current editor text is passed in `env.vars["copilot.editor"]` +- the current editor selection is passed in `env.vars["copilot.selection"]` +- the file references are passed in `env.files` + +## Default script + +The following script can used as a starter template to create the default script when the user does not use the `/run` command. + + diff --git a/docs/src/content/docs/reference/vscode/index.mdx b/docs/src/content/docs/reference/vscode/index.mdx index 99875b80e2..10a776e561 100644 --- a/docs/src/content/docs/reference/vscode/index.mdx +++ b/docs/src/content/docs/reference/vscode/index.mdx @@ -14,3 +14,4 @@ contains the latest stable release of the [extension](https://marketplace.visual - [Download](https://marketplace.visualstudio.com/items?itemName=genaiscript.genaiscript-vscode) - [Installation instructions](/genaiscript/getting-started/installation/#visual-studio-code-extension) +- [Copilot Chat Integration](/genaiscript/reference/copilot-chat/) \ No newline at end of file diff --git a/packages/core/src/annotations.ts b/packages/core/src/annotations.ts index 41e2d15b1d..16dc7152f3 100644 --- a/packages/core/src/annotations.ts +++ b/packages/core/src/annotations.ts @@ -18,6 +18,13 @@ const AZURE_DEVOPS_ANNOTATIONS_RX = // Example: foo.ts:10:error TS1005: ';' expected. const TYPESCRIPT_ANNOTATIONS_RX = /^(?[^:\s].*?):(?\d+)(?::(?\d+))?(?::\d+)?\s+-\s+(?error|warning)\s+(?[^:]+)\s*:\s*(?.*)$/gim +// Maps severity strings to `DiagnosticSeverity`. +const SEV_MAP: Record = Object.freeze({ + ["info"]: "info", + ["notice"]: "info", // Maps 'notice' to 'info' severity + ["warning"]: "warning", + ["error"]: "error", +}) /** * Parses annotations from TypeScript, GitHub Actions, and Azure DevOps. @@ -28,20 +35,12 @@ const TYPESCRIPT_ANNOTATIONS_RX = export function parseAnnotations(text: string): Diagnostic[] { if (!text) return [] - // Maps severity strings to `DiagnosticSeverity`. - const sevMap: Record = { - ["info"]: "info", - ["notice"]: "info", // Maps 'notice' to 'info' severity - ["warning"]: "warning", - ["error"]: "error", - } - // Helper function to add an annotation to the set. // Extracts groups from the regex match and constructs a `Diagnostic` object. const addAnnotation = (m: RegExpMatchArray) => { const { file, line, endLine, severity, code, message } = m.groups const annotation: Diagnostic = { - severity: sevMap[severity?.toLowerCase()] ?? "info", // Default to "info" if severity is missing + severity: SEV_MAP[severity?.toLowerCase()] ?? "info", // Default to "info" if severity is missing filename: file, range: [ [parseInt(line) - 1, 0], // Start of range, 0-based index @@ -72,6 +71,23 @@ export function eraseAnnotations(text: string) { ].reduce((t, rx) => t.replace(rx, ""), text) } +export function convertAnnotationsToItems(text: string) { + return [ + TYPESCRIPT_ANNOTATIONS_RX, + GITHUB_ANNOTATIONS_RX, + AZURE_DEVOPS_ANNOTATIONS_RX, + ].reduce( + (t, rx) => + t.replace(rx, (s) => { + const m = rx.exec(s) + if (!m) return s + const { file, line, severity, code, message } = m.groups + return `- ${SEV_MAP[severity?.toLowerCase()] ?? "info"}: ${message} (${file}#L${line} ${code || ""})` + }), + text + ) +} + /** * Converts a `Diagnostic` to a GitHub Action command string. * diff --git a/packages/core/src/chat.ts b/packages/core/src/chat.ts index 6abb09bb0c..510fa3ff96 100644 --- a/packages/core/src/chat.ts +++ b/packages/core/src/chat.ts @@ -833,7 +833,7 @@ export function appendSystemMessage( if (last?.role !== "system") { last = { role: "system", - content, + content: "", } as ChatCompletionSystemMessageParam messages.unshift(last) } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index cad5a57a1c..6d6373b2d0 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -84,7 +84,8 @@ export const CACHE_LLMREQUEST_PREFIX = "genaiscript/cache/llm/" export const CACHE_AIREQUEST_PREFIX = "genaiscript/cache/ai/" export const TRACE_NODE_PREFIX = "genaiscript/trace/" export const EXTENSION_ID = "genaiscript.genaiscript-vscode" -export const CHAT_PARTICIPANT_ID = TOOL_ID +export const COPILOT_CHAT_PARTICIPANT_ID = TOOL_ID +export const COPILOT_CHAT_PARTICIPANT_SCRIPT_ID = "copilotchat" export const BING_SEARCH_ENDPOINT = "https://api.bing.microsoft.com/v7.0/search" export const SYSTEM_FENCE = "\n" export const MAX_DATA_REPAIRS = 1 diff --git a/packages/core/src/promptdom.ts b/packages/core/src/promptdom.ts index 6079da628d..36a89e7dc1 100644 --- a/packages/core/src/promptdom.ts +++ b/packages/core/src/promptdom.ts @@ -11,20 +11,19 @@ import { MARKDOWN_PROMPT_FENCE, PROMPT_FENCE, PROMPTY_REGEX, - SYSTEM_FENCE, TEMPLATE_ARG_DATA_SLICE_SAMPLE, TEMPLATE_ARG_FILE_MAX_TOKENS, } from "./constants" import { parseModelIdentifier } from "./models" -import { appendAssistantMessage, appendUserMessage } from "./chat" +import { + appendAssistantMessage, + appendSystemMessage, + appendUserMessage, +} from "./chat" import { errorMessage } from "./error" import { tidyData } from "./tidy" import { dedent } from "./indent" -import { - ChatCompletionMessageParam, - ChatCompletionSystemMessageParam, - ChatCompletionUserMessageParam, -} from "./chattypes" +import { ChatCompletionMessageParam } from "./chattypes" import { resolveTokenEncoder } from "./encoders" import { expandFiles } from "./fs" import { interpolateVariables } from "./mustache" @@ -938,17 +937,8 @@ export async function renderPromptNode( if (truncated) await tracePromptNode(trace, node, { label: "truncated" }) const messages: ChatCompletionMessageParam[] = [] - const appendSystem = (content: string) => { - const last = messages.find( - ({ role }) => role === "system" - ) as ChatCompletionSystemMessageParam - if (last) last.content += content + SYSTEM_FENCE - else - messages.push({ - role: "system", - content, - } as ChatCompletionSystemMessageParam) - } + const appendSystem = (content: string) => + appendSystemMessage(messages, content) const appendUser = (content: string) => appendUserMessage(messages, content) const appendAssistant = (content: string) => appendAssistantMessage(messages, content) diff --git a/packages/sample/genaisrc/bicep-best-practices.genai.mjs b/packages/sample/genaisrc/bicep-best-practices.genai.mjs index f1682b67aa..ff70946515 100644 --- a/packages/sample/genaisrc/bicep-best-practices.genai.mjs +++ b/packages/sample/genaisrc/bicep-best-practices.genai.mjs @@ -1,13 +1,14 @@ script({ title: "Bicep Best Practices", temperature: 0, + system: ["system", "system.annotations"] }) def("FILE", env.files, { endsWith: ".bicep" }) $`You are an expert at Azure Bicep. -Review the bicep in FILE and generate annotations to enhance the script base on best practices +Review the bicep in FILE and generate errors to enhance the script base on best practices (https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices). - Generate the top 3 most important annotations. diff --git a/packages/core/src/genaisrc/copilot_chat_participant.genai.mjs b/packages/vscode/genaisrc/copilotchat.genai.mts similarity index 69% rename from packages/core/src/genaisrc/copilot_chat_participant.genai.mjs rename to packages/vscode/genaisrc/copilotchat.genai.mts index 20c91f0ba8..70d1f151d0 100644 --- a/packages/core/src/genaisrc/copilot_chat_participant.genai.mjs +++ b/packages/vscode/genaisrc/copilotchat.genai.mts @@ -1,30 +1,39 @@ script({ + model: "large", system: [ // List of system components and tools available for the script "system", + "system.safety_harmful_content", + "system.safety_jailbreak", + "system.safety_protected_material", "system.tools", "system.files", + "system.files_schema", "system.diagrams", "system.annotations", "system.git_info", "system.github_info", "system.safety_harmful_content", + "system.agent_fs", + "system.agent_git", + "system.agent_github", + "system.agent_interpreter", + "system.agent_docs", ], - tools: ["agent"], // Tools that the script can use - group: "infrastructure", // Group categorization for the script + group: "copilot", // Group categorization for the script parameters: { question: { - type: "string", // Type of the parameter - description: "the user question", // Description of the parameter + type: "string", + description: "the user question", }, "copilot.editor": { type: "string", - description: "the content of the opened editor", + description: "the content of the opened editor, if any", default: "", }, "copilot.selection": { type: "string", - description: "the content of the opened editor", + description: "the content of the opened editor, if any", default: "", }, }, @@ -60,6 +69,5 @@ $`## Context` // Define a variable FILE with the file data from the environment variables // The { ignoreEmpty: true, flex: 1 } options specify to ignore empty files and to use flexible token allocation def("FILE", env.files, { lineNumbers: false, ignoreEmpty: true, flex: 1 }) - -if (editor) writeText(editor, { flex: 4 }) -if (selection) writeText(selection, { flex: 5 }) +def("EDITOR", editor, { flex: 4, ignoreEmpty: true }) +def("SELECTION", selection, { flex: 5, ignoreEmpty: true }) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 4524eb7dff..d302da30e1 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -59,11 +59,12 @@ "name": "genaiscript", "fullName": "GenAIScript", "isSticky": false, - "disambiguation": [ + "commands": [ { - "category": "workspace_question", - "description": "Runs GenAIScript agents defined in the current respository.", - "examples": [] + "name": "run", + "description": "Runs a GenAIScript script. The query should start with the script filename without the extension.", + "isSticky": false, + "sampleRequest": "/run poem" } ] } @@ -249,6 +250,10 @@ { "command": "genaiscript.request.status", "when": "false" + }, + { + "command": "genaiscript.samples.download", + "when": "false" } ], "view/title": [ diff --git a/packages/vscode/postpackage.mjs b/packages/vscode/postpackage.mjs index 397fe82208..be7fa8eb3b 100644 --- a/packages/vscode/postpackage.mjs +++ b/packages/vscode/postpackage.mjs @@ -2,9 +2,7 @@ import "zx/globals" const pkg = await fs.readJSON("./package.json") pkg.enabledApiProposals = pkg._enabledApiProposals -pkg.contributes.chatParticipants = pkg._chatParticipants pkg.displayName = "GenAIScript Insiders" delete pkg._enabledApiProposals -delete pkg._chatParticipants await fs.writeJSON("./package.json", pkg, { spaces: 4 }) console.log(`cleaned package.json`) diff --git a/packages/vscode/prepackage.mjs b/packages/vscode/prepackage.mjs index c1bc41d56f..e0c0d452c3 100644 --- a/packages/vscode/prepackage.mjs +++ b/packages/vscode/prepackage.mjs @@ -2,7 +2,6 @@ import "zx/globals" const pkg = await fs.readJSON("./package.json") pkg._enabledApiProposals = pkg.enabledApiProposals -pkg._chatParticipants = pkg.contributes.chatParticipants pkg.displayName = "GenAIScript" delete pkg.enabledApiProposals await fs.writeJSON("./package.json", pkg, { spaces: 4 }) diff --git a/packages/vscode/src/chatparticipant.ts b/packages/vscode/src/chatparticipant.ts index 058afc9e60..991cda5db2 100644 --- a/packages/vscode/src/chatparticipant.ts +++ b/packages/vscode/src/chatparticipant.ts @@ -1,9 +1,14 @@ import * as vscode from "vscode" import { ExtensionState } from "./state" -import { TOOL_ID } from "../../core/src/constants" +import { + COPILOT_CHAT_PARTICIPANT_SCRIPT_ID, + COPILOT_CHAT_PARTICIPANT_ID, + ICON_LOGO_NAME, +} from "../../core/src/constants" import { Fragment } from "../../core/src/generation" -import { prettifyMarkdown } from "../../core/src/markdown" -import { eraseAnnotations } from "../../core/src/annotations" +import { cleanMarkdown } from "../../core/src/markdown" +import { convertAnnotationsToItems } from "../../core/src/annotations" +import { dedent } from "../../core/src/indent" export async function activateChatParticipant(state: ExtensionState) { const { context } = state @@ -26,21 +31,48 @@ export async function activateChatParticipant(state: ExtensionState) { } const participant = vscode.chat.createChatParticipant( - TOOL_ID, + COPILOT_CHAT_PARTICIPANT_ID, async ( request: vscode.ChatRequest, context: vscode.ChatContext, response: vscode.ChatResponseStream, token: vscode.CancellationToken ) => { - const { command, prompt, references } = request - if (command) throw new Error("Command not supported") + let { command, prompt, references, model } = request if (!state.project) await state.parseWorkspace() if (token.isCancellationRequested) return - const template = state.project.templates.find( - (t) => t.id === "copilot_chat_participant" - ) + let template: PromptScript + if (command === "run") { + const scriptid = prompt.split(" ")[0] + prompt = prompt.slice(scriptid.length).trim() + template = state.project.templates.find( + (t) => t.id === scriptid + ) + if (!template) { + response.markdown(dedent`Oops, I could not find any genaiscript matching \`${scriptid}\`. Try one of the following: + ${state.project.templates + .filter((s) => !s.system && !s.unlisted) + .map((s) => `- \`${s.id}\`: ${s.title}`) + .join("\n")} + `) + return + } + } else { + template = state.project.templates.find( + (t) => t.id === COPILOT_CHAT_PARTICIPANT_SCRIPT_ID + ) + if (!template) { + response.markdown( + dedent` + The \`${COPILOT_CHAT_PARTICIPANT_SCRIPT_ID}\` script has not been configured yet in this workspace. + + Save [starter template](https://microsoft.github.io/genaiscript/reference/vscode/github-copilot-chat#copilotchat) in your workspace to get started. + ` + ) + return + } + } const { files, vars } = resolveReference(references) const fragment: Fragment = { files, @@ -65,12 +97,13 @@ export async function activateChatParticipant(state: ExtensionState) { const { text = "" } = res || {} response.markdown( new vscode.MarkdownString( - prettifyMarkdown(eraseAnnotations(text)), + cleanMarkdown(convertAnnotationsToItems(text)), true ) ) } ) + participant.iconPath = new vscode.ThemeIcon(ICON_LOGO_NAME) subscriptions.push(participant) } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 53e14f2052..a37be7f923 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -30,7 +30,7 @@ export async function activate(context: ExtensionContext) { const state = new ExtensionState(context) activatePromptCommands(state) activateFragmentCommands(state) - // activateSamplesCommands(state) + activateSamplesCommands(state) activateMarkdownTextDocumentContentProvider(state) activatePromptTreeDataProvider(state) activateConnectionInfoTree(state) diff --git a/packages/vscode/src/promptcommands.ts b/packages/vscode/src/promptcommands.ts index cc741e9a71..6efec3c84b 100644 --- a/packages/vscode/src/promptcommands.ts +++ b/packages/vscode/src/promptcommands.ts @@ -37,7 +37,7 @@ export function activatePromptCommands(state: ExtensionState) { ), registerCommand( "genaiscript.prompt.fork", - async (template: PromptScript) => { + async (template: PromptScript | string) => { if (!template) { if (!state.project) await state.parseWorkspace() const templates = state.project?.templates @@ -50,14 +50,17 @@ export function activatePromptCommands(state: ExtensionState) { ) if (picked === undefined) return template = picked.template + } else if (typeof template === "string") { + if (!state.project) await state.parseWorkspace() + template = state.project?.templates.find( + (t) => t.id === template + ) } - const name = await vscode.window.showInputBox({ - title: `Pick a file name for the new GenAIScript script.`, - value: template.id, - }) - if (name === undefined) return await showPrompt( - await copyPrompt(template, { fork: true, name }) + await copyPrompt(template, { + fork: true, + name: template.id, + }) ) } ), diff --git a/packages/vscode/src/samplescommands.ts b/packages/vscode/src/samplescommands.ts index 0c5b347156..f15a51d862 100644 --- a/packages/vscode/src/samplescommands.ts +++ b/packages/vscode/src/samplescommands.ts @@ -9,7 +9,7 @@ import { writeFile } from "./fs" export function activateSamplesCommands(state: ExtensionState) { const { context, host } = state - registerCommand("genaiscript.samples.download", async () => { + registerCommand("genaiscript.samples.download", async (name: string) => { const dir = Utils.joinPath(context.extensionUri, GENAI_SRC) const files = await vscode.workspace.fs.readDirectory( Utils.joinPath(context.extensionUri, GENAI_SRC) @@ -31,16 +31,18 @@ export function activateSamplesCommands(state: ExtensionState) { ) ).map((s) => ({ ...s, meta: parsePromptScriptMeta(s.jsSource) })) - const res = await vscode.window.showQuickPick< - vscode.QuickPickItem & { filename: string; jsSource: string } - >( - samples.map((s) => ({ - label: s.meta.title, - detail: s.meta.description, - ...s, - })), - { title: "Pick a sample to download" } - ) + const res = + samples.find((s) => s.filename === name) || + (await vscode.window.showQuickPick< + vscode.QuickPickItem & { filename: string; jsSource: string } + >( + samples.map((s) => ({ + label: s.meta.title, + detail: s.meta.description, + ...s, + })), + { title: "Pick a sample to download" } + )) if (res === undefined) return const { jsSource, filename } = res diff --git a/packages/vscode/src/state.ts b/packages/vscode/src/state.ts index 595d89f76b..fe268a4bb6 100644 --- a/packages/vscode/src/state.ts +++ b/packages/vscode/src/state.ts @@ -342,7 +342,7 @@ stats/ vscode.commands.executeCommand( "workbench.view.extension.genaiscript" ) - if (options.mode !== "notebook" && !hasOutputOrTraceOpened()) + if (!options.mode && !hasOutputOrTraceOpened()) vscode.commands.executeCommand("genaiscript.request.open.output") r.request .then((resp) => {