1- import { describe , it , expect } from "vitest" ;
1+ import { describe , it , expect , beforeEach } from "vitest" ;
22import Anthropic from "@anthropic-ai/sdk" ;
33import { HumanMessage , AIMessage } from "@langchain/core/messages" ;
44import { ChatAnthropic } from "../chat_models.js" ;
5+ import { extractGeneratedFiles } from "../utils/extractGeneratedFiles.js" ;
56import fs from "fs" ;
67import path from "path" ;
78import os from "os" ;
89
910describe ( "Files API with Code Execution" , ( ) => {
11+ let model : ReturnType < ChatAnthropic [ "bindTools" ] > ;
12+
13+ beforeEach ( ( ) => {
14+ // Create ChatAnthropic model with code execution beta header and bind tools
15+ const baseModel = new ChatAnthropic ( {
16+ model : "claude-3-5-haiku-20241022" ,
17+ temperature : 0 ,
18+ clientOptions : {
19+ defaultHeaders : {
20+ "anthropic-beta" : "code-execution-2025-08-25,files-api-2025-04-14" ,
21+ } ,
22+ } ,
23+ } ) ;
24+
25+ // Bind the code_execution tool
26+ model = baseModel . bindTools ( [
27+ {
28+ type : "code_execution_20250825" as const ,
29+ name : "code_execution" ,
30+ } ,
31+ ] ) ;
32+ } ) ;
33+
1034 it ( "should handle createMessageWithFiles using container_upload blocks" , async ( ) => {
1135 // Create Anthropic client for Files API
1236 const anthropicClient = new Anthropic ( {
@@ -26,23 +50,6 @@ describe("Files API with Code Execution", () => {
2650 file : fs . createReadStream ( tmpFilePath ) ,
2751 } ) ;
2852
29- // Create ChatAnthropic model with code execution beta header
30- const model = new ChatAnthropic ( {
31- model : "claude-3-5-haiku-20241022" ,
32- temperature : 0 ,
33- clientOptions : {
34- defaultHeaders : {
35- "anthropic-beta" : "code-execution-2025-08-25,files-api-2025-04-14" ,
36- } ,
37- } ,
38- } ) ;
39-
40- // Define the built-in code_execution tool
41- const codeExecutionTool = {
42- type : "code_execution_20250825" as const ,
43- name : "code_execution" ,
44- } ;
45-
4653 // Create a message with container_upload block
4754 const message = new HumanMessage ( {
4855 content : [
@@ -55,9 +62,7 @@ describe("Files API with Code Execution", () => {
5562 } ) ;
5663
5764 // Invoke the model with the file
58- const result = await model . invoke ( [ message ] , {
59- tools : [ codeExecutionTool ] ,
60- } ) ;
65+ const result = await model . invoke ( [ message ] ) ;
6166
6267 // Verify the result is an AIMessage
6368 expect ( result ) . toBeInstanceOf ( AIMessage ) ;
@@ -86,26 +91,64 @@ describe("Files API with Code Execution", () => {
8691 )
8792 ) . toBe ( true ) ;
8893
89- // The response should contain text with the calculated average
90- const textBlocks = content . filter (
91- ( block ) =>
92- typeof block === "object" && "type" in block && block . type === "text"
93- ) ;
94- const responseText = textBlocks
95- . map ( ( block ) =>
96- typeof block === "object" && "text" in block ? block . text : ""
97- )
98- . join ( " " )
99- . toLowerCase ( ) ;
100-
10194 // The response should mention the average age (30)
102- expect ( responseText ) . toMatch ( / a v e r a g e | m e a n / i) ;
103- expect ( responseText ) . toMatch ( / 3 0 / ) ;
95+ expect ( result . text ) . toMatch ( / a v e r a g e | m e a n / i) ;
96+ expect ( result . text ) . toMatch ( / 3 0 / ) ;
10497 } finally {
10598 // Clean up temporary file
10699 if ( fs . existsSync ( tmpFilePath ) ) {
107100 fs . unlinkSync ( tmpFilePath ) ;
108101 }
109102 }
110103 } , 60000 ) ;
104+
105+ it ( "should pass container and file outputs across multiple turns" , async ( ) => {
106+ // First invocation: Calculate mean and avg, store to file
107+ const firstMessage = new HumanMessage (
108+ "Calculate the mean and average of these numbers: 10, 20, 30, 40, 50. Store the results in a file called 'results.txt'."
109+ ) ;
110+
111+ const firstResult = await model . invoke ( [ firstMessage ] ) ;
112+
113+ // Verify first result succeeded
114+ expect ( firstResult ) . toBeInstanceOf ( AIMessage ) ;
115+
116+ // Extract container ID from first response
117+ const container = ( firstResult as AIMessage ) . additional_kwargs
118+ ?. container as { id : string ; expires_at : string } | undefined ;
119+ expect ( container ?. id ) . toBeTruthy ( ) ;
120+
121+ // Verify file output was created using extractGeneratedFilesAnthropic
122+ const fileIds = extractGeneratedFiles (
123+ firstResult as unknown as Anthropic . Beta . BetaMessage
124+ ) ;
125+ expect ( fileIds . length ) . toBeGreaterThan ( 0 ) ;
126+
127+ // Second invocation: Read the file with same container
128+ // This should succeed because we apply the workaround in message_inputs.ts
129+ const secondMessage = new HumanMessage (
130+ "What are the contents of the results.txt file?"
131+ ) ;
132+
133+ const secondResult = await model . invoke (
134+ [ firstMessage , firstResult , secondMessage ] ,
135+ {
136+ container : container ?. id , // Pass container to reuse files
137+ }
138+ ) ;
139+
140+ // Verify second result succeeded
141+ expect ( secondResult ) . toBeInstanceOf ( AIMessage ) ;
142+ const secondContent = Array . isArray ( secondResult . content )
143+ ? secondResult . content
144+ : [ ] ;
145+ expect ( secondContent . length ) . toBeGreaterThan ( 0 ) ;
146+
147+ // Verify the same container was reused
148+ const secondContainer = ( secondResult as AIMessage ) . additional_kwargs
149+ ?. container as { id : string ; expires_at : string } | undefined ;
150+ expect ( secondContainer ?. id ) . toBe ( container ?. id ) ;
151+
152+ expect ( secondResult . text ) . toMatch ( / 3 0 / ) ;
153+ } , 60000 ) ;
111154} ) ;
0 commit comments