import { createAgent, HumanMessage } from 'langchain'; import nock from 'nock'; import { LangchainChatModelAdapter } from 'src'; import { OpenAIChatModel } from './openai'; import { createMockHttpRequests, createSSEStream, mockFinalResponse, mockStreamFinalResponseEvents, mockStreamToolCallEvents, mockToolCallResponse, weatherTool, } from './openai.fixtures'; describe('OpenAI Integration with Langchain Agent', () => { const baseURL = 'https://api.openai.com/v1'; beforeEach(() => { nock.cleanAll(); }); afterEach(() => { nock.cleanAll(); }); it('should execute agent with tool calling through langchain adapter', async () => { nock(baseURL) .post('/responses', (body) => { expect(body).toMatchObject({ model: 'gpt-4o', input: 'What is the weather in tokyo?', tools: [ { type: 'function', name: 'get_weather', description: 'Get weather for a given city.', parameters: { type: 'object', properties: { city: { type: 'string', }, }, required: ['city'], additionalProperties: false, }, }, ], parallel_tool_calls: true, store: false, stream: false, }); return true; }) .reply(200, mockToolCallResponse); nock(baseURL) .post('/responses', (body) => { expect(body).toMatchObject({ model: 'gpt-4o', input: expect.arrayContaining([ { role: 'user', content: 'What is the weather in tokyo?' }, { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '' }], }, { type: 'function_call', call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW', name: 'get_weather', arguments: '{"city":"Tokyo"}', }, { type: 'function_call_output', call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW', output: "It's always sunny in Tokyo!", }, ]), parallel_tool_calls: true, store: false, stream: false, }); return true; }) .reply(200, mockFinalResponse); const openaiChatModel = new OpenAIChatModel('gpt-4o', createMockHttpRequests(), { baseURL }); const chatModel = new LangchainChatModelAdapter(openaiChatModel); const agent = createAgent({ model: chatModel, tools: [weatherTool], }); const result = await agent.invoke({ messages: [new HumanMessage('What is the weather in tokyo?')], }); expect(result).toBeDefined(); expect(result.messages).toHaveLength(4); expect(result.messages[0]).toMatchObject({ content: 'What is the weather in tokyo?', }); expect(result.messages[1]).toMatchObject({ id: 'resp_02a127c1e73b5fe4016989e989cb188195a27d4911084e4223', tool_calls: [ { type: 'tool_call', id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW', name: 'get_weather', args: { city: 'Tokyo', }, }, ], }); expect(result.messages[2]).toMatchObject({ content: "It's always sunny in Tokyo!", name: 'get_weather', tool_call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW', }); expect(result.messages[3]).toMatchObject({ id: 'resp_00a8729c01103919016989e98b13888190bca486b9676ce0cd', content: [ { type: 'text', text: "It's always sunny in Tokyo!", }, ], }); expect(nock.isDone()).toBe(true); }); it('should execute agent with streaming through langchain adapter', async () => { nock(baseURL) .post('/responses', (body) => { expect(body).toMatchObject({ model: 'gpt-4o', input: 'What is the weather in tokyo?', stream: true, }); return true; }) .reply(() => { const stream = createSSEStream(mockStreamToolCallEvents); return [ 200, stream, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, ]; }); nock(baseURL) .post('/responses', (body) => { expect(body).toMatchObject({ model: 'gpt-4o', stream: true, }); return true; }) .reply(() => { const stream = createSSEStream(mockStreamFinalResponseEvents); return [ 200, stream, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, ]; }); const openaiChatModel = new OpenAIChatModel('gpt-4o', createMockHttpRequests(), { baseURL }); const chatModel = new LangchainChatModelAdapter(openaiChatModel); const agent = createAgent({ model: chatModel, tools: [weatherTool], }); const chunks: unknown[] = []; const stream = await agent.stream( { messages: [{ role: 'user', content: 'What is the weather in tokyo?' }] }, { streamMode: 'messages' }, ); for await (const chunk of stream) { chunks.push(chunk); } expect(chunks).toHaveLength(10); const getChunkData = (chunk: unknown) => { const chunkArray = chunk as unknown[]; return { message: chunkArray[0] as Record, metadata: chunkArray[1] as Record, }; }; const { message: message1, metadata: metadata1 } = getChunkData(chunks[0]); const toolCalls1 = message1.tool_calls as Array>; expect(toolCalls1).toHaveLength(1); expect(toolCalls1[0]).toMatchObject({ name: 'get_weather', args: { city: 'Tokyo', }, id: 'call_StreamTest123', type: 'tool_call', }); expect(metadata1.langgraph_step).toBeDefined(); const { message: message2 } = getChunkData(chunks[1]); expect(message2.usage_metadata).toEqual({ input_tokens: 46, output_tokens: 15, total_tokens: 61, }); const responseMetadata2 = message2.response_metadata as Record; expect(responseMetadata2.finish_reason).toBe('stop'); const { message: message3 } = getChunkData(chunks[2]); expect(message3.content).toBe("It's always sunny in Tokyo!"); expect(message3.tool_call_id).toBe('call_StreamTest123'); expect(message3.name).toBe('get_weather'); const { message: message4 } = getChunkData(chunks[3]); const content4 = message4.content as Array<{ type: string; text: string }>; expect(content4[0].text).toBe("It's"); const { message: message5 } = getChunkData(chunks[4]); const content5 = message5.content as Array<{ type: string; text: string }>; expect(content5[0].text).toBe(' always'); const { message: message6 } = getChunkData(chunks[5]); const content6 = message6.content as Array<{ type: string; text: string }>; expect(content6[0].text).toBe(' sunny'); const { message: message7 } = getChunkData(chunks[6]); const content7 = message7.content as Array<{ type: string; text: string }>; expect(content7[0].text).toBe(' in'); const { message: message8 } = getChunkData(chunks[7]); const content8 = message8.content as Array<{ type: string; text: string }>; expect(content8[0].text).toBe(' Tokyo'); const { message: message9 } = getChunkData(chunks[8]); const content9 = message9.content as Array<{ type: string; text: string }>; expect(content9[0].text).toBe('!'); const { message: message10 } = getChunkData(chunks[9]); expect(message10.usage_metadata).toEqual({ input_tokens: 76, output_tokens: 8, total_tokens: 84, }); const responseMetadata10 = message10.response_metadata as Record; expect(responseMetadata10.finish_reason).toBe('stop'); for (const chunk of chunks) { const { metadata } = getChunkData(chunk); expect(metadata).toBeDefined(); expect(metadata.langgraph_step).toBeDefined(); } expect(nock.isDone()).toBe(true); }); });