@langchain/core
Version:
Core LangChain.js abstractions and schemas
543 lines (542 loc) • 23.1 kB
JavaScript
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
import { ModelAbortError } from "../errors/index.js";
import { convertToOpenAIImageBlock, isBase64ContentBlock, isURLContentBlock } from "../messages/content/data.js";
import { isBaseMessage } from "../messages/base.js";
import { AIMessage, AIMessageChunk, isAIMessage, isAIMessageChunk } from "../messages/ai.js";
import { coerceMessageLikeToMessage } from "../messages/utils.js";
import { getEnvironmentVariable } from "../utils/env.js";
import { callbackHandlerPrefersChatModelStreamEvents, callbackHandlerPrefersStreaming } from "../callbacks/base.js";
import { CallbackManager } from "../callbacks/manager.js";
import { concat } from "../utils/stream.js";
import { RUN_KEY } from "../outputs.js";
import { getSchemaDescription, isInteropZodSchema } from "../utils/types/zod.js";
import { isSerializableSchema } from "../utils/standard_schema.js";
import { toJsonSchema } from "../utils/json_schema.js";
import { RunnableLambda } from "../runnables/base.js";
import "../messages/index.js";
import { BaseLanguageModel } from "./base.js";
import { castStandardMessageContent, iife } from "./utils.js";
import { assembleStructuredOutputPipeline } from "./structured_output.js";
import { ChatModelStream } from "./stream.js";
import { convertChunksToEvents } from "./compat.js";
//#region src/language_models/chat_models.ts
var chat_models_exports = /* @__PURE__ */ __exportAll({
BaseChatModel: () => BaseChatModel,
SimpleChatModel: () => SimpleChatModel
});
function _formatForTracing(messages) {
const messagesToTrace = [];
for (const message of messages) {
let messageToTrace = message;
if (Array.isArray(message.content)) for (let idx = 0; idx < message.content.length; idx++) {
const block = message.content[idx];
if (isURLContentBlock(block) || isBase64ContentBlock(block)) {
if (messageToTrace === message) messageToTrace = new message.constructor({
...messageToTrace,
content: [
...message.content.slice(0, idx),
convertToOpenAIImageBlock(block),
...message.content.slice(idx + 1)
]
});
}
}
messagesToTrace.push(messageToTrace);
}
return messagesToTrace;
}
/**
* Base class for chat models. It extends the BaseLanguageModel class and
* provides methods for generating chat based on input messages.
*/
var BaseChatModel = class BaseChatModel extends BaseLanguageModel {
lc_namespace = [
"langchain",
"chat_models",
this._llmType()
];
disableStreaming = false;
outputVersion;
get callKeys() {
return [...super.callKeys, "outputVersion"];
}
constructor(fields) {
super(fields);
this.outputVersion = iife(() => {
const outputVersion = fields.outputVersion ?? getEnvironmentVariable("LC_OUTPUT_VERSION");
if (outputVersion && ["v0", "v1"].includes(outputVersion)) return outputVersion;
return "v0";
});
}
_separateRunnableConfigFromCallOptionsCompat(options) {
const [runnableConfig, callOptions] = super._separateRunnableConfigFromCallOptions(options);
callOptions.signal = runnableConfig.signal;
return [runnableConfig, callOptions];
}
/**
* Invokes the chat model with a single input.
* @param input The input for the language model.
* @param options The call options.
* @returns A Promise that resolves to a BaseMessageChunk.
*/
async invoke(input, options) {
const promptValue = BaseChatModel._convertInputToPromptValue(input);
return (await this.generatePrompt([promptValue], options, options?.callbacks)).generations[0][0].message;
}
async *_streamResponseChunks(_messages, _options, _runManager) {
throw new Error("Not implemented.");
}
/**
* Stream chat model events using the new content-block-centric protocol.
*
* Override this method to provide native event streaming from the provider SDK.
* The default implementation bridges from `_streamResponseChunks` by
* synthesizing lifecycle events from `ChatGenerationChunk` objects.
*
* ## Event lifecycle
*
* ```
* MessageStart
* -> ContentBlockStart(index, contentBlock)
* -> ContentBlockDelta(index, delta) ...
* -> ContentBlockFinish(index, contentBlock)
* -> MessageFinish(reason, usage?)
* ```
*
* Content blocks may interleave (e.g., parallel tool calls). The only
* invariant: a block's start precedes its deltas, and its deltas precede
* its finish.
*
* @param messages - The input messages.
* @param options - Parsed call options.
* @param runManager - Optional callback manager for the run.
* @returns An async generator of {@link ChatModelStreamEvent}.
*/
async *_streamChatModelEvents(messages, options, runManager) {
yield* convertChunksToEvents(this._streamResponseChunks(messages, options, runManager), { signal: options.signal });
}
/**
* Create a {@link ChatModelStream} for the given input.
*
* Returns a stream object that is both `AsyncIterable<ChatModelStreamEvent>`
* and `PromiseLike<AIMessage>`, with typed sub-stream accessors for
* `.text`, `.toolCalls`, `.reasoning`, and `.usage`.
*
* @param input - The input messages.
* @param options - Optional call options.
* @returns A {@link ChatModelStream}.
*
* @example
* ```ts
* const stream = model.streamV2([{ role: "user", content: "Hello" }]);
*
* // Stream text
* for await (const token of stream.text) {
* process.stdout.write(token);
* }
*
* // Or await the full message
* const message = await stream;
* ```
*/
streamV2(input, options) {
const messages = BaseChatModel._convertInputToPromptValue(input).toChatMessages();
const [, callOptions] = this._separateRunnableConfigFromCallOptionsCompat(options);
return new ChatModelStream(this._streamChatModelEvents(messages, callOptions));
}
async *_streamIterator(input, options) {
if (this._streamResponseChunks === BaseChatModel.prototype._streamResponseChunks || this.disableStreaming) yield this.invoke(input, options);
else {
const messages = BaseChatModel._convertInputToPromptValue(input).toChatMessages();
const [runnableConfig, callOptions] = this._separateRunnableConfigFromCallOptionsCompat(options);
const inheritableMetadata = {
...runnableConfig.metadata,
...this.getLsParamsWithDefaults(callOptions)
};
const invocationParams = this.invocationParams(callOptions);
const callbackManager_ = await CallbackManager.configure(runnableConfig.callbacks, this.callbacks, runnableConfig.tags, this.tags, inheritableMetadata, this.metadata, {
verbose: this.verbose,
tracerInheritableMetadata: this._filterInvocationParamsForTracing(invocationParams)
});
const extra = {
options: callOptions,
invocation_params: invocationParams,
batch_size: 1
};
const outputVersion = callOptions.outputVersion ?? this.outputVersion;
const runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), [_formatForTracing(messages)], runnableConfig.runId, void 0, extra, void 0, void 0, runnableConfig.runName);
let generationChunk;
let llmOutput;
try {
for await (const chunk of this._streamResponseChunks(messages, callOptions, runManagers?.[0])) {
callOptions.signal?.throwIfAborted();
if (chunk.message.id == null) {
const runId = runManagers?.at(0)?.runId;
if (runId != null) chunk.message._updateId(`run-${runId}`);
}
chunk.message.response_metadata = {
...chunk.generationInfo,
...chunk.message.response_metadata
};
if (outputVersion === "v1") yield castStandardMessageContent(chunk.message);
else yield chunk.message;
if (!generationChunk) generationChunk = chunk;
else generationChunk = generationChunk.concat(chunk);
if (isAIMessageChunk(chunk.message) && chunk.message.usage_metadata !== void 0) llmOutput = { tokenUsage: {
promptTokens: chunk.message.usage_metadata.input_tokens,
completionTokens: chunk.message.usage_metadata.output_tokens,
totalTokens: chunk.message.usage_metadata.total_tokens
} };
}
callOptions.signal?.throwIfAborted();
} catch (err) {
await Promise.all((runManagers ?? []).map((runManager) => runManager?.handleLLMError(err)));
throw err;
}
await Promise.all((runManagers ?? []).map((runManager) => runManager?.handleLLMEnd({
generations: [[generationChunk]],
llmOutput
})));
}
}
getLsParams(options) {
const providerName = this.getName().startsWith("Chat") ? this.getName().replace("Chat", "") : this.getName();
return {
ls_model_type: "chat",
ls_stop: options.stop,
ls_provider: providerName
};
}
/**
* Wraps getLsParams() and always appends ls_integration.
* This ensures the integration tag is present even when
* partner packages fully override getLsParams().
*/
getLsParamsWithDefaults(options) {
return {
...this.getLsParams(options),
ls_integration: "langchain_chat_model"
};
}
/** @ignore */
async _generateUncached(messages, parsedOptions, handledOptions, startedRunManagers) {
const baseMessages = messages.map((messageList) => messageList.map(coerceMessageLikeToMessage));
let runManagers;
if (startedRunManagers !== void 0 && startedRunManagers.length === baseMessages.length) runManagers = startedRunManagers;
else {
const inheritableMetadata = {
...handledOptions.metadata,
...this.getLsParamsWithDefaults(parsedOptions)
};
const invocationParams = this.invocationParams(parsedOptions);
const callbackManager_ = await CallbackManager.configure(handledOptions.callbacks, this.callbacks, handledOptions.tags, this.tags, inheritableMetadata, this.metadata, {
verbose: this.verbose,
tracerInheritableMetadata: this._filterInvocationParamsForTracing(invocationParams)
});
const extra = {
options: parsedOptions,
invocation_params: invocationParams,
batch_size: 1
};
runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), baseMessages.map(_formatForTracing), handledOptions.runId, void 0, extra, void 0, void 0, handledOptions.runName);
}
const outputVersion = parsedOptions.outputVersion ?? this.outputVersion;
const generations = [];
const llmOutputs = [];
const hasChatModelStreamEventHandler = !!runManagers?.[0].handlers.find(callbackHandlerPrefersChatModelStreamEvents);
const hasStreamingHandler = !!runManagers?.[0].handlers.find(callbackHandlerPrefersStreaming);
if (hasChatModelStreamEventHandler && !this.disableStreaming && baseMessages.length === 1 && (this._streamChatModelEvents !== BaseChatModel.prototype._streamChatModelEvents || this._streamResponseChunks !== BaseChatModel.prototype._streamResponseChunks)) try {
let sawEvent = false;
const runManager = runManagers?.[0];
const events = this._streamChatModelEvents(baseMessages[0], parsedOptions);
const message = await new ChatModelStream({ async *[Symbol.asyncIterator]() {
for await (const event of events) {
parsedOptions.signal?.throwIfAborted();
sawEvent = true;
const streamEvent = event.event === "message-start" && event.id == null && runManager?.runId != null ? {
...event,
id: `run-${runManager.runId}`
} : event;
await runManager?.handleChatModelStreamEvent(streamEvent);
yield streamEvent;
}
} });
parsedOptions.signal?.throwIfAborted();
if (!sawEvent) throw new Error("Received empty response from chat model call.");
if (message.id == null) {
const runId = runManagers?.at(0)?.runId;
if (runId != null) message._updateId(`run-${runId}`);
}
const generation = {
text: message.text,
message
};
generations.push([generation]);
const llmOutput = message.usage_metadata !== void 0 ? { tokenUsage: {
promptTokens: message.usage_metadata.input_tokens,
completionTokens: message.usage_metadata.output_tokens,
totalTokens: message.usage_metadata.total_tokens
} } : void 0;
await runManagers?.[0].handleLLMEnd({
generations,
llmOutput
});
} catch (e) {
await runManagers?.[0].handleLLMError(e);
throw e;
}
else if (hasStreamingHandler && !this.disableStreaming && baseMessages.length === 1 && this._streamResponseChunks !== BaseChatModel.prototype._streamResponseChunks) try {
const stream = await this._streamResponseChunks(baseMessages[0], parsedOptions, runManagers?.[0]);
let aggregated;
let llmOutput;
for await (const chunk of stream) {
if (parsedOptions.signal?.aborted) {
const partialMessage = aggregated?.message;
throw new ModelAbortError("Model invocation was aborted.", partialMessage);
}
if (chunk.message.id == null) {
const runId = runManagers?.at(0)?.runId;
if (runId != null) chunk.message._updateId(`run-${runId}`);
}
if (aggregated === void 0) aggregated = chunk;
else aggregated = concat(aggregated, chunk);
if (isAIMessageChunk(chunk.message) && chunk.message.usage_metadata !== void 0) llmOutput = { tokenUsage: {
promptTokens: chunk.message.usage_metadata.input_tokens,
completionTokens: chunk.message.usage_metadata.output_tokens,
totalTokens: chunk.message.usage_metadata.total_tokens
} };
}
if (parsedOptions.signal?.aborted) {
const partialMessage = aggregated?.message;
throw new ModelAbortError("Model invocation was aborted.", partialMessage);
}
if (aggregated === void 0) throw new Error("Received empty response from chat model call.");
if (outputVersion === "v1") aggregated.message = castStandardMessageContent(aggregated.message);
generations.push([aggregated]);
await runManagers?.[0].handleLLMEnd({
generations,
llmOutput
});
} catch (e) {
await runManagers?.[0].handleLLMError(e);
throw e;
}
else {
const results = await Promise.allSettled(baseMessages.map(async (messageList, i) => {
const generateResults = await this._generate(messageList, {
...parsedOptions,
promptIndex: i
}, runManagers?.[i]);
if (outputVersion === "v1") for (const generation of generateResults.generations) generation.message = castStandardMessageContent(generation.message);
return generateResults;
}));
await Promise.all(results.map(async (pResult, i) => {
if (pResult.status === "fulfilled") {
const result = pResult.value;
for (const generation of result.generations) {
if (generation.message.id == null) {
const runId = runManagers?.at(0)?.runId;
if (runId != null) generation.message._updateId(`run-${runId}`);
}
generation.message.response_metadata = {
...generation.generationInfo,
...generation.message.response_metadata
};
}
if (result.generations.length === 1) result.generations[0].message.response_metadata = {
...result.llmOutput,
...result.generations[0].message.response_metadata
};
generations[i] = result.generations;
llmOutputs[i] = result.llmOutput;
return runManagers?.[i]?.handleLLMEnd({
generations: [result.generations],
llmOutput: result.llmOutput
});
} else {
await runManagers?.[i]?.handleLLMError(pResult.reason);
return Promise.reject(pResult.reason);
}
}));
}
const output = {
generations,
llmOutput: llmOutputs.length ? this._combineLLMOutput?.(...llmOutputs) : void 0
};
Object.defineProperty(output, RUN_KEY, {
value: runManagers ? { runIds: runManagers?.map((manager) => manager.runId) } : void 0,
configurable: true
});
return output;
}
async _generateCached({ messages, cache, llmStringKey, parsedOptions, handledOptions }) {
const baseMessages = messages.map((messageList) => messageList.map(coerceMessageLikeToMessage));
const inheritableMetadata = {
...handledOptions.metadata,
...this.getLsParamsWithDefaults(parsedOptions)
};
const invocationParams = this.invocationParams(parsedOptions);
const callbackManager_ = await CallbackManager.configure(handledOptions.callbacks, this.callbacks, handledOptions.tags, this.tags, inheritableMetadata, this.metadata, {
verbose: this.verbose,
tracerInheritableMetadata: this._filterInvocationParamsForTracing(invocationParams)
});
const extra = {
options: parsedOptions,
invocation_params: invocationParams,
batch_size: 1
};
const runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), baseMessages.map(_formatForTracing), handledOptions.runId, void 0, extra, void 0, void 0, handledOptions.runName);
const missingPromptIndices = [];
const cachedResults = (await Promise.allSettled(baseMessages.map(async (baseMessage, index) => {
const prompt = BaseChatModel._convertInputToPromptValue(baseMessage).toString();
const result = await cache.lookup(prompt, llmStringKey);
if (result == null) missingPromptIndices.push(index);
return result;
}))).map((result, index) => ({
result,
runManager: runManagers?.[index]
})).filter(({ result }) => result.status === "fulfilled" && result.value != null || result.status === "rejected");
const outputVersion = parsedOptions.outputVersion ?? this.outputVersion;
const generations = [];
await Promise.all(cachedResults.map(async ({ result: promiseResult, runManager }, i) => {
if (promiseResult.status === "fulfilled") {
const result = promiseResult.value;
generations[i] = result.map((result) => {
if ("message" in result && isBaseMessage(result.message) && isAIMessage(result.message)) {
result.message.usage_metadata = {
input_tokens: 0,
output_tokens: 0,
total_tokens: 0
};
if (outputVersion === "v1") result.message = castStandardMessageContent(result.message);
}
result.generationInfo = {
...result.generationInfo,
tokenUsage: {}
};
return result;
});
if (result.length) await runManager?.handleLLMNewToken(result[0].text);
return runManager?.handleLLMEnd({ generations: [result] }, void 0, void 0, void 0, { cached: true });
} else {
await runManager?.handleLLMError(promiseResult.reason, void 0, void 0, void 0, { cached: true });
return Promise.reject(promiseResult.reason);
}
}));
const output = {
generations,
missingPromptIndices,
startedRunManagers: runManagers
};
Object.defineProperty(output, RUN_KEY, {
value: runManagers ? { runIds: runManagers?.map((manager) => manager.runId) } : void 0,
configurable: true
});
return output;
}
/**
* Generates chat based on the input messages.
* @param messages An array of arrays of BaseMessage instances.
* @param options The call options or an array of stop sequences.
* @param callbacks The callbacks for the language model.
* @returns A Promise that resolves to an LLMResult.
*/
async generate(messages, options, callbacks) {
let parsedOptions;
if (Array.isArray(options)) parsedOptions = { stop: options };
else parsedOptions = options;
const baseMessages = messages.map((messageList) => messageList.map(coerceMessageLikeToMessage));
const [runnableConfig, callOptions] = this._separateRunnableConfigFromCallOptionsCompat(parsedOptions);
runnableConfig.callbacks = runnableConfig.callbacks ?? callbacks;
if (!this.cache) return this._generateUncached(baseMessages, callOptions, runnableConfig);
const { cache } = this;
const llmStringKey = this._getSerializedCacheKeyParametersForCall(callOptions);
const { generations, missingPromptIndices, startedRunManagers } = await this._generateCached({
messages: baseMessages,
cache,
llmStringKey,
parsedOptions: callOptions,
handledOptions: runnableConfig
});
let llmOutput = {};
if (missingPromptIndices.length > 0) {
const results = await this._generateUncached(missingPromptIndices.map((i) => baseMessages[i]), callOptions, runnableConfig, startedRunManagers !== void 0 ? missingPromptIndices.map((i) => startedRunManagers?.[i]) : void 0);
await Promise.all(results.generations.map(async (generation, index) => {
const promptIndex = missingPromptIndices[index];
generations[promptIndex] = generation;
const prompt = BaseChatModel._convertInputToPromptValue(baseMessages[promptIndex]).toString();
return cache.update(prompt, llmStringKey, generation);
}));
llmOutput = results.llmOutput ?? {};
}
return {
generations,
llmOutput
};
}
/**
* Get the parameters used to invoke the model
*/
invocationParams(_options) {
return {};
}
_modelType() {
return "base_chat_model";
}
/**
* Generates a prompt based on the input prompt values.
* @param promptValues An array of BasePromptValue instances.
* @param options The call options or an array of stop sequences.
* @param callbacks The callbacks for the language model.
* @returns A Promise that resolves to an LLMResult.
*/
async generatePrompt(promptValues, options, callbacks) {
const promptMessages = promptValues.map((promptValue) => promptValue.toChatMessages());
return this.generate(promptMessages, options, callbacks);
}
withStructuredOutput(outputSchema, config) {
if (typeof this.bindTools !== "function") throw new Error(`Chat model must implement ".bindTools()" to use withStructuredOutput.`);
if (config?.strict) throw new Error(`"strict" mode is not supported for this model by default.`);
const schema = outputSchema;
const name = config?.name;
const description = getSchemaDescription(schema) ?? "A function available to call.";
const method = config?.method;
const includeRaw = config?.includeRaw;
if (method === "jsonMode") throw new Error(`Base withStructuredOutput implementation only supports "functionCalling" as a method.`);
let functionName = name ?? "extract";
if (!isInteropZodSchema(schema) && !isSerializableSchema(schema) && "name" in schema) functionName = schema.name;
const asJsonSchema = isInteropZodSchema(schema) || isSerializableSchema(schema) ? toJsonSchema(schema) : schema;
const tools = [{
type: "function",
function: {
name: functionName,
description,
parameters: asJsonSchema
}
}];
return assembleStructuredOutputPipeline(this.bindTools(tools), RunnableLambda.from((input) => {
if (!AIMessageChunk.isInstance(input)) throw new Error("Input is not an AIMessageChunk.");
if (!input.tool_calls || input.tool_calls.length === 0) throw new Error("No tool calls found in the response.");
const toolCall = input.tool_calls.find((tc) => tc.name === functionName);
if (!toolCall) throw new Error(`No tool call found with name ${functionName}.`);
return toolCall.args;
}), includeRaw, includeRaw ? "StructuredOutputRunnable" : "StructuredOutput");
}
};
/**
* An abstract class that extends BaseChatModel and provides a simple
* implementation of _generate.
*/
var SimpleChatModel = class extends BaseChatModel {
async _generate(messages, options, runManager) {
const message = new AIMessage(await this._call(messages, options, runManager));
if (typeof message.content !== "string") throw new Error("Cannot generate with a simple chat model when output is not a string.");
return { generations: [{
text: message.content,
message
}] };
}
};
//#endregion
export { BaseChatModel, SimpleChatModel, chat_models_exports };
//# sourceMappingURL=chat_models.js.map