401 lines
14 KiB
JavaScript
401 lines
14 KiB
JavaScript
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);
|
|
}
|