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
JavaScript
// 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