chatparticipant.ts•9.16 kB
// cspell: disable
import * as vscode from "vscode"
import { ExtensionState } from "./state"
import {
COPILOT_CHAT_PARTICIPANT_SCRIPT_ID,
COPILOT_CHAT_PARTICIPANT_ID,
ICON_LOGO_NAME,
MODEL_PROVIDER_GITHUB_COPILOT_CHAT,
} from "../../core/src/constants"
import { Fragment } from "../../core/src/generation"
import { convertAnnotationsToItems } from "../../core/src/annotations"
import { deleteUndefinedValues } from "../../core/src/cleaners"
import { patchCachedImages } from "../../core/src/filecache"
export async function activateChatParticipant(state: ExtensionState) {
const { context } = state
const { subscriptions } = context
const resolveReference = (
references: readonly vscode.ChatPromptReference[]
): { files: string[]; vars: Record<string, string> } => {
const files = []
const vars: Record<string, string> = {}
for (const reference of references) {
const { id, value } = reference
if (typeof value === "string") vars[id] = value
else if (value instanceof vscode.Uri)
files.push(vscode.workspace.asRelativePath(value, false))
else if (value instanceof vscode.Location)
files.push(vscode.workspace.asRelativePath(value.uri, false)) // TODO range
else
state.output.appendLine(
`unknown reference type: ${typeof value}`
)
}
return { files, vars }
}
const participant = vscode.chat.createChatParticipant(
COPILOT_CHAT_PARTICIPANT_ID,
async (
request: vscode.ChatRequest,
context: vscode.ChatContext,
response: vscode.ChatResponseStream,
token: vscode.CancellationToken
) => {
let { command, prompt, references, model } = request
const md = (t: string, ...enabledCommands: string[]) => {
const ms = new vscode.MarkdownString(t + "\n", true)
if (enabledCommands.length)
ms.isTrusted = {
enabledCommands,
}
response.markdown(ms)
}
// this command does not require any parsing
if (command === "models") {
const chatModels = await vscode.lm.selectChatModels()
const languageChatModels = await state.languageChatModels()
md(`This is the current model alias mapping:\n`)
for (const chatModel of chatModels) {
md(
`- \`${languageChatModels[chatModel.id] || "---"}\` > \`${chatModel.id}\`, ${chatModel.name}, max ${chatModel.maxInputTokens}\n`
)
}
return
}
// parse and analyze results
await state.parseWorkspace()
if (token.isCancellationRequested) return
const { project } = state
const templates = project.scripts
.filter((s) => !s.isSystem && !s.unlisted)
.sort((a, b) => a.id.localeCompare(b.id))
const mdHelp = () =>
md(
`\n\n[Docs](https://microsoft.github.io/genaiscript/reference/vscode/github-copilot-chat/) | [Samples](https://microsoft.github.io/genaiscript/samples/)\n`
)
const mdEmpty = () =>
md(
`\n😞 Oops, I could not find any genaiscript. [Create a new script](command:genaiscript.prompt.create)?\n`,
"genaiscript.prompt.create"
)
const mdTemplateList = () => {
templates.forEach((s) => {
response.markdown(`- \`${s.id}\`: `)
if (s.filename)
response.anchor(vscode.Uri.file(s.filename), s.id)
response.markdown(` ${s.title}\n`)
})
}
// list command
if (command === "list") {
if (templates.length) {
md("Use `@genaiscript /run <scriptid> ...` with:\n")
mdTemplateList()
} else {
mdEmpty()
}
mdHelp()
return
}
// try resolving template or handling run
let scriptid = ""
let template: PromptScript
if (command === "run") {
scriptid = prompt.split(" ")[0]
prompt = prompt.slice(scriptid.length).trim()
template = templates.find((t) => t.id === scriptid)
} else {
template = templates.find(
(t) => t.id === COPILOT_CHAT_PARTICIPANT_SCRIPT_ID
)
}
// tell user which templates are available
if (!template) {
if (scriptid === "")
md(`😓 Please specify a genaiscript to run.\n`)
else
md(
`😕 Oops, I could not find any genaiscript matching \`${scriptid}\`.\n`
)
if (templates.length === 0) {
mdEmpty()
} else {
md(`Try one of the following:\n`)
mdTemplateList()
}
mdHelp()
return
}
const { files, vars } = resolveReference(references)
const history = renderHistory(context)
const fragment: Fragment = {
files,
}
const canceller = token.onCancellationRequested(
async () => await state.cancelAiRequest()
)
const res = await state.requestAI({
scriptId: template.id,
label: "genaiscript agent",
parameters: deleteUndefinedValues({
...vars,
"copilot.history": history,
"copilot.model": `${MODEL_PROVIDER_GITHUB_COPILOT_CHAT}:${model.id}`,
question: prompt,
}),
githubCopilotChatModelId: model.id,
fragment,
mode: "chat",
})
canceller.dispose()
if (token.isCancellationRequested) return
const { text = "", status, statusText, runId } = res || {}
if (status !== "success") md("$(error) " + statusText)
if (text) {
let patched = convertAnnotationsToItems(text)
const dir = state.host.projectUri
patched = patchCachedImages(patched, (url) => {
const wurl = vscode.Uri.joinPath(dir, url).toString()
state.output.appendLine(`image: ${url}`)
state.output.appendLine(` ${wurl}`)
return wurl
})
md("\n\n" + patched)
}
// TODO open url
if (runId) {
const server = state.host.server
md(
`\n\n[Trace](${server.browserUrl}#scriptid=${template.id}&runid=${res.runId})`
)
}
}
)
participant.iconPath = new vscode.ThemeIcon(ICON_LOGO_NAME)
subscriptions.push(participant)
}
function renderHistory(
context: vscode.ChatContext
): (HistoryMessageUser | HistoryMessageAssistant)[] {
const { history } = context
if (!history?.length) return undefined
const res = history
.map((message) => {
if (message instanceof vscode.ChatRequestTurn) {
return {
role: "user",
content: message.prompt,
} satisfies HistoryMessageUser
} else if (message instanceof vscode.ChatResponseTurn) {
return {
role: "assistant",
name: message.participant,
content: message.response
.map((r) => {
if (r instanceof vscode.ChatResponseMarkdownPart) {
return r.value.value
} else if (
r instanceof vscode.ChatResponseAnchorPart
) {
if (r.value instanceof vscode.Uri)
return vscode.workspace.asRelativePath(
r.value.fsPath
)
else
return vscode.workspace.asRelativePath(
r.value.uri.fsPath
)
}
return ""
})
.join(""),
} as HistoryMessageAssistant
} else return undefined
})
.filter((f) => !!f)
return res.length ? res : undefined
}