Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-falcons-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/anthropic": patch
---

fix(anthropic): add support for container upload block type in message formatting
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, it, expect } from "vitest";
import Anthropic from "@anthropic-ai/sdk";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "../chat_models.js";
import fs from "fs";
import path from "path";
import os from "os";

describe("Files API with Code Execution", () => {
it("should handle createMessageWithFiles using container_upload blocks", async () => {
// Create Anthropic client for Files API
const anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});

// Create a temporary CSV file
const csvContent =
"name,age,city\nAlice,30,NYC\nBob,25,LA\nCharlie,35,Chicago";
const tmpDir = os.tmpdir();
const tmpFilePath = path.join(tmpDir, "test_data.csv");
fs.writeFileSync(tmpFilePath, csvContent);

try {
// Upload file using Anthropic Files API
const fileUpload = await anthropicClient.beta.files.upload({
file: fs.createReadStream(tmpFilePath),
});

// Create ChatAnthropic model with code execution beta header
const model = new ChatAnthropic({
model: "claude-3-5-haiku-20241022",
temperature: 0,
clientOptions: {
defaultHeaders: {
"anthropic-beta": "code-execution-2025-08-25,files-api-2025-04-14",
},
},
});

// Define the built-in code_execution tool
const codeExecutionTool = {
type: "code_execution_20250825" as const,
name: "code_execution",
};

// Create a message with container_upload block
const message = new HumanMessage({
content: [
{
type: "text",
text: "Analyze this CSV data and tell me the average age",
},
{ type: "container_upload", file_id: fileUpload.id },
],
});

// Invoke the model with the file
const result = await model.invoke([message], {
tools: [codeExecutionTool],
});

// Verify the result is an AIMessage
expect(result).toBeInstanceOf(AIMessage);

// The response should contain content blocks
const content = Array.isArray(result.content) ? result.content : [];
expect(content.length).toBeGreaterThan(0);

// Verify that code_execution tool was used (server_tool_use block)
expect(
content.some(
(block) =>
typeof block === "object" &&
"type" in block &&
block.type === "server_tool_use"
)
).toBe(true);

// Verify that we got a code execution result
expect(
content.some(
(block) =>
typeof block === "object" &&
"type" in block &&
block.type === "bash_code_execution_tool_result"
)
).toBe(true);

// The response should contain text with the calculated average
const textBlocks = content.filter(
(block) =>
typeof block === "object" && "type" in block && block.type === "text"
);
const responseText = textBlocks
.map((block) =>
typeof block === "object" && "text" in block ? block.text : ""
)
.join(" ")
.toLowerCase();

// The response should mention the average age (30)
expect(responseText).toMatch(/average|mean/i);
expect(responseText).toMatch(/30/);
} finally {
// Clean up temporary file
if (fs.existsSync(tmpFilePath)) {
fs.unlinkSync(tmpFilePath);
}
}
}, 60000);
});
49 changes: 49 additions & 0 deletions libs/providers/langchain-anthropic/src/tests/chat_models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,52 @@ test("Can properly format anthropic messages when given two tool results", async
system: undefined,
});
});

test("Can properly format messages with container_upload blocks", async () => {
const messageHistory = [
new HumanMessage({
content: [
{ type: "text", text: "Analyze this CSV data" },
{ type: "container_upload", file_id: "file_abc123" },
],
}),
];

const formattedMessages = _convertMessagesToAnthropicPayload(messageHistory);

expect(formattedMessages).toEqual({
messages: [
{
role: "user",
content: [
{ type: "text", text: "Analyze this CSV data" },
{ type: "container_upload", file_id: "file_abc123" },
],
},
],
system: undefined,
});
});

test("Drop content blocks that we don't know how to handle", async () => {
const messageHistory = [
new HumanMessage({
content: [
{ type: "text", text: "Hello" },
{ type: "some-unexpected-block-type", some_unexpected_field: "abc123" },
],
}),
];

const formattedMessages = _convertMessagesToAnthropicPayload(messageHistory);

expect(formattedMessages).toEqual({
messages: [
{
role: "user",
content: [{ type: "text", text: "Hello" }],
},
],
system: undefined,
});
});
5 changes: 4 additions & 1 deletion libs/providers/langchain-anthropic/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export type AnthropicWebSearchResultBlockParam =
// TODO(hntrl): beta blocks should be separated
export type AnthropicSearchResultBlockParam =
Anthropic.Beta.BetaSearchResultBlockParam;
export type AnthropicContainerUploadBlockParam =
Anthropic.Beta.BetaContainerUploadBlockParam;

// Union of all possible content block types including server tool use
export type ChatAnthropicContentBlock =
Expand All @@ -61,7 +63,8 @@ export type ChatAnthropicContentBlock =
| AnthropicServerToolUseBlockParam
| AnthropicWebSearchToolResultBlockParam
| AnthropicWebSearchResultBlockParam
| AnthropicSearchResultBlockParam;
| AnthropicSearchResultBlockParam
| AnthropicContainerUploadBlockParam;

export function isAnthropicImageBlockParam(
block: unknown
Expand Down
12 changes: 12 additions & 0 deletions libs/providers/langchain-anthropic/src/utils/message_inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
AnthropicWebSearchToolResultBlockParam,
AnthropicSearchResultBlockParam,
AnthropicToolResponse,
AnthropicContainerUploadBlockParam,
} from "../types.js";
import {
_isAnthropicImageBlockParam,
Expand Down Expand Up @@ -271,7 +272,18 @@ function* _formatContentBlocks(
...(cacheControl ? { cache_control: cacheControl } : {}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
} else if (contentPart.type === "container_upload") {
yield {
...contentPart,
...(cacheControl ? { cache_control: cacheControl } : {}),
} as AnthropicContainerUploadBlockParam;
}

// Note that we are intentionally dropping any blocks that we don't
// recognize. This is to allow for cross-compatibility between different
// providers that may have different block types. Ie if we take a message
// output from OpenAI and send it to Anthropic, we want to drop any blocks
// that Anthropic doesn't understand.
}
}

Expand Down