UNPKG

@langchain/anthropic

Version:
328 lines (326 loc) 13.4 kB
import { _isAnthropicCompactionBlock, _isAnthropicImageBlockParam, _isAnthropicRedactedThinkingBlock, _isAnthropicSearchResultBlock, _isAnthropicThinkingBlock, standardContentBlockConverter } from "./content.js"; import { _formatStandardContent } from "./standard.js"; import { AIMessage, HumanMessage, convertToProviderContentBlock, isDataContentBlock, parseBase64DataUrl } from "@langchain/core/messages"; //#region src/utils/message_inputs.ts function _formatImage(imageUrl) { const parsed = 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) { 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") previousMessage.content.push({ type: "tool_result", content: message.content, tool_use_id: message.tool_call_id }); else updatedMsgs.push(new HumanMessage({ content: [{ type: "tool_result", content: message.content, tool_use_id: message.tool_call_id }] })); } else updatedMsgs.push(new HumanMessage({ content: [{ type: "tool_result", ...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 === void 0) 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* _formatContentBlocks(content, toolCalls) { const toolTypes = [ "bash_code_execution_tool_result", "input_json_delta", "server_tool_use", "text_editor_code_execution_tool_result", "tool_result", "tool_use", "web_search_result", "web_search_tool_result" ]; const textTypes = ["text", "text_delta"]; for (const contentPart of content) { if (isDataContentBlock(contentPart)) yield convertToProviderContentBlock(contentPart, standardContentBlockConverter); const cacheControl = "cache_control" in contentPart ? contentPart.cache_control : void 0; if (contentPart.type === "image_url") { let source; if (typeof contentPart.image_url === "string") source = _formatImage(contentPart.image_url); else if (typeof contentPart.image_url === "object" && contentPart.image_url !== null && "url" in contentPart.image_url && typeof contentPart.image_url.url === "string") source = _formatImage(contentPart.image_url.url); if (source) yield { type: "image", source, ...cacheControl ? { cache_control: cacheControl } : {} }; } else if (_isAnthropicImageBlockParam(contentPart)) yield contentPart; else if (contentPart.type === "image") { let source; if ("url" in contentPart && typeof contentPart.url === "string") source = _formatImage(contentPart.url); else if ("data" in contentPart && (typeof contentPart.data === "string" || contentPart.data instanceof Uint8Array)) source = { type: "base64", media_type: "mimeType" in contentPart && typeof contentPart.mimeType === "string" ? contentPart.mimeType : "image/jpeg", data: typeof contentPart.data === "string" ? contentPart.data : Buffer.from(contentPart.data).toString("base64") }; else if ("fileId" in contentPart && typeof contentPart.fileId === "string") source = { type: "file", file_id: contentPart.fileId }; if (source) yield { type: "image", source, ...cacheControl ? { cache_control: cacheControl } : {} }; } else if (contentPart.type === "file") { let source; if ("url" in contentPart && typeof contentPart.url === "string") source = { type: "url", url: contentPart.url }; else if ("data" in contentPart && (typeof contentPart.data === "string" || contentPart.data instanceof Uint8Array)) source = { type: "base64", media_type: "mimeType" in contentPart && typeof contentPart.mimeType === "string" ? contentPart.mimeType : "application/pdf", data: typeof contentPart.data === "string" ? contentPart.data : Buffer.from(contentPart.data).toString("base64") }; else if ("fileId" in contentPart && typeof contentPart.fileId === "string") source = { type: "file", file_id: contentPart.fileId }; if (source) yield { type: "document", source, ...cacheControl ? { cache_control: cacheControl } : {} }; } else if (contentPart.type === "document") yield { ...contentPart, ...cacheControl ? { cache_control: cacheControl } : {} }; else if (_isAnthropicThinkingBlock(contentPart)) yield { type: "thinking", thinking: contentPart.thinking, signature: contentPart.signature, ...cacheControl ? { cache_control: cacheControl } : {} }; else if (_isAnthropicRedactedThinkingBlock(contentPart)) yield { type: "redacted_thinking", data: contentPart.data, ...cacheControl ? { cache_control: cacheControl } : {} }; else if (_isAnthropicCompactionBlock(contentPart)) yield { type: "compaction", content: contentPart.content, ...cacheControl ? { cache_control: cacheControl } : {} }; else if (_isAnthropicSearchResultBlock(contentPart)) yield { type: "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 }; else if (textTypes.find((t) => t === contentPart.type) && "text" in contentPart) yield { type: "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 (contentPartCopy.type === "input_json_delta") continue; if (contentPartCopy.type === "tool_use" && typeof contentPartCopy.input === "string") { const matchingToolCall = toolCalls?.find((tc) => tc.id === contentPartCopy.id); if (matchingToolCall) contentPartCopy.input = matchingToolCall.args; else contentPartCopy.input = content.filter((nestedContentPart) => nestedContentPart.index === contentPartCopy.index && nestedContentPart.type === "input_json_delta" && typeof nestedContentPart.input === "string").reduce((accumulator, nestedContentPart) => accumulator + nestedContentPart.input, contentPartCopy.input); } if ("index" in contentPartCopy) delete contentPartCopy.index; if ("input" in contentPartCopy) { if (typeof contentPartCopy.input === "string") try { contentPartCopy.input = JSON.parse(contentPartCopy.input); } catch { contentPartCopy.input = {}; } } yield { ...contentPartCopy, ...cacheControl ? { cache_control: cacheControl } : {} }; } else if (contentPart.type === "container_upload") yield { ...contentPart, ...cacheControl ? { cache_control: cacheControl } : {} }; } } function _formatContent(message, toolCalls) { const { content } = message; if (typeof content === "string") return content; else return Array.from(_formatContentBlocks(content, toolCalls)); } /** * 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; return { messages: mergeMessages((system !== void 0 ? mergedMessages.slice(1) : mergedMessages).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.type}" is not supported.`); if (AIMessage.isInstance(message) && message.response_metadata?.output_version === "v1") return { role, content: _formatStandardContent(message) }; if (AIMessage.isInstance(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 formattedContent = _formatContent(message, message.tool_calls); const formattedContentArr = Array.isArray(formattedContent) ? formattedContent : [{ type: "text", text: formattedContent }]; const missingToolCalls = message.tool_calls.filter((toolCall) => !content.find((contentPart) => (contentPart.type === "tool_use" || contentPart.type === "input_json_delta" || contentPart.type === "server_tool_use") && contentPart.id === toolCall.id)); return { role, content: [...formattedContentArr, ...missingToolCalls.map(_convertLangChainToolCallToAnthropic)] }; } else return { role, content: _formatContent(message, AIMessage.isInstance(message) ? message.tool_calls : void 0) }; })), system }; } /** * Applies cache_control to the last content block of the last message in the payload. * This is the recommended approach for prompt caching as it applies the cache_control * at the final formatting layer, after all message processing is complete. * * This matches the Python langchain-anthropic implementation where cache_control * is applied via model_settings rather than modifying message content blocks directly. * * @param payload - The formatted Anthropic message payload * @param cacheControl - The cache control configuration to apply * @returns The payload with cache_control applied to the last content block */ function applyCacheControlToPayload(payload, cacheControl) { if (!payload.messages || payload.messages.length === 0) return payload; const messages = [...payload.messages]; const lastMessageIndex = messages.length - 1; const lastMessage = messages[lastMessageIndex]; if (!lastMessage) return payload; if (typeof lastMessage.content === "string") { messages[lastMessageIndex] = { ...lastMessage, content: [{ type: "text", text: lastMessage.content, cache_control: cacheControl }] }; return { ...payload, messages }; } if (Array.isArray(lastMessage.content) && lastMessage.content.length > 0) { const content = [...lastMessage.content]; const lastBlockIndex = content.length - 1; content[lastBlockIndex] = { ...content[lastBlockIndex], cache_control: cacheControl }; messages[lastMessageIndex] = { ...lastMessage, content }; return { ...payload, messages }; } return payload; } 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)) currentMessage = { ...currentMessage, content: [...normalizeContent(currentMessage.content), ...normalizeContent(nextMessage.content)] }; else { result.push(currentMessage); currentMessage = nextMessage; } } result.push(currentMessage); return result; } //#endregion export { _convertMessagesToAnthropicPayload, applyCacheControlToPayload }; //# sourceMappingURL=message_inputs.js.map