import { AIMessageChunk, HumanMessage, isAIMessage, } from "@langchain/core/messages"; import { concat } from "@langchain/core/utils/stream"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function extractToolCalls(content) { const toolCalls = []; for (const block of content) { if (block.type === "tool_use") { toolCalls.push({ name: block.name, args: block.input, id: block.id, type: "tool_call", }); } } return toolCalls; } function _formatImage(imageUrl) { const regex = /^data:(image\/.+);base64,(.+)$/; const match = imageUrl.match(regex); if (match === null) { throw new Error([ "Anthropic only supports base64-encoded images currently.", "Example: data:image/png;base64,/9j/4AAQSk...", ].join("\n\n")); } return { type: "base64", media_type: match[1] ?? "", data: match[2] ?? "", // eslint-disable-next-line @typescript-eslint/no-explicit-any }; } function _mergeMessages(messages) { // Merge runs of human/tool messages into single human messages with content blocks. const merged = []; for (const message of messages) { if (message._getType() === "tool") { if (typeof message.content === "string") { const previousMessage = merged[merged.length - 1]; if (previousMessage?._getType() === "human" && Array.isArray(previousMessage.content) && "type" in previousMessage.content[0] && previousMessage.content[0].type === "tool_result") { // If the previous message was a tool result, we merge this tool message into it. previousMessage.content.push({ type: "tool_result", content: message.content, tool_use_id: message.tool_call_id, }); } else { // If not, we create a new human message with the tool result. merged.push(new HumanMessage({ content: [ { type: "tool_result", content: message.content, tool_use_id: message.tool_call_id, }, ], })); } } else { merged.push(new HumanMessage({ content: message.content })); } } else { const previousMessage = merged[merged.length - 1]; if (previousMessage?._getType() === "human" && message._getType() === "human") { // eslint-disable-next-line @typescript-eslint/no-explicit-any let combinedContent; if (typeof previousMessage.content === "string") { combinedContent = [{ type: "text", text: previousMessage.content }]; } else { combinedContent = previousMessage.content; } if (typeof message.content === "string") { combinedContent.push({ type: "text", text: message.content }); } else { combinedContent = combinedContent.concat(message.content); } previousMessage.content = combinedContent; } else { merged.push(message); } } } return merged; } export function _convertLangChainToolCallToAnthropic(toolCall // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { if (toolCall.id === undefined) { throw new Error(`Anthropic requires all tool calls to have an "id".`); } return { type: "tool_use", id: toolCall.id, name: toolCall.name, input: toolCall.args, }; } function _formatContent(content) { if (typeof content === "string") { return content; } else { const contentBlocks = content.map((contentPart) => { if (contentPart.type === "image_url") { let source; if (typeof contentPart.image_url === "string") { source = _formatImage(contentPart.image_url); } else { source = _formatImage(contentPart.image_url.url); } return { type: "image", source, }; } else if (contentPart.type === "text") { // Assuming contentPart is of type MessageContentText here return { type: "text", text: contentPart.text, }; } else if (contentPart.type === "tool_use" || contentPart.type === "tool_result") { // TODO: Fix when SDK types are fixed return { ...contentPart, // eslint-disable-next-line @typescript-eslint/no-explicit-any }; } else { throw new Error("Unsupported message content format"); } }); return contentBlocks; } } export function formatMessagesForAnthropic(messages) { const mergedMessages = _mergeMessages(messages); let system; if (mergedMessages.length > 0 && mergedMessages[0]._getType() === "system") { if (typeof messages[0].content !== "string") { throw new Error("System message content must be a string."); } system = messages[0].content; } const conversationMessages = system !== undefined ? mergedMessages.slice(1) : mergedMessages; const formattedMessages = conversationMessages.map((message) => { let role; if (message._getType() === "human") { role = "user"; } else if (message._getType() === "ai") { role = "assistant"; } else if (message._getType() === "tool") { role = "user"; } else if (message._getType() === "system") { throw new Error("System messages are only permitted as the first passed message."); } else { throw new Error(`Message type "${message._getType()}" is not supported.`); } if (isAIMessage(message) && !!message.tool_calls?.length) { if (typeof message.content === "string") { if (message.content === "") { return { role, content: message.tool_calls.map(_convertLangChainToolCallToAnthropic), }; } else { return { role, content: [ { type: "text", text: message.content }, ...message.tool_calls.map(_convertLangChainToolCallToAnthropic), ], }; } } else { const { content } = message; const hasMismatchedToolCalls = !message.tool_calls.every((toolCall) => content.find((contentPart) => contentPart.type === "tool_use" && contentPart.id === toolCall.id)); if (hasMismatchedToolCalls) { console.warn(`The "tool_calls" field on a message is only respected if content is a string.`); } return { role, content: _formatContent(message.content), }; } } else { return { role, content: _formatContent(message.content), }; } }); return { messages: formattedMessages, system, }; } export function isAnthropicTool(tool) { if (typeof tool !== "object" || !tool) return false; return "input_schema" in tool; } export function _makeMessageChunkFromAnthropicEvent( // eslint-disable-next-line @typescript-eslint/no-explicit-any data, fields) { if (data.type === "message_start") { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { content, usage, ...additionalKwargs } = data.message; // eslint-disable-next-line @typescript-eslint/no-explicit-any const filteredAdditionalKwargs = {}; for (const [key, value] of Object.entries(additionalKwargs)) { if (value !== undefined && value !== null) { filteredAdditionalKwargs[key] = value; } } return new AIMessageChunk({ content: fields.coerceContentToString ? "" : [], additional_kwargs: filteredAdditionalKwargs, }); } else if (data.type === "message_delta") { let usageMetadata; return new AIMessageChunk({ content: fields.coerceContentToString ? "" : [], additional_kwargs: { ...data.delta }, usage_metadata: usageMetadata, }); } else if (data.type === "content_block_start" && data.content_block.type === "tool_use") { return new AIMessageChunk({ content: fields.coerceContentToString ? "" : [ { index: data.index, ...data.content_block, input: "", }, ], additional_kwargs: {}, }); } else if (data.type === "content_block_delta" && data.delta.type === "text_delta") { const content = data.delta?.text; if (content !== undefined) { return new AIMessageChunk({ content: fields.coerceContentToString ? content : [ { index: data.index, ...data.delta, }, ], additional_kwargs: {}, }); } } else if (data.type === "content_block_delta" && data.delta.type === "input_json_delta") { return new AIMessageChunk({ content: fields.coerceContentToString ? "" : [ { index: data.index, input: data.delta.partial_json, type: data.delta.type, }, ], additional_kwargs: {}, }); } else if (data.type === "message_stop" && data["amazon-bedrock-invocationMetrics"] !== undefined) { return new AIMessageChunk({ content: "", response_metadata: { "amazon-bedrock-invocationMetrics": data["amazon-bedrock-invocationMetrics"], }, usage_metadata: { input_tokens: data["amazon-bedrock-invocationMetrics"].inputTokenCount, output_tokens: data["amazon-bedrock-invocationMetrics"].outputTokenCount, total_tokens: data["amazon-bedrock-invocationMetrics"].inputTokenCount + data["amazon-bedrock-invocationMetrics"].outputTokenCount, }, }); } return null; } export function extractToolCallChunk(chunk) { let newToolCallChunk; // Initial chunk for tool calls from anthropic contains identifying information like ID and name. // This chunk does not contain any input JSON. const toolUseChunks = Array.isArray(chunk.content) ? chunk.content.find((c) => c.type === "tool_use") : undefined; if (toolUseChunks && "index" in toolUseChunks && "name" in toolUseChunks && "id" in toolUseChunks) { newToolCallChunk = { args: "", id: toolUseChunks.id, name: toolUseChunks.name, index: toolUseChunks.index, type: "tool_call_chunk", }; } // Chunks after the initial chunk only contain the index and partial JSON. const inputJsonDeltaChunks = Array.isArray(chunk.content) ? chunk.content.find((c) => c.type === "input_json_delta") : undefined; if (inputJsonDeltaChunks && "index" in inputJsonDeltaChunks && "input" in inputJsonDeltaChunks) { if (typeof inputJsonDeltaChunks.input === "string") { newToolCallChunk = { args: inputJsonDeltaChunks.input, index: inputJsonDeltaChunks.index, type: "tool_call_chunk", }; } else { newToolCallChunk = { args: JSON.stringify(inputJsonDeltaChunks.input, null, 2), index: inputJsonDeltaChunks.index, type: "tool_call_chunk", }; } } return newToolCallChunk; } export function extractToken(chunk) { return typeof chunk.content === "string" && chunk.content !== "" ? chunk.content : undefined; } export function extractToolUseContent(chunk, concatenatedChunks) { let newConcatenatedChunks = concatenatedChunks; // Remove `tool_use` content types until the last chunk. let toolUseContent; if (!newConcatenatedChunks) { newConcatenatedChunks = chunk; } else { newConcatenatedChunks = concat(newConcatenatedChunks, chunk); } if (Array.isArray(newConcatenatedChunks.content) && newConcatenatedChunks.content.find((c) => c.type === "tool_use")) { try { const toolUseMsg = newConcatenatedChunks.content.find((c) => c.type === "tool_use"); if (!toolUseMsg || !("input" in toolUseMsg || "name" in toolUseMsg || "id" in toolUseMsg)) return; const parsedArgs = JSON.parse(toolUseMsg.input); if (parsedArgs) { toolUseContent = { type: "tool_use", id: toolUseMsg.id, name: toolUseMsg.name, input: parsedArgs, }; } } catch (_) { // no-op } } return { toolUseContent, concatenatedChunks: newConcatenatedChunks, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function _toolsInParams(params) { return !!(params.tools && params.tools.length > 0); }