UNPKG

vscode-chat-langchain-bridge

Version:

Create VS Code Chat participants (agents) with LangChain/LangGraph: tool-calling and streaming bridge for LanguageModelChat.

322 lines (318 loc) 9.88 kB
// src/chat_models.ts import { BaseChatModel } from "@langchain/core/language_models/chat_models"; import { AIMessage as AIMessage2, AIMessageChunk, ToolMessage as ToolMessage2 } from "@langchain/core/messages"; import { ChatGenerationChunk } from "@langchain/core/outputs"; import { ChatResponseProgressPart, LanguageModelChatMessage as LanguageModelChatMessage2, LanguageModelTextPart as LanguageModelTextPart2, LanguageModelToolCallPart as LanguageModelToolCallPart2 } from "vscode"; // src/utils.ts import { AIMessage, HumanMessage, SystemMessage, ToolMessage } from "@langchain/core/messages"; import { DynamicStructuredTool as DynamicStructuredTool2, StructuredTool } from "@langchain/core/tools"; import { ChatRequestTurn, ChatResponseAnchorPart, ChatResponseMarkdownPart, ChatResponseTurn, LanguageModelChatMessage, LanguageModelTextPart, LanguageModelToolCallPart, LanguageModelToolResultPart, Uri } from "vscode"; // src/tools.ts import { DynamicStructuredTool } from "@langchain/core/tools"; import { zodToJsonSchema } from "zod-to-json-schema"; var ChatVSCodeTool = class extends DynamicStructuredTool { inputSchema; constructor(fields) { super(fields); this.inputSchema = zodToJsonSchema(fields.schema); } static lc_name() { return "ChatVSCodeTool"; } invoke(inputOrOptions, configOrToken) { if (inputOrOptions && typeof inputOrOptions === "object" && "input" in inputOrOptions) { const options = inputOrOptions; return super.invoke(options.input); } else { const input = inputOrOptions; const config = configOrToken; return super.invoke(input, config); } } }; // src/utils.ts function toVSCodeChatTool(tool) { if (tool instanceof ChatVSCodeTool) { return tool; } if (isDynamicStructuredTool(tool)) { return new ChatVSCodeTool({ name: tool.name, description: tool.description, schema: tool.schema, func: (input, runManager, config) => { return tool.invoke(input, config); } }); } if (isStructuredTool(tool)) { return new ChatVSCodeTool({ name: tool.name, description: tool.description, schema: tool.schema, func: (input, runManager, config) => { return tool.invoke(input, config); } }); } throw new Error("Invalid tool type"); } function isTextContentBlock(part) { return part.type === "text"; } function isImageContentBlock(part) { return part.type === "image"; } function toTextContent(content) { if (typeof content === "string") { return [new LanguageModelTextPart(content)]; } if (Array.isArray(content)) { return content.map((part) => { if (isTextContentBlock(part)) { return new LanguageModelTextPart(part.text); } else if (isImageContentBlock(part)) { if (part.url) { return new LanguageModelTextPart(part.url); } if (part.data && part.mimeType) { const base64String = typeof part.data === "string" ? part.data : Buffer.from(part.data).toString("base64"); const dataUrl = `data:${part.mimeType};base64,${base64String}`; return new LanguageModelTextPart(dataUrl); } else if (part.fileId) { throw new Error(`Sending image via fileId not yet supported by this parser.`); } else { throw new Error(`message part type not supported: ${part}`); } } else { throw new Error(`message part type not supported: ${part}`); } }); } else { throw new Error("Unknown message content type"); } } function convertBaseMessage(message) { if (AIMessage.isInstance(message)) { if (!!message.tool_calls?.length) { const aiMessage = message; const toolCallParts = aiMessage.tool_calls.map( (toolCall) => { return new LanguageModelToolCallPart( toolCall.id || "", toolCall.name, toolCall.args ); } ); return LanguageModelChatMessage.Assistant( toolCallParts, aiMessage.name ); } return LanguageModelChatMessage.Assistant( toTextContent(message.content), message.name ); } if (HumanMessage.isInstance(message)) { const humanMessage = message; return LanguageModelChatMessage.User( toTextContent(humanMessage.content), humanMessage.name ); } if (ToolMessage.isInstance(message)) { const toolResult = new LanguageModelToolResultPart( message.tool_call_id, toTextContent(message.content) ); return LanguageModelChatMessage.User( [toolResult], message.name ); } if (SystemMessage.isInstance(message)) { return LanguageModelChatMessage.User( toTextContent(message.content), message.name ); } throw new Error(`Unsupported message type: ${message}`); } function convertVscodeHistory(chatContext) { const result = []; for (const message of chatContext.history) { if (message instanceof ChatRequestTurn) { result.push(new HumanMessage({ content: message.prompt })); } else if (message instanceof ChatResponseTurn) { result.push(new AIMessage({ content: chatResponseToString(message) })); } } return result; } function chatResponseToString(response) { return response.response.map((r) => { if (r instanceof ChatResponseMarkdownPart) { return r.value.value; } else if (r instanceof ChatResponseAnchorPart) { if (r.value instanceof Uri) { return r.value.fsPath; } else { return r.value.uri.fsPath; } } return ""; }).join(""); } function isDynamicStructuredTool(tool) { return tool instanceof DynamicStructuredTool2; } function isStructuredTool(tool) { return tool instanceof StructuredTool; } // src/chat_models.ts var ChatVSCode = class extends BaseChatModel { model; token; responseStream; constructor(fields) { super(fields ?? {}); this.model = fields.model; this.token = fields.token; this.responseStream = fields.responseStream; } static lc_name() { return "ChatVSCode"; } _llmType() { return "vscode"; } async _generate(messages, options, runManager) { let vscodeMessages = messages.map( (message2) => { return convertBaseMessage(message2); } ); const lastMessage = messages.at(-1); if (messages.length > 0 && ToolMessage2.isInstance(lastMessage)) { vscodeMessages.push(LanguageModelChatMessage2.User(` Above is the result from one or more tool calls. The user cannot see the results, so you should use this information to continue the conversation. `)); } const response = await this.model.sendRequest( vscodeMessages, { tools: options.tools?.map((tool) => { return { name: tool.name, description: tool.description, inputSchema: tool.inputSchema }; }) }, this.token ); let text = ""; const toolCalls = []; for await (const part of response.stream) { if (part instanceof LanguageModelTextPart2) { text += part.value; runManager?.handleLLMNewToken(part.value); } else if (part instanceof LanguageModelToolCallPart2) { const toolCall = { id: part.callId, name: part.name, args: part.input, type: "tool_call" }; toolCalls.push(toolCall); } else { console.warn(`Unknown part type received from model stream:`); console.warn(part); const progressText = new ChatResponseProgressPart(part.value); this.responseStream?.push(progressText); } } let result = { generations: [] }; const message = new AIMessage2(text); message.tool_calls = toolCalls; const generation = { text, message }; result.generations.push(generation); return result; } async *_streamResponseChunks(messages, options, runManager) { const copilotMessages = messages.map(convertBaseMessage); const response = this.model.sendRequest( copilotMessages, { tools: options.tools?.map((tool) => { return { name: tool.name, description: tool.description, inputSchema: tool.inputSchema }; }) }, this.token ); for await (const part of (await response).stream) { if (this.token.isCancellationRequested) { break; } if (part instanceof LanguageModelTextPart2) { const chunk = new ChatGenerationChunk({ text: part.value, message: new AIMessageChunk(part.value) }); runManager?.handleLLMNewToken(part.value); yield chunk; } else if (part instanceof LanguageModelToolCallPart2) { const chunkMessage = new AIMessageChunk({ tool_call_chunks: [ { name: part.name, args: JSON.stringify(part.input), // Chunk the input as a JSON string id: part.callId, type: "tool_call_chunk" } ] }); const chunk = new ChatGenerationChunk({ text: "", message: chunkMessage }); yield chunk; } else { console.log("Is ChatResponseProgressPart:", part instanceof ChatResponseProgressPart); console.warn(`Unknown part type received from model stream:`); console.warn(part); } } } bindTools(tools, kwargs) { if (kwargs && "strict" in kwargs) { delete kwargs.strict; } return this.withConfig({ tools: tools.map((tool) => toVSCodeChatTool(tool)), ...kwargs }); } }; export { ChatVSCode, convertVscodeHistory, toVSCodeChatTool }; //# sourceMappingURL=index.js.map