@langchain/core
Version:
Core LangChain.js abstractions and schemas
1 lines • 12.4 kB
Source Map (JSON)
{"version":3,"file":"fake_model_builder.cjs","names":["BaseChatModel","BaseMessage","RunnableLambda","AIMessage"],"sources":["../../src/testing/fake_model_builder.ts"],"sourcesContent":["/* oxlint-disable @typescript-eslint/no-explicit-any */\nimport { CallbackManagerForLLMRun } from \"../callbacks/manager.js\";\nimport {\n BaseChatModel,\n BaseChatModelCallOptions,\n} from \"../language_models/chat_models.js\";\nimport {\n BaseLanguageModelInput,\n StructuredOutputMethodOptions,\n StructuredOutputMethodParams,\n} from \"../language_models/base.js\";\nimport { BaseMessage, AIMessage } from \"../messages/index.js\";\nimport type { ToolCall } from \"../messages/tool.js\";\nimport type { ChatResult } from \"../outputs.js\";\nimport { Runnable, RunnableLambda } from \"../runnables/base.js\";\nimport { StructuredTool } from \"../tools/index.js\";\nimport type { InteropZodType } from \"../utils/types/zod.js\";\nimport type { ToolSpec } from \"../utils/testing/chat_models.js\";\n\ntype ResponseFactory = (messages: BaseMessage[]) => BaseMessage | Error;\n\ntype QueueEntry =\n | { kind: \"message\"; message: BaseMessage }\n | { kind: \"toolCalls\"; toolCalls: ToolCall[] }\n | { kind: \"error\"; error: Error }\n | { kind: \"factory\"; factory: ResponseFactory };\n\ninterface FakeModelCall {\n messages: BaseMessage[];\n options: any;\n}\n\ninterface FakeModelState {\n callIndex: number;\n calls: FakeModelCall[];\n}\n\nfunction deriveContent(messages: BaseMessage[]): string {\n return messages\n .map((m) => m.text)\n .filter(Boolean)\n .join(\"-\");\n}\n\nlet idCounter = 0;\nfunction nextToolCallId(): string {\n idCounter += 1;\n return `fake_tc_${idCounter}`;\n}\n\n/**\n * A fake chat model for testing, created via {@link fakeModel}.\n *\n * Queue responses with `.respond()` and `.respondWithTools()`, then\n * pass the instance directly wherever a chat model is expected.\n * Responses are consumed in first-in-first-out order — one per `invoke()` call.\n * When all queued responses are consumed, further invocations throw.\n */\nexport class FakeBuiltModel extends BaseChatModel {\n private queue: QueueEntry[] = [];\n\n private _alwaysThrowError: Error | undefined;\n\n private _structuredResponseValue: any;\n\n private _tools: (StructuredTool | ToolSpec)[] = [];\n\n private _state: FakeModelState = { callIndex: 0, calls: [] };\n\n /**\n * All invocations recorded by this model, in order.\n * Each entry contains the `messages` array and `options` that were\n * passed to `invoke()`.\n */\n get calls(): FakeModelCall[] {\n return this._state.calls;\n }\n\n /**\n * The number of times this model has been invoked.\n */\n get callCount(): number {\n return this._state.calls.length;\n }\n\n constructor() {\n super({});\n }\n\n _llmType(): string {\n return \"fake-model-builder\";\n }\n\n _combineLLMOutput() {\n return [];\n }\n\n /**\n * Enqueue a response that the model will return on its next invocation.\n * @param entry A {@link BaseMessage} to return, an `Error` to throw, or\n * a factory `(messages) => BaseMessage | Error` for dynamic responses.\n * @returns `this`, for chaining.\n */\n respond(entry: BaseMessage | Error | ResponseFactory): this {\n if (typeof entry === \"function\") {\n this.queue.push({ kind: \"factory\", factory: entry });\n } else if (BaseMessage.isInstance(entry)) {\n this.queue.push({ kind: \"message\", message: entry });\n } else {\n this.queue.push({ kind: \"error\", error: entry });\n }\n return this;\n }\n\n /**\n * Enqueue an {@link AIMessage} that carries the given tool calls.\n * Content is derived from the input messages at invocation time.\n * @param toolCalls Array of tool calls. Each entry needs `name` and\n * `args`; `id` is optional and auto-generated when omitted.\n * @returns `this`, for chaining.\n */\n respondWithTools(\n toolCalls: Array<{ name: string; args: Record<string, any>; id?: string }>\n ): this {\n this.queue.push({\n kind: \"toolCalls\",\n toolCalls: toolCalls.map((tc) => ({\n name: tc.name,\n args: tc.args,\n id: tc.id ?? nextToolCallId(),\n type: \"tool_call\" as const,\n })),\n });\n return this;\n }\n\n /**\n * Make every invocation throw the given error, regardless of the queue.\n * @param error The error to throw.\n * @returns `this`, for chaining.\n */\n alwaysThrow(error: Error): this {\n this._alwaysThrowError = error;\n return this;\n }\n\n /**\n * Set the value that {@link withStructuredOutput} will resolve to.\n * @param value The structured object to return.\n * @returns `this`, for chaining.\n */\n structuredResponse(value: Record<string, any>): this {\n this._structuredResponseValue = value;\n return this;\n }\n\n /**\n * Bind tools to the model. Returns a new model that shares the same\n * response queue and call history.\n * @param tools The tools to bind, as {@link StructuredTool} instances or\n * plain {@link ToolSpec} objects.\n * @returns A new RunnableBinding with the tools bound.\n */\n bindTools(tools: (StructuredTool | ToolSpec)[]) {\n const merged = [...this._tools, ...tools];\n const next = new FakeBuiltModel();\n next.queue = this.queue;\n next._alwaysThrowError = this._alwaysThrowError;\n next._structuredResponseValue = this._structuredResponseValue;\n next._tools = merged;\n next._state = this._state;\n\n return next.withConfig({} as BaseChatModelCallOptions);\n }\n\n /**\n * Returns a {@link Runnable} that produces the {@link structuredResponse}\n * value. The schema argument is accepted for compatibility but ignored.\n * @param _params Schema or params (ignored).\n * @param _config Options (ignored).\n * @returns A Runnable that resolves to the structured response value.\n */\n withStructuredOutput<\n RunOutput extends Record<string, any> = Record<string, any>,\n >(\n _params:\n | StructuredOutputMethodParams<RunOutput, boolean>\n | InteropZodType<RunOutput>\n | Record<string, any>,\n _config?: StructuredOutputMethodOptions<boolean>\n ):\n | Runnable<BaseLanguageModelInput, RunOutput>\n | Runnable<\n BaseLanguageModelInput,\n { raw: BaseMessage; parsed: RunOutput }\n > {\n const { _structuredResponseValue } = this;\n return RunnableLambda.from(async () => {\n return _structuredResponseValue as RunOutput;\n }) as Runnable;\n }\n\n async _generate(\n messages: BaseMessage[],\n options?: this[\"ParsedCallOptions\"],\n _runManager?: CallbackManagerForLLMRun\n ): Promise<ChatResult> {\n this._state.calls.push({ messages: [...messages], options });\n\n const currentCallIndex = this._state.callIndex;\n this._state.callIndex += 1;\n\n if (this._alwaysThrowError) {\n throw this._alwaysThrowError;\n }\n\n const entry = this.queue[currentCallIndex];\n if (!entry) {\n throw new Error(\n `FakeModel: no response queued for invocation ${currentCallIndex} (${this.queue.length} total queued).`\n );\n }\n\n if (entry.kind === \"error\") {\n throw entry.error;\n }\n\n if (entry.kind === \"factory\") {\n const result = entry.factory(messages);\n if (!BaseMessage.isInstance(result)) {\n throw result;\n }\n return {\n generations: [{ text: \"\", message: result }],\n };\n }\n\n if (entry.kind === \"message\") {\n return {\n generations: [{ text: \"\", message: entry.message }],\n };\n }\n\n const content = deriveContent(messages);\n const message = new AIMessage({\n content,\n id: currentCallIndex.toString(),\n tool_calls:\n entry.toolCalls.length > 0\n ? entry.toolCalls.map((tc) => ({\n ...tc,\n type: \"tool_call\" as const,\n }))\n : undefined,\n });\n\n return {\n generations: [{ text: content, message }],\n llmOutput: {},\n };\n }\n}\n\n/**\n * Creates a new {@link FakeBuiltModel} for testing.\n *\n * Returns a chainable builder — queue responses, then pass the model\n * anywhere a chat model is expected. Responses are consumed in FIFO\n * order, one per `invoke()` call.\n *\n * ## API summary\n *\n * | Method | Description |\n * | --- | --- |\n * | `fakeModel()` | Creates a new fake chat model. Returns a chainable builder. |\n * | `.respond(message)` | Queue an `AIMessage` (or any `BaseMessage`) to return on the next invocation. |\n * | `.respond(error)` | Queue an `Error` to throw on the next invocation. |\n * | `.respond(factory)` | Queue a function `(messages) => BaseMessage \\| Error` for dynamic responses. |\n * | `.respondWithTools(toolCalls)` | Shorthand for `.respond()` with tool calls. Each entry needs `name` and `args`; `id` is optional. |\n * | `.alwaysThrow(error)` | Make every invocation throw this error, regardless of the queue. |\n * | `.structuredResponse(value)` | Set the value returned by `.withStructuredOutput()`. |\n * | `.bindTools(tools)` | Bind tools to the model. Returns a `RunnableBinding` that shares the response queue and call recording. |\n * | `.withStructuredOutput(schema)` | Returns a runnable that produces the `.structuredResponse()` value. |\n * | `.calls` | Array of `{ messages, options }` for every invocation (read-only). |\n * | `.callCount` | Number of times the model has been invoked. |\n *\n * @example\n * ```typescript\n * const model = fakeModel()\n * .respondWithTools([{ name: \"search\", args: { query: \"weather\" } }])\n * .respond(new AIMessage(\"Sunny and warm.\"));\n *\n * const r1 = await model.invoke([new HumanMessage(\"What's the weather?\")]);\n * // r1.tool_calls[0].name === \"search\"\n *\n * const r2 = await model.invoke([new HumanMessage(\"Thanks\")]);\n * // r2.content === \"Sunny and warm.\"\n * ```\n */\nexport function fakeModel(): FakeBuiltModel {\n return new FakeBuiltModel();\n}\n"],"mappings":";;;;;;AAqCA,SAAS,cAAc,UAAiC;CACtD,OAAO,SACJ,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;AACb;AAEA,IAAI,YAAY;AAChB,SAAS,iBAAyB;CAChC,aAAa;CACb,OAAO,WAAW;AACpB;;;;;;;;;AAUA,IAAa,iBAAb,MAAa,uBAAuBA,oCAAAA,cAAc;CAChD,QAA8B,CAAC;CAE/B;CAEA;CAEA,SAAgD,CAAC;CAEjD,SAAiC;EAAE,WAAW;EAAG,OAAO,CAAC;CAAE;;;;;;CAO3D,IAAI,QAAyB;EAC3B,OAAO,KAAK,OAAO;CACrB;;;;CAKA,IAAI,YAAoB;EACtB,OAAO,KAAK,OAAO,MAAM;CAC3B;CAEA,cAAc;EACZ,MAAM,CAAC,CAAC;CACV;CAEA,WAAmB;EACjB,OAAO;CACT;CAEA,oBAAoB;EAClB,OAAO,CAAC;CACV;;;;;;;CAQA,QAAQ,OAAoD;EAC1D,IAAI,OAAO,UAAU,YACnB,KAAK,MAAM,KAAK;GAAE,MAAM;GAAW,SAAS;EAAM,CAAC;OAC9C,IAAIC,aAAAA,YAAY,WAAW,KAAK,GACrC,KAAK,MAAM,KAAK;GAAE,MAAM;GAAW,SAAS;EAAM,CAAC;OAEnD,KAAK,MAAM,KAAK;GAAE,MAAM;GAAS,OAAO;EAAM,CAAC;EAEjD,OAAO;CACT;;;;;;;;CASA,iBACE,WACM;EACN,KAAK,MAAM,KAAK;GACd,MAAM;GACN,WAAW,UAAU,KAAK,QAAQ;IAChC,MAAM,GAAG;IACT,MAAM,GAAG;IACT,IAAI,GAAG,MAAM,eAAe;IAC5B,MAAM;GACR,EAAE;EACJ,CAAC;EACD,OAAO;CACT;;;;;;CAOA,YAAY,OAAoB;EAC9B,KAAK,oBAAoB;EACzB,OAAO;CACT;;;;;;CAOA,mBAAmB,OAAkC;EACnD,KAAK,2BAA2B;EAChC,OAAO;CACT;;;;;;;;CASA,UAAU,OAAsC;EAC9C,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,GAAG,KAAK;EACxC,MAAM,OAAO,IAAI,eAAe;EAChC,KAAK,QAAQ,KAAK;EAClB,KAAK,oBAAoB,KAAK;EAC9B,KAAK,2BAA2B,KAAK;EACrC,KAAK,SAAS;EACd,KAAK,SAAS,KAAK;EAEnB,OAAO,KAAK,WAAW,CAAC,CAA6B;CACvD;;;;;;;;CASA,qBAGE,SAIA,SAMI;EACJ,MAAM,EAAE,6BAA6B;EACrC,OAAOC,eAAAA,eAAe,KAAK,YAAY;GACrC,OAAO;EACT,CAAC;CACH;CAEA,MAAM,UACJ,UACA,SACA,aACqB;EACrB,KAAK,OAAO,MAAM,KAAK;GAAE,UAAU,CAAC,GAAG,QAAQ;GAAG;EAAQ,CAAC;EAE3D,MAAM,mBAAmB,KAAK,OAAO;EACrC,KAAK,OAAO,aAAa;EAEzB,IAAI,KAAK,mBACP,MAAM,KAAK;EAGb,MAAM,QAAQ,KAAK,MAAM;EACzB,IAAI,CAAC,OACH,MAAM,IAAI,MACR,gDAAgD,iBAAiB,IAAI,KAAK,MAAM,OAAO,gBACzF;EAGF,IAAI,MAAM,SAAS,SACjB,MAAM,MAAM;EAGd,IAAI,MAAM,SAAS,WAAW;GAC5B,MAAM,SAAS,MAAM,QAAQ,QAAQ;GACrC,IAAI,CAACD,aAAAA,YAAY,WAAW,MAAM,GAChC,MAAM;GAER,OAAO,EACL,aAAa,CAAC;IAAE,MAAM;IAAI,SAAS;GAAO,CAAC,EAC7C;EACF;EAEA,IAAI,MAAM,SAAS,WACjB,OAAO,EACL,aAAa,CAAC;GAAE,MAAM;GAAI,SAAS,MAAM;EAAQ,CAAC,EACpD;EAGF,MAAM,UAAU,cAAc,QAAQ;EAatC,OAAO;GACL,aAAa,CAAC;IAAE,MAAM;IAAS,SAAA,IAbbE,WAAAA,UAAU;KAC5B;KACA,IAAI,iBAAiB,SAAS;KAC9B,YACE,MAAM,UAAU,SAAS,IACrB,MAAM,UAAU,KAAK,QAAQ;MAC3B,GAAG;MACH,MAAM;KACR,EAAE,IACF,KAAA;IACR,CAGuC;GAAE,CAAC;GACxC,WAAW,CAAC;EACd;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,YAA4B;CAC1C,OAAO,IAAI,eAAe;AAC5B"}