* Implement self-host mode * Migrate from XML tool calls to native tool calls and reimplement agent mode * Use ChatOpenRouter for copilot-plus-flash for native tool call sse support * Fix QA exclusion for search v3 * Update plan and remove debug messages in agent * Migrate chains off of xml and clean up related logic * Implement agent reasoning block * Update css for responsiveness * Implement agent query pre-expansion and proper reasoning block display for search * Update agent docs * Refine agent reason block * Refine agent reasoning UX * Fallback to plus non-agent if native tool call is not supported by the model * Fix time filter query * Update self-host description * Skip agent reasoning block and old tool call banners during chat save and load * Fix projects mode switch to plus and chat auto saves as non-project bug
4.9 KiB
BedrockChatModel Tool Calling Implementation
Status: ✅ IMPLEMENTED
Native tool/function calling support has been added to the custom BedrockChatModel class for Agent mode.
Summary
The BedrockChatModel now supports LangChain's native tool calling via model.bindTools(tools), enabling it to work with Agent mode just like ChatOpenAI, ChatAnthropic, and ChatGoogleGenerativeAI.
Implementation Details
1. bindTools() Method
bindTools(tools: StructuredToolInterface[]): BedrockChatModel {
const bound = Object.create(this) as BedrockChatModel;
bound.boundTools = tools;
return bound;
}
Creates a new instance with tools bound, following LangChain's pattern.
2. Tool Format Conversion
private convertToolsToClaude(tools: StructuredToolInterface[]): any[] {
return tools.map((tool) => {
let inputSchema: any = { type: "object", properties: {} };
if (tool.schema) {
inputSchema = isInteropZodSchema(tool.schema)
? toJsonSchema(tool.schema)
: tool.schema;
}
return {
name: tool.name,
description: tool.description || "",
input_schema: inputSchema,
};
});
}
Uses LangChain's isInteropZodSchema and toJsonSchema for proper schema conversion.
3. Request Body with Tools
Tools are included in the request payload when bound:
if (this.boundTools && this.boundTools.length > 0) {
payload.tools = this.convertToolsToClaude(this.boundTools);
}
4. ToolMessage Handling
buildRequestBody handles ToolMessage (tool results) as tool_result content blocks:
if (messageType === "tool") {
const toolMessage = message as ToolMessage;
conversation.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolMessage.tool_call_id,
content: toolResultContent,
},
],
});
}
5. AIMessage with Tool Calls
buildRequestBody handles AIMessage with tool_calls as tool_use content blocks:
if (toolCalls && toolCalls.length > 0) {
const contentBlocks: ContentBlock[] = [];
// Add text if present
// Add tool_use blocks for each tool call
for (const tc of toolCalls) {
contentBlocks.push({
type: "tool_use",
id: tc.id || `tool_${Date.now()}`,
name: tc.name,
input: tc.args as Record<string, unknown>,
});
}
}
6. Non-Streaming Tool Call Extraction
_generate extracts tool calls from Claude's response:
private extractToolCalls(data: any): any[] | undefined {
if (!Array.isArray(data?.content)) return undefined;
const toolUseBlocks = data.content.filter(
(block: any) => block.type === "tool_use"
);
if (toolUseBlocks.length === 0) return undefined;
return toolUseBlocks.map((block: any) => ({
id: block.id,
name: block.name,
args: block.input || {},
type: "tool_call" as const,
}));
}
7. Streaming Tool Call Chunks
processStreamEvent emits tool_call_chunks for LangChain's concat mechanism:
private extractToolCallChunk(event: any): { id?: string; index: number; name?: string; args?: string } | null {
// content_block_start with tool_use - initial tool call info
if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
return {
id: event.content_block.id,
index: event.index ?? 0,
name: event.content_block.name,
args: "",
};
}
// content_block_delta with input_json_delta - partial tool args
if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
return {
index: event.index ?? 0,
args: event.delta.partial_json || "",
};
}
return null;
}
Tool call chunks are emitted as AIMessageChunk with tool_call_chunks:
const toolCallChunk = this.extractToolCallChunk(innerEvent);
if (toolCallChunk) {
const messageChunk = new AIMessageChunk({
content: "",
response_metadata: chunkMetadata,
tool_call_chunks: [toolCallChunk],
});
deltaChunks.push(new ChatGenerationChunk({ message: messageChunk, text: "" }));
}
Testing
const model = new BedrockChatModel({
modelId: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
apiKey: "...",
endpoint: "...",
streamEndpoint: "...",
});
const tools = [
{
name: "get_weather",
description: "Get weather for a location",
schema: z.object({ location: z.string() }),
},
];
const boundModel = model.bindTools(tools);
const response = await boundModel.invoke([new HumanMessage("What's the weather in Tokyo?")]);
console.log(response.tool_calls); // Should have tool call