UNPKG

@langchain/anthropic

Version:
549 lines (548 loc) 22.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports._convertLangChainToolCallToAnthropic = _convertLangChainToolCallToAnthropic; exports._convertMessagesToAnthropicPayload = _convertMessagesToAnthropicPayload; /** * This util file contains functions for converting LangChain messages to Anthropic messages. */ const messages_1 = require("@langchain/core/messages"); const types_js_1 = require("../types.cjs"); function _formatImage(imageUrl) { const parsed = (0, messages_1.parseBase64DataUrl)({ dataUrl: imageUrl }); if (parsed) { return { type: "base64", media_type: parsed.mime_type, data: parsed.data, }; } let parsedUrl; try { parsedUrl = new URL(imageUrl); } catch { throw new Error([ `Malformed image URL: ${JSON.stringify(imageUrl)}. Content blocks of type 'image_url' must be a valid http, https, or base64-encoded data URL.`, "Example: data:image/png;base64,/9j/4AAQSk...", "Example: https://example.com/image.jpg", ].join("\n\n")); } if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") { return { type: "url", url: imageUrl, }; } throw new Error([ `Invalid image URL protocol: ${JSON.stringify(parsedUrl.protocol)}. Anthropic only supports images as http, https, or base64-encoded data URLs on 'image_url' content blocks.`, "Example: data:image/png;base64,/9j/4AAQSk...", "Example: https://example.com/image.jpg", ].join("\n\n")); } function _ensureMessageContents(messages) { // Merge runs of human/tool messages into single human messages with content blocks. const updatedMsgs = []; for (const message of messages) { if (message._getType() === "tool") { if (typeof message.content === "string") { const previousMessage = updatedMsgs[updatedMsgs.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. updatedMsgs.push(new messages_1.HumanMessage({ content: [ { type: "tool_result", content: message.content, tool_use_id: message.tool_call_id, }, ], })); } } else { updatedMsgs.push(new messages_1.HumanMessage({ content: [ { type: "tool_result", // rare case: message.content could be undefined ...(message.content != null ? { content: _formatContent(message) } : {}), tool_use_id: message.tool_call_id, }, ], })); } } else { updatedMsgs.push(message); } } return updatedMsgs; } function _convertLangChainToolCallToAnthropic(toolCall) { 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, }; } const standardContentBlockConverter = { providerName: "anthropic", fromStandardTextBlock(block) { return { type: "text", text: block.text, ...("citations" in (block.metadata ?? {}) ? { citations: block.metadata.citations } : {}), ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), }; }, fromStandardImageBlock(block) { if (block.source_type === "url") { const data = (0, messages_1.parseBase64DataUrl)({ dataUrl: block.url, asTypedArray: false, }); if (data) { return { type: "image", source: { type: "base64", data: data.data, media_type: data.mime_type, }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), }; } else { return { type: "image", source: { type: "url", url: block.url, media_type: block.mime_type ?? "", }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), }; } } else { if (block.source_type === "base64") { return { type: "image", source: { type: "base64", data: block.data, media_type: block.mime_type ?? "", }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), }; } else { throw new Error(`Unsupported image source type: ${block.source_type}`); } } }, fromStandardFileBlock(block) { const mime_type = (block.mime_type ?? "").split(";")[0]; if (block.source_type === "url") { if (mime_type === "application/pdf" || mime_type === "") { return { type: "document", source: { type: "url", url: block.url, media_type: block.mime_type ?? "", }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), ...("citations" in (block.metadata ?? {}) ? { citations: block.metadata.citations } : {}), ...("context" in (block.metadata ?? {}) ? { context: block.metadata.context } : {}), ...("title" in (block.metadata ?? {}) ? { title: block.metadata.title } : {}), }; } throw new Error(`Unsupported file mime type for file url source: ${block.mime_type}`); } else if (block.source_type === "text") { if (mime_type === "text/plain" || mime_type === "") { return { type: "document", source: { type: "text", data: block.text, media_type: block.mime_type ?? "", }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), ...("citations" in (block.metadata ?? {}) ? { citations: block.metadata.citations } : {}), ...("context" in (block.metadata ?? {}) ? { context: block.metadata.context } : {}), ...("title" in (block.metadata ?? {}) ? { title: block.metadata.title } : {}), }; } else { throw new Error(`Unsupported file mime type for file text source: ${block.mime_type}`); } } else if (block.source_type === "base64") { if (mime_type === "application/pdf" || mime_type === "") { return { type: "document", source: { type: "base64", data: block.data, media_type: "application/pdf", }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), ...("citations" in (block.metadata ?? {}) ? { citations: block.metadata.citations } : {}), ...("context" in (block.metadata ?? {}) ? { context: block.metadata.context } : {}), ...("title" in (block.metadata ?? {}) ? { title: block.metadata.title } : {}), }; } else if (["image/jpeg", "image/png", "image/gif", "image/webp"].includes(mime_type)) { return { type: "document", source: { type: "content", content: [ { type: "image", source: { type: "base64", data: block.data, media_type: mime_type, }, }, ], }, ...("cache_control" in (block.metadata ?? {}) ? { cache_control: block.metadata.cache_control } : {}), ...("citations" in (block.metadata ?? {}) ? { citations: block.metadata.citations } : {}), ...("context" in (block.metadata ?? {}) ? { context: block.metadata.context } : {}), ...("title" in (block.metadata ?? {}) ? { title: block.metadata.title } : {}), }; } else { throw new Error(`Unsupported file mime type for file base64 source: ${block.mime_type}`); } } else { throw new Error(`Unsupported file source type: ${block.source_type}`); } }, }; function _formatContent(message) { const toolTypes = [ "tool_use", "tool_result", "input_json_delta", "server_tool_use", "web_search_tool_result", "web_search_result", ]; const textTypes = ["text", "text_delta"]; const { content } = message; if (typeof content === "string") { return content; } else { const contentBlocks = content.map((contentPart) => { if ((0, messages_1.isDataContentBlock)(contentPart)) { return (0, messages_1.convertToProviderContentBlock)(contentPart, standardContentBlockConverter); } const cacheControl = "cache_control" in contentPart ? contentPart.cache_control : undefined; 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", // Explicitly setting the type as "image" source, ...(cacheControl ? { cache_control: cacheControl } : {}), }; } else if ((0, types_js_1.isAnthropicImageBlockParam)(contentPart)) { return contentPart; } else if (contentPart.type === "document") { // PDF return { ...contentPart, ...(cacheControl ? { cache_control: cacheControl } : {}), }; } else if (contentPart.type === "thinking") { const block = { type: "thinking", // Explicitly setting the type as "thinking" thinking: contentPart.thinking, signature: contentPart.signature, ...(cacheControl ? { cache_control: cacheControl } : {}), }; return block; } else if (contentPart.type === "redacted_thinking") { const block = { type: "redacted_thinking", // Explicitly setting the type as "redacted_thinking" data: contentPart.data, ...(cacheControl ? { cache_control: cacheControl } : {}), }; return block; } else if (contentPart.type === "search_result") { const block = { type: "search_result", // Explicitly setting the type as "search_result" title: contentPart.title, source: contentPart.source, ...("cache_control" in contentPart && contentPart.cache_control ? { cache_control: contentPart.cache_control } : {}), ...("citations" in contentPart && contentPart.citations ? { citations: contentPart.citations } : {}), content: contentPart.content, }; return block; } else if (textTypes.find((t) => t === contentPart.type) && "text" in contentPart) { // Assuming contentPart is of type MessageContentText here return { type: "text", // Explicitly setting the type as "text" text: contentPart.text, ...(cacheControl ? { cache_control: cacheControl } : {}), ...("citations" in contentPart && contentPart.citations ? { citations: contentPart.citations } : {}), }; } else if (toolTypes.find((t) => t === contentPart.type)) { const contentPartCopy = { ...contentPart }; if ("index" in contentPartCopy) { // Anthropic does not support passing the index field here, so we remove it. delete contentPartCopy.index; } if (contentPartCopy.type === "input_json_delta") { // `input_json_delta` type only represents yielding partial tool inputs // and is not a valid type for Anthropic messages. contentPartCopy.type = "tool_use"; } if ("input" in contentPartCopy) { // Anthropic tool use inputs should be valid objects, when applicable. if (typeof contentPartCopy.input === "string") { try { contentPartCopy.input = JSON.parse(contentPartCopy.input); } catch { contentPartCopy.input = {}; } } } // TODO: Fix when SDK types are fixed return { ...contentPartCopy, ...(cacheControl ? { cache_control: cacheControl } : {}), // eslint-disable-next-line @typescript-eslint/no-explicit-any }; } else if ("functionCall" in contentPart && contentPart.functionCall && typeof contentPart.functionCall === "object" && (0, messages_1.isAIMessage)(message)) { const correspondingToolCall = message.tool_calls?.find((toolCall) => toolCall.name === contentPart.functionCall.name); if (!correspondingToolCall) { throw new Error(`Could not find tool call for function call ${contentPart.functionCall.name}`); } // Google GenAI models include a `functionCall` object inside content. We should ignore it as Anthropic will not support it. return { id: correspondingToolCall.id, type: "tool_use", name: correspondingToolCall.name, input: contentPart.functionCall.args, }; } else { throw new Error("Unsupported message content format"); } }); return contentBlocks; } } /** * Formats messages as a prompt for the model. * Used in LangSmith, export is important here. * @param messages The base messages to format as a prompt. * @returns The formatted prompt. */ function _convertMessagesToAnthropicPayload(messages) { const mergedMessages = _ensureMessageContents(messages); let system; if (mergedMessages.length > 0 && mergedMessages[0]._getType() === "system") { 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 ((0, messages_1.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.type === "input_json_delta" || contentPart.type === "server_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), }; } } else { return { role, content: _formatContent(message), }; } }); return { messages: mergeMessages(formattedMessages), system, }; } function mergeMessages(messages) { if (!messages || messages.length <= 1) { return messages; } const result = []; let currentMessage = messages[0]; const normalizeContent = (content) => { if (typeof content === "string") { return [ { type: "text", text: content, }, ]; } return content; }; const isToolResultMessage = (msg) => { if (msg.role !== "user") return false; if (typeof msg.content === "string") { return false; } return (Array.isArray(msg.content) && msg.content.every((item) => item.type === "tool_result")); }; for (let i = 1; i < messages.length; i += 1) { const nextMessage = messages[i]; if (isToolResultMessage(currentMessage) && isToolResultMessage(nextMessage)) { // Merge the messages by combining their content arrays currentMessage = { ...currentMessage, content: [ ...normalizeContent(currentMessage.content), ...normalizeContent(nextMessage.content), ], }; } else { result.push(currentMessage); currentMessage = nextMessage; } } result.push(currentMessage); return result; }