|
3 | 3 | * Licensed under the MIT License. See License.md in the project root for license information. |
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 |
|
6 | | -import * as vscode from "vscode"; |
| 6 | +import type { CopilotClient, CopilotSession } from "@github/copilot-sdk"; |
| 7 | +import type * as vscode from "vscode"; |
7 | 8 | import { InvalidCopilotResponseError } from "../errors"; |
8 | 9 |
|
9 | | -const languageModelPreference: { vendor: "copilot", family: string }[] = [ |
10 | | - // not yet seen/available |
11 | | - // { vendor: "copilot", family: "gpt-4-turbo-preview" }, |
12 | | - // seen/available |
13 | | - { vendor: "copilot", family: "gpt-4o" }, |
14 | | - { vendor: "copilot", family: "gpt-4-turbo" }, |
15 | | - { vendor: "copilot", family: "gpt-4" }, |
16 | | - { vendor: "copilot", family: "gpt-3.5-turbo" }, |
17 | | -]; |
18 | | - |
19 | | -async function selectMostPreferredLm(): Promise<vscode.LanguageModelChat | undefined> { |
20 | | - const lms = await vscode.lm.selectChatModels({ vendor: "copilot" }); |
21 | | - const lmsInPreferredOrder = (lms || []) |
22 | | - .filter((lm) => languageModelPreference.some((pref) => pref.family === lm.family && pref.vendor === lm.vendor)) |
23 | | - .sort((a, b) => languageModelPreference.findIndex((pref) => pref.family === a.family && pref.vendor === a.vendor) - languageModelPreference.findIndex((pref) => pref.family === b.family && pref.vendor === b.vendor)); |
24 | | - return lmsInPreferredOrder?.at(0); |
| 10 | +let client: CopilotClient | undefined; |
| 11 | +let session: CopilotSession | undefined; |
| 12 | + |
| 13 | +async function loadCopilotSdk(): Promise<typeof import("@github/copilot-sdk")> { |
| 14 | + return await import("@github/copilot-sdk"); |
25 | 15 | } |
26 | 16 |
|
27 | | -export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], relevantContext?: string): string { |
28 | | - return `The User is asking you to choose an item based on the following information: |
29 | | - 1. Choose from this list of picks ${picks.join(", ")}. |
30 | | - 2. You must choose one item from the list |
31 | | - 3. Use information from this context, if available, to make your decision: ${relevantContext ? relevantContext : "No context available"} |
32 | | - 4. If there is an option to skipForNow, you can choose that option. |
33 | | - Respond with a JSON object of the pick you have chosen. Do not respond in a conversational tone, only JSON. `; |
| 17 | +function getCopilotCliPath(): string { |
| 18 | + return require.resolve(`@github/copilot-${process.platform}-${process.arch}`); |
| 19 | +} |
| 20 | + |
| 21 | +export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], placeholder?: string): string { |
| 22 | + return ` |
| 23 | + Task: choose one pick. |
| 24 | +
|
| 25 | + Input: |
| 26 | + picks: ${JSON.stringify(picks)} |
| 27 | + placeholder: ${placeholder ?? null} |
| 28 | +
|
| 29 | + Rules: |
| 30 | + - If the picks represent container image tags, always prefer the newest available image tag. |
| 31 | +
|
| 32 | + Output: |
| 33 | + Return ONLY the JSON object with fields "label" and "description". |
| 34 | + `; |
34 | 35 | } |
35 | 36 |
|
36 | 37 | export function createPrimaryPromptToGetPickManyQuickPickInput(picks: string[], relevantContext?: string): string { |
@@ -63,35 +64,68 @@ export function createPrimaryPromptForWorkspaceFolderPick(folders: readonly vsco |
63 | 64 | Respond with the workspace folder you have chosen. Do not respond in a conversational tone. `; |
64 | 65 | } |
65 | 66 |
|
66 | | -export async function doCopilotInteraction(primaryPrompt: string): Promise<string> { |
67 | | - let messages: vscode.LanguageModelChatMessage[] = []; |
68 | | - const lm: vscode.LanguageModelChat | undefined = await selectMostPreferredLm(); |
69 | | - |
70 | | - messages = [ |
71 | | - new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.User, primaryPrompt) |
72 | | - ]; |
73 | | - |
74 | | - if (lm !== undefined) { |
75 | | - const chatRequestOptions = { justification: `Access to Copilot for the @azure agent.` }; |
76 | | - const request = await lm.sendRequest(messages, chatRequestOptions); |
77 | | - const fragments: string[] = []; |
78 | | - try { |
79 | | - // Consume the stream and collect fragments |
80 | | - for await (const fragment of request.text) { |
81 | | - fragments.push(fragment); |
82 | | - } |
83 | | - |
84 | | - // Combine fragments into a single string |
85 | | - const responseText = fragments.join(""); |
86 | | - const cleanedResponse = extractJsonString(responseText); |
87 | | - return cleanedResponse; |
88 | | - } catch { |
89 | | - throw new InvalidCopilotResponseError(); |
90 | | - } |
| 67 | +export async function doGithubCopilotInteraction(primaryPrompt: string, relevantContext?: string): Promise<string> { |
| 68 | + const session = await getCopilotSession(relevantContext); |
| 69 | + const response = await session.sendAndWait({ |
| 70 | + prompt: primaryPrompt, |
| 71 | + mode: "immediate" |
| 72 | + }); |
| 73 | + |
| 74 | + if (!response || !response.data) { |
| 75 | + throw new InvalidCopilotResponseError(); |
| 76 | + } |
| 77 | + |
| 78 | + return response?.data.content; |
| 79 | +} |
| 80 | + |
| 81 | +export async function getCopilotSession(relevantContext?: string): Promise<CopilotSession> { |
| 82 | + if (session) { |
| 83 | + return session; |
| 84 | + } |
| 85 | + |
| 86 | + const { CopilotClient } = await loadCopilotSdk(); |
| 87 | + client = new CopilotClient({ cliPath: getCopilotCliPath() }); |
| 88 | + session = await client.createSession({ |
| 89 | + onPermissionRequest: () => ({ kind: "approved" }) |
| 90 | + }); |
| 91 | + const activityChildren = extractActivityChildren(relevantContext || ''); |
| 92 | + const subscriptionId = relevantContext ? extractSubscriptionIdFromContext(relevantContext) : undefined; |
| 93 | + |
| 94 | + await session.sendAndWait({ |
| 95 | + mode: "immediate", |
| 96 | + prompt: `Picker. Choose one. Never explain your reasoning. Never use markdown. Output: {"label":"X","description":"Y"} |
| 97 | +
|
| 98 | + Rules (priority) |
| 99 | + 1. Match activityChildren "Use X" → pick X |
| 100 | + 2. Match subscriptionId → pick.data /subscriptions/{id} |
| 101 | + 3. Else skipForNow |
| 102 | +
|
| 103 | + Context: ${JSON.stringify({ activityChildren, subscriptionId, relevantContext })}` |
| 104 | + }); |
| 105 | + |
| 106 | + return session; |
| 107 | +} |
| 108 | + |
| 109 | +export async function disposeCopilotSession(): Promise<void> { |
| 110 | + if (client) { |
| 111 | + await client.stop(); |
| 112 | + client = undefined; |
| 113 | + session = undefined; |
91 | 114 | } |
92 | | - return ""; |
93 | 115 | } |
94 | 116 |
|
95 | | -function extractJsonString(raw: string): string { |
96 | | - return raw.replace(/```json\s*|```/g, '').trim(); |
| 117 | +function extractSubscriptionIdFromContext(context: string): string | undefined { |
| 118 | + const regex = /https:\/\/management\.[^/]+\/subscriptions\/([0-9a-fA-F-]{36})/; |
| 119 | + const match = context.match(regex); |
| 120 | + return match ? match[1] : undefined; |
| 121 | +} |
| 122 | + |
| 123 | +function extractActivityChildren(context: string): string | undefined { |
| 124 | + try { |
| 125 | + const activityLog = JSON.parse(context) as { children?: unknown }; |
| 126 | + const children = activityLog.children; |
| 127 | + return JSON.stringify(children); |
| 128 | + } catch { |
| 129 | + return undefined; |
| 130 | + } |
97 | 131 | } |
0 commit comments