UNPKG

@langchain/core

Version:
1 lines 14.5 kB
{"version":3,"file":"stream.cjs","names":[],"sources":["../../../src/utils/testing/stream.ts"],"sourcesContent":["import type { AIMessage } from \"../../messages/index.js\";\nimport type { UsageMetadata } from \"../../messages/metadata.js\";\nimport { ChatModelStream } from \"../../language_models/stream.js\";\n\n/**\n * The `this` context that Vitest provides to custom matchers via `expect.extend`.\n * @see https://vitest.dev/guide/extending-matchers.html\n */\ninterface ExpectExtendThis {\n isNot?: boolean;\n equals(a: unknown, b: unknown): boolean;\n utils: {\n matcherHint(\n name: string,\n received?: string,\n expected?: string,\n options?: { isNot?: boolean }\n ): string;\n printReceived(value: unknown): string;\n printExpected(value: unknown): string;\n };\n}\n\ninterface ExpectationResult {\n pass: boolean;\n message: () => string;\n actual?: unknown;\n expected?: unknown;\n}\n\nfunction isChatModelStream(received: unknown): received is ChatModelStream {\n if (received == null || typeof received !== \"object\") {\n return false;\n }\n const stream = received as ChatModelStream;\n return (\n typeof stream.text !== \"undefined\" &&\n typeof stream.toolCalls !== \"undefined\" &&\n typeof stream.reasoning !== \"undefined\" &&\n typeof stream.usage !== \"undefined\" &&\n typeof stream.output !== \"undefined\" &&\n typeof stream[Symbol.asyncIterator] === \"function\"\n );\n}\n\nfunction matchesPartialObject(\n actual: Record<string, unknown> | undefined,\n expected: Record<string, unknown>,\n equals: ExpectExtendThis[\"equals\"]\n): boolean {\n if (actual == null) {\n return false;\n }\n return Object.entries(expected).every(([key, value]) =>\n equals(actual[key], value)\n );\n}\n\nfunction matchesStreamUsage(\n actual: UsageMetadata | undefined,\n expected: StreamUsageExpectation,\n equals: ExpectExtendThis[\"equals\"]\n): boolean {\n if (actual == null) {\n return false;\n }\n return matchesPartialObject(\n actual as Record<string, unknown>,\n expected as Record<string, unknown>,\n equals\n );\n}\n\nfunction getOutputText(message: AIMessage): string | undefined {\n const content = message.content as Array<{ type: string; text?: string }>;\n return content.find((block) => block.type === \"text\")?.text;\n}\n\nfunction matchesStreamOutput(\n message: AIMessage,\n expected: StreamOutputExpectation,\n equals: ExpectExtendThis[\"equals\"]\n): boolean {\n if (expected.id !== undefined && message.id !== expected.id) {\n return false;\n }\n if (expected.text !== undefined && getOutputText(message) !== expected.text) {\n return false;\n }\n if (expected.toolCalls !== undefined) {\n const calls = message.tool_calls ?? [];\n if (calls.length !== expected.toolCalls.length) {\n return false;\n }\n for (let i = 0; i < expected.toolCalls.length; i++) {\n const call = calls[i];\n const exp = expected.toolCalls[i]!;\n if (call?.name !== exp.name || !equals(call.args, exp.args)) {\n return false;\n }\n }\n }\n if (\n expected.usage !== undefined &&\n !matchesStreamUsage(message.usage_metadata, expected.usage, equals)\n ) {\n return false;\n }\n if (\n expected.responseMetadata !== undefined &&\n !matchesPartialObject(\n message.response_metadata as Record<string, unknown>,\n expected.responseMetadata,\n equals\n )\n ) {\n return false;\n }\n return true;\n}\n\nfunction invalidStreamResult(\n received: unknown,\n matcherName: string,\n utils: ExpectExtendThis[\"utils\"]\n): ExpectationResult {\n return {\n pass: false,\n message: () =>\n `${utils.matcherHint(matcherName)}\\n\\n` +\n `Expected: ChatModelStream (return value of model.streamEvents(\"Hello\"))\\n` +\n `Received: ${utils.printReceived(received)}`,\n actual: received,\n expected: \"ChatModelStream\",\n };\n}\n\nfunction applyNot(pass: boolean, isNot?: boolean): boolean {\n return isNot ? !pass : pass;\n}\n\nexport async function toHaveStreamText(\n this: ExpectExtendThis,\n received: unknown,\n expected: string\n): Promise<ExpectationResult> {\n const { isNot, utils } = this;\n const matcherName = \"toHaveStreamText\";\n\n if (!isChatModelStream(received)) {\n return invalidStreamResult(received, matcherName, utils);\n }\n\n const actual = await received.text;\n const pass = applyNot(actual === expected, isNot);\n\n return {\n pass,\n message: () =>\n `${utils.matcherHint(matcherName, undefined, undefined, { isNot })}\\n\\n` +\n `Expected stream text: ${isNot ? \"not \" : \"\"}${utils.printExpected(expected)}\\n` +\n `Received stream text: ${utils.printReceived(actual)}`,\n actual,\n expected,\n };\n}\n\nexport async function toHaveStreamReasoning(\n this: ExpectExtendThis,\n received: unknown,\n expected: string\n): Promise<ExpectationResult> {\n const { isNot, utils } = this;\n const matcherName = \"toHaveStreamReasoning\";\n\n if (!isChatModelStream(received)) {\n return invalidStreamResult(received, matcherName, utils);\n }\n\n const actual = await received.reasoning;\n const pass = applyNot(actual === expected, isNot);\n\n return {\n pass,\n message: () =>\n `${utils.matcherHint(matcherName, undefined, undefined, { isNot })}\\n\\n` +\n `Expected stream reasoning: ${isNot ? \"not \" : \"\"}${utils.printExpected(expected)}\\n` +\n `Received stream reasoning: ${utils.printReceived(actual)}`,\n actual,\n expected,\n };\n}\n\nexport type StreamToolCallExpectation = {\n name: string;\n args: Record<string, unknown>;\n};\n\nexport type StreamUsageExpectation = {\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n input_token_details?: Record<string, unknown>;\n output_token_details?: Record<string, unknown>;\n};\n\nexport type StreamOutputExpectation = {\n id?: string;\n text?: string;\n toolCalls?: StreamToolCallExpectation[];\n usage?: StreamUsageExpectation;\n responseMetadata?: Record<string, unknown>;\n};\n\nexport async function toHaveStreamToolCalls(\n this: ExpectExtendThis,\n received: unknown,\n expected: StreamToolCallExpectation[]\n): Promise<ExpectationResult> {\n const { isNot, utils } = this;\n const matcherName = \"toHaveStreamToolCalls\";\n\n if (!isChatModelStream(received)) {\n return invalidStreamResult(received, matcherName, utils);\n }\n\n const actual = await received.toolCalls;\n\n let pass =\n actual.length === expected.length &&\n expected.every((exp, i) => {\n const call = actual[i];\n return call?.name === exp.name && this.equals(call.args, exp.args);\n });\n pass = applyNot(pass, isNot);\n\n return {\n pass,\n message: () =>\n `${utils.matcherHint(matcherName, undefined, undefined, { isNot })}\\n\\n` +\n `Expected stream tool calls: ${utils.printExpected(expected)}\\n` +\n `Received stream tool calls: ${utils.printReceived(\n actual.map((tc) => ({ name: tc.name, args: tc.args }))\n )}`,\n actual: actual.map((tc) => ({ name: tc.name, args: tc.args })),\n expected,\n };\n}\n\nexport async function toHaveStreamUsage(\n this: ExpectExtendThis,\n received: unknown,\n expected: StreamUsageExpectation\n): Promise<ExpectationResult> {\n const { isNot, utils } = this;\n const matcherName = \"toHaveStreamUsage\";\n\n if (!isChatModelStream(received)) {\n return invalidStreamResult(received, matcherName, utils);\n }\n\n const actual = await received.usage;\n const pass = applyNot(\n matchesStreamUsage(actual, expected, this.equals),\n isNot\n );\n\n return {\n pass,\n message: () =>\n `${utils.matcherHint(matcherName, undefined, undefined, { isNot })}\\n\\n` +\n `Expected stream usage: ${utils.printExpected(expected)}\\n` +\n `Received stream usage: ${utils.printReceived(actual)}`,\n actual,\n expected,\n };\n}\n\nexport async function toHaveStreamOutput(\n this: ExpectExtendThis,\n received: unknown,\n expected: StreamOutputExpectation\n): Promise<ExpectationResult> {\n const { isNot, utils } = this;\n const matcherName = \"toHaveStreamOutput\";\n\n if (!isChatModelStream(received)) {\n return invalidStreamResult(received, matcherName, utils);\n }\n\n const message = await received.output;\n const pass = applyNot(\n matchesStreamOutput(message, expected, this.equals),\n isNot\n );\n\n return {\n pass,\n message: () =>\n `${utils.matcherHint(matcherName, undefined, undefined, { isNot })}\\n\\n` +\n `Expected stream output: ${utils.printExpected(expected)}\\n` +\n `Received stream output: ${utils.printReceived({\n id: message.id,\n text: getOutputText(message),\n tool_calls: message.tool_calls?.map((tc) => ({\n name: tc.name,\n args: tc.args,\n })),\n usage_metadata: message.usage_metadata,\n response_metadata: message.response_metadata,\n })}`,\n actual: message,\n expected,\n };\n}\n\n/** Stream matchers for `expect.extend()`. */\nexport const streamMatchers = {\n toHaveStreamText,\n toHaveStreamReasoning,\n toHaveStreamToolCalls,\n toHaveStreamUsage,\n toHaveStreamOutput,\n};\n\n/**\n * Custom assertion helpers for values returned by `BaseChatModel.streamEvents()`.\n *\n * These matchers consume the stream lazily through the corresponding\n * `ChatModelStream` promise-backed properties.\n *\n * @typeParam R - The assertion return type provided by the test framework.\n */\nexport interface StreamMatchers<R = unknown> {\n /**\n * Asserts that the stream resolves to the expected concatenated text.\n *\n * @param expected - The exact text expected from `ChatModelStream.text`.\n */\n toHaveStreamText(expected: string): R;\n\n /**\n * Asserts that the stream resolves to the expected concatenated reasoning text.\n *\n * @param expected - The exact reasoning text expected from `ChatModelStream.reasoning`.\n */\n toHaveStreamReasoning(expected: string): R;\n\n /**\n * Asserts that the stream resolves to the expected ordered tool calls.\n *\n * @param expected - Tool call names and arguments expected from `ChatModelStream.toolCalls`.\n */\n toHaveStreamToolCalls(expected: StreamToolCallExpectation[]): R;\n\n /**\n * Asserts that the stream resolves to usage metadata matching the expected fields.\n *\n * @param expected - A partial usage metadata object expected from `ChatModelStream.usage`.\n */\n toHaveStreamUsage(expected: StreamUsageExpectation): R;\n\n /**\n * Asserts that the final streamed output message matches the expected fields.\n *\n * @param expected - A partial output expectation checked against `ChatModelStream.output`.\n */\n toHaveStreamOutput(expected: StreamOutputExpectation): R;\n}\n"],"mappings":"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"}