diff --git a/langchain-core/src/runnables/base.ts b/langchain-core/src/runnables/base.ts index b9d4b691def1..a692426eed1d 100644 --- a/langchain-core/src/runnables/base.ts +++ b/langchain-core/src/runnables/base.ts @@ -281,18 +281,23 @@ export abstract class Runnable< } protected _separateRunnableConfigFromCallOptions( - options: Partial = {} + options?: Partial ): [RunnableConfig, Omit, keyof RunnableConfig>] { - const runnableConfig: RunnableConfig = ensureConfig({ - callbacks: options.callbacks, - tags: options.tags, - metadata: options.metadata, - runName: options.runName, - configurable: options.configurable, - recursionLimit: options.recursionLimit, - maxConcurrency: options.maxConcurrency, - }); - const callOptions = { ...options }; + let runnableConfig; + if (options === undefined) { + runnableConfig = ensureConfig(options); + } else { + runnableConfig = ensureConfig({ + callbacks: options.callbacks, + tags: options.tags, + metadata: options.metadata, + runName: options.runName, + configurable: options.configurable, + recursionLimit: options.recursionLimit, + maxConcurrency: options.maxConcurrency, + }); + } + const callOptions = { ...(options as Partial) }; delete callOptions.callbacks; delete callOptions.tags; delete callOptions.metadata; diff --git a/langchain-core/src/runnables/config.ts b/langchain-core/src/runnables/config.ts index d5dc9b8b1083..ecad12f431c2 100644 --- a/langchain-core/src/runnables/config.ts +++ b/langchain-core/src/runnables/config.ts @@ -119,6 +119,9 @@ const PRIMITIVES = new Set(["string", "number", "boolean"]); /** * Ensure that a passed config is an object with all required keys present. + * + * Note: To make sure async local storage loading works correctly, this + * should not be called with a default or prepopulated config argument. */ export function ensureConfig( config?: CallOptions diff --git a/langchain-core/src/runnables/tests/runnable_remote.test.ts b/langchain-core/src/runnables/tests/runnable_remote.test.ts index 2a7a8f564635..fe94c405cf9d 100644 --- a/langchain-core/src/runnables/tests/runnable_remote.test.ts +++ b/langchain-core/src/runnables/tests/runnable_remote.test.ts @@ -133,7 +133,7 @@ describe("RemoteRunnable", () => { expect(fetch).toHaveBeenCalledWith( `${BASE_URL}/a/invoke`, expect.objectContaining({ - body: '{"input":{"text":"string"},"config":{},"kwargs":{}}', + body: '{"input":{"text":"string"},"config":{"tags":[],"metadata":{},"recursionLimit":25},"kwargs":{}}', }) ); expect(result).toEqual(["a", "b", "c"]); diff --git a/langchain-core/src/singletons/tests/async_local_storage.test.ts b/langchain-core/src/singletons/tests/async_local_storage.test.ts index c5c369a297a4..e64e3bfdc802 100644 --- a/langchain-core/src/singletons/tests/async_local_storage.test.ts +++ b/langchain-core/src/singletons/tests/async_local_storage.test.ts @@ -112,9 +112,7 @@ test("Config should be automatically populated after setting global async local ); expect(res4?.tags).toEqual(["tester_with_config"]); - const chatModel = new FakeListChatModel({ responses: ["test"] }).bind({ - stop: [], - }); + const chatModel = new FakeListChatModel({ responses: ["test"] }); const outer4 = RunnableLambda.from(async () => { const res = await chatModel.invoke("hey"); return res; diff --git a/langchain/src/runnables/tests/runnable_remote.int.test.ts b/langchain/src/runnables/tests/runnable_remote.int.test.ts deleted file mode 100644 index dd094db18d78..000000000000 --- a/langchain/src/runnables/tests/runnable_remote.int.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { applyPatch } from "@langchain/core/utils/json_patch"; -import { RemoteRunnable } from "../remote.js"; - -test("streamLog hosted langserve", async () => { - const remote = new RemoteRunnable({ - url: `https://chat-langchain-backend.langchain.dev/chat`, - }); - const result = await remote.streamLog({ - question: "What is a document loader?", - }); - let totalByteSize = 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let aggregate: any = {}; - for await (const chunk of result) { - const jsonString = JSON.stringify(chunk); - aggregate = applyPatch(aggregate, chunk.ops).newDocument; - const byteSize = Buffer.byteLength(jsonString, "utf-8"); - totalByteSize += byteSize; - } - console.log("aggregate", aggregate); - console.log("totalByteSize", totalByteSize); -}); diff --git a/langchain/src/runnables/tests/runnable_remote.test.ts b/langchain/src/runnables/tests/runnable_remote.test.ts deleted file mode 100644 index 9946ada7eaa7..000000000000 --- a/langchain/src/runnables/tests/runnable_remote.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* eslint-disable no-promise-executor-return */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { jest, test } from "@jest/globals"; -import { - AIMessage, - AIMessageChunk, - HumanMessage, - SystemMessage, -} from "@langchain/core/messages"; - -import { RemoteRunnable } from "../remote.js"; -import { ChatPromptValue } from "../../prompts/chat.js"; - -const BASE_URL = "http://my-langserve-endpoint"; - -function respToStream(resp: string): ReadableStream { - const chunks = resp.split("\n"); - return new ReadableStream({ - start(controller) { - for (const chunk of chunks) { - controller.enqueue(Buffer.from(`${chunk}\n`)); - } - controller.close(); - }, - }); -} - -const aResp = `event: data -data: ["a", "b", "c", "d"] - -event: end`; - -const bResp = `event: data -data: {"content": "", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "\\"", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "object", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "1", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": ",", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": " object", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "2", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": ",", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": " object", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "3", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": ",", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": " object", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "4", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": ",", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": " object", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "5", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "\\"", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: data -data: {"content": "", "additional_kwargs": {}, "type": "AIMessageChunk", "example": false} - -event: end`; - -const strangeTypesResp = `event: data -data: {"content": "what is a document loader", "additional_kwargs": {}, "type": "human", "example": false} - -event: data -data: {"messages":[{"content":"You are an expert programmer and problem-solver, tasked with answering any question about Langchain.","type":"system","additional_kwargs":{}},{"content":"I am an AI","type":"ai","additional_kwargs":{}}]} - -event: end`; - -describe("RemoteRunnable", () => { - beforeEach(() => { - // mock langserve service - const returnDataByEndpoint: Record = { - "/a/invoke": JSON.stringify({ output: ["a", "b", "c"] }), - "/a/batch": JSON.stringify({ - output: [ - ["a", "b", "c"], - ["d", "e", "f"], - ], - }), - "/a/stream": respToStream(aResp), - "/b/stream": respToStream(bResp), - "/strange_types/stream": respToStream(strangeTypesResp), - }; - - const oldFetch = global.fetch; - - global.fetch = jest - .fn() - .mockImplementation(async (url: any, init?: any) => { - if (!url.startsWith(BASE_URL)) return await oldFetch(url, init); - const { pathname } = new URL(url); - const resp: Response = new Response(returnDataByEndpoint[pathname]); - return resp; - }) as any; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test("Invoke local langserve", async () => { - // mock fetch, expect /invoke - const remote = new RemoteRunnable({ url: `${BASE_URL}/a` }); - const result = await remote.invoke({ text: "string" }); - expect(fetch).toHaveBeenCalledWith( - `${BASE_URL}/a/invoke`, - expect.objectContaining({ - body: '{"input":{"text":"string"},"config":{},"kwargs":{}}', - }) - ); - expect(result).toEqual(["a", "b", "c"]); - }); - - test("Invoke local langserve passing a configurable object", async () => { - // mock fetch, expect /invoke - const remote = new RemoteRunnable({ url: `${BASE_URL}/a` }); - const result = await remote.invoke( - { text: "string" }, - { - configurable: { - destination: "destination", - integration_id: "integration_id", - user_id: "user_id", - }, - } - ); - expect(fetch).toHaveBeenCalledWith( - `${BASE_URL}/a/invoke`, - expect.objectContaining({ - body: expect.any(String), - }) - ); - expect(result).toEqual(["a", "b", "c"]); - }); - - test("Batch local langserve", async () => { - const returnData = [ - ["a", "b", "c"], - ["d", "e", "f"], - ]; - const remote = new RemoteRunnable({ url: `${BASE_URL}/a` }); - const result = await remote.batch([{ text: "1" }, { text: "2" }]); - expect(result).toEqual(returnData); - }); - - test("Stream local langserve", async () => { - const remote = new RemoteRunnable({ url: `${BASE_URL}/a` }); - const stream = await remote.stream({ text: "What are the 5 best apples?" }); - let chunkCount = 0; - for await (const chunk of stream) { - expect(chunk).toEqual(["a", "b", "c", "d"]); - chunkCount += 1; - } - expect(chunkCount).toBe(1); - }); - - test("Stream model output", async () => { - const remote = new RemoteRunnable({ url: `${BASE_URL}/b` }); - const stream = await remote.stream({ text: "What are the 5 best apples?" }); - let chunkCount = 0; - let accumulator: AIMessageChunk | null = null; - for await (const chunk of stream) { - const innerChunk = chunk as AIMessageChunk; - accumulator = accumulator ? accumulator.concat(innerChunk) : innerChunk; - chunkCount += 1; - } - expect(chunkCount).toBe(18); - expect(accumulator?.content).toEqual( - '"object1, object2, object3, object4, object5"' - ); - }); - - test("Stream legacy data type formats", async () => { - const remote = new RemoteRunnable({ url: `${BASE_URL}/strange_types` }); - const stream = await remote.stream({ text: "What are the 5 best apples?" }); - const chunks = []; - for await (const chunk of stream) { - console.log(chunk); - chunks.push(chunk); - } - expect(chunks[0]).toBeInstanceOf(HumanMessage); - expect(chunks[1]).toBeInstanceOf(ChatPromptValue); - expect((chunks[1] as ChatPromptValue).messages[0]).toBeInstanceOf( - SystemMessage - ); - expect((chunks[1] as ChatPromptValue).messages[1]).toBeInstanceOf( - AIMessage - ); - }); -});