@langchain/core
Version:
Core LangChain.js abstractions and schemas
604 lines (603 loc) • 27.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.SimpleChatModel = exports.BaseChatModel = exports.createChatMessageChunkEncoderStream = void 0;
const zod_to_json_schema_1 = require("zod-to-json-schema");
const index_js_1 = require("../messages/index.cjs");
const outputs_js_1 = require("../outputs.cjs");
const base_js_1 = require("./base.cjs");
const manager_js_1 = require("../callbacks/manager.cjs");
const base_js_2 = require("../runnables/base.cjs");
const stream_js_1 = require("../utils/stream.cjs");
const passthrough_js_1 = require("../runnables/passthrough.cjs");
const is_zod_schema_js_1 = require("../utils/types/is_zod_schema.cjs");
const base_js_3 = require("../callbacks/base.cjs");
/**
* Creates a transform stream for encoding chat message chunks.
* @deprecated Use {@link BytesOutputParser} instead
* @returns A TransformStream instance that encodes chat message chunks.
*/
function createChatMessageChunkEncoderStream() {
const textEncoder = new TextEncoder();
return new TransformStream({
transform(chunk, controller) {
controller.enqueue(textEncoder.encode(typeof chunk.content === "string"
? chunk.content
: JSON.stringify(chunk.content)));
},
});
}
exports.createChatMessageChunkEncoderStream = createChatMessageChunkEncoderStream;
/**
* Base class for chat models. It extends the BaseLanguageModel class and
* provides methods for generating chat based on input messages.
*/
class BaseChatModel extends base_js_1.BaseLanguageModel {
constructor(fields) {
super(fields);
// Only ever instantiated in main LangChain
Object.defineProperty(this, "lc_namespace", {
enumerable: true,
configurable: true,
writable: true,
value: ["langchain", "chat_models", this._llmType()]
});
Object.defineProperty(this, "disableStreaming", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
}
_separateRunnableConfigFromCallOptionsCompat(options) {
// For backwards compat, keep `signal` in both runnableConfig and callOptions
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);
const result = await this.generatePrompt([promptValue], options, options?.callbacks);
const chatGeneration = result.generations[0][0];
// TODO: Remove cast after figuring out inheritance
return chatGeneration.message;
}
// eslint-disable-next-line require-yield
async *_streamResponseChunks(_messages, _options, _runManager) {
throw new Error("Not implemented.");
}
async *_streamIterator(input, options) {
// Subclass check required to avoid double callbacks with default implementation
if (this._streamResponseChunks ===
BaseChatModel.prototype._streamResponseChunks ||
this.disableStreaming) {
yield this.invoke(input, options);
}
else {
const prompt = BaseChatModel._convertInputToPromptValue(input);
const messages = prompt.toChatMessages();
const [runnableConfig, callOptions] = this._separateRunnableConfigFromCallOptionsCompat(options);
const inheritableMetadata = {
...runnableConfig.metadata,
...this.getLsParams(callOptions),
};
const callbackManager_ = await manager_js_1.CallbackManager.configure(runnableConfig.callbacks, this.callbacks, runnableConfig.tags, this.tags, inheritableMetadata, this.metadata, { verbose: this.verbose });
const extra = {
options: callOptions,
invocation_params: this?.invocationParams(callOptions),
batch_size: 1,
};
const runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), [messages], runnableConfig.runId, undefined, extra, undefined, undefined, runnableConfig.runName);
let generationChunk;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let llmOutput;
try {
for await (const chunk of this._streamResponseChunks(messages, callOptions, runManagers?.[0])) {
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,
};
yield chunk.message;
if (!generationChunk) {
generationChunk = chunk;
}
else {
generationChunk = generationChunk.concat(chunk);
}
if ((0, index_js_1.isAIMessageChunk)(chunk.message) &&
chunk.message.usage_metadata !== undefined) {
llmOutput = {
tokenUsage: {
promptTokens: chunk.message.usage_metadata.input_tokens,
completionTokens: chunk.message.usage_metadata.output_tokens,
totalTokens: chunk.message.usage_metadata.total_tokens,
},
};
}
}
}
catch (err) {
await Promise.all((runManagers ?? []).map((runManager) => runManager?.handleLLMError(err)));
throw err;
}
await Promise.all((runManagers ?? []).map((runManager) => runManager?.handleLLMEnd({
// TODO: Remove cast after figuring out inheritance
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,
};
}
/** @ignore */
async _generateUncached(messages, parsedOptions, handledOptions, startedRunManagers) {
const baseMessages = messages.map((messageList) => messageList.map(index_js_1.coerceMessageLikeToMessage));
let runManagers;
if (startedRunManagers !== undefined &&
startedRunManagers.length === baseMessages.length) {
runManagers = startedRunManagers;
}
else {
const inheritableMetadata = {
...handledOptions.metadata,
...this.getLsParams(parsedOptions),
};
// create callback manager and start run
const callbackManager_ = await manager_js_1.CallbackManager.configure(handledOptions.callbacks, this.callbacks, handledOptions.tags, this.tags, inheritableMetadata, this.metadata, { verbose: this.verbose });
const extra = {
options: parsedOptions,
invocation_params: this?.invocationParams(parsedOptions),
batch_size: 1,
};
runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), baseMessages, handledOptions.runId, undefined, extra, undefined, undefined, handledOptions.runName);
}
const generations = [];
const llmOutputs = [];
// Even if stream is not explicitly called, check if model is implicitly
// called from streamEvents() or streamLog() to get all streamed events.
// Bail out if _streamResponseChunks not overridden
const hasStreamingHandler = !!runManagers?.[0].handlers.find(base_js_3.callbackHandlerPrefersStreaming);
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;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let llmOutput;
for await (const chunk of stream) {
if (chunk.message.id == null) {
const runId = runManagers?.at(0)?.runId;
if (runId != null)
chunk.message._updateId(`run-${runId}`);
}
if (aggregated === undefined) {
aggregated = chunk;
}
else {
aggregated = (0, stream_js_1.concat)(aggregated, chunk);
}
if ((0, index_js_1.isAIMessageChunk)(chunk.message) &&
chunk.message.usage_metadata !== undefined) {
llmOutput = {
tokenUsage: {
promptTokens: chunk.message.usage_metadata.input_tokens,
completionTokens: chunk.message.usage_metadata.output_tokens,
totalTokens: chunk.message.usage_metadata.total_tokens,
},
};
}
}
if (aggregated === undefined) {
throw new Error("Received empty response from chat model call.");
}
generations.push([aggregated]);
await runManagers?.[0].handleLLMEnd({
generations,
llmOutput,
});
}
catch (e) {
await runManagers?.[0].handleLLMError(e);
throw e;
}
}
else {
// generate results
const results = await Promise.allSettled(baseMessages.map((messageList, i) => this._generate(messageList, { ...parsedOptions, promptIndex: i }, runManagers?.[i])));
// handle results
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 {
// status === "rejected"
await runManagers?.[i]?.handleLLMError(pResult.reason);
return Promise.reject(pResult.reason);
}
}));
}
// create combined output
const output = {
generations,
llmOutput: llmOutputs.length
? this._combineLLMOutput?.(...llmOutputs)
: undefined,
};
Object.defineProperty(output, outputs_js_1.RUN_KEY, {
value: runManagers
? { runIds: runManagers?.map((manager) => manager.runId) }
: undefined,
configurable: true,
});
return output;
}
async _generateCached({ messages, cache, llmStringKey, parsedOptions, handledOptions, }) {
const baseMessages = messages.map((messageList) => messageList.map(index_js_1.coerceMessageLikeToMessage));
const inheritableMetadata = {
...handledOptions.metadata,
...this.getLsParams(parsedOptions),
};
// create callback manager and start run
const callbackManager_ = await manager_js_1.CallbackManager.configure(handledOptions.callbacks, this.callbacks, handledOptions.tags, this.tags, inheritableMetadata, this.metadata, { verbose: this.verbose });
const extra = {
options: parsedOptions,
invocation_params: this?.invocationParams(parsedOptions),
batch_size: 1,
};
const runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), baseMessages, handledOptions.runId, undefined, extra, undefined, undefined, handledOptions.runName);
// generate results
const missingPromptIndices = [];
const results = await Promise.allSettled(baseMessages.map(async (baseMessage, index) => {
// Join all content into one string for the prompt index
const prompt = BaseChatModel._convertInputToPromptValue(baseMessage).toString();
const result = await cache.lookup(prompt, llmStringKey);
if (result == null) {
missingPromptIndices.push(index);
}
return result;
}));
// Map run managers to the results before filtering out null results
// Null results are just absent from the cache.
const cachedResults = results
.map((result, index) => ({ result, runManager: runManagers?.[index] }))
.filter(({ result }) => (result.status === "fulfilled" && result.value != null) ||
result.status === "rejected");
// Handle results and call run managers
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 &&
(0, index_js_1.isBaseMessage)(result.message) &&
(0, index_js_1.isAIMessage)(result.message)) {
// eslint-disable-next-line no-param-reassign
result.message.usage_metadata = {
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
};
}
// eslint-disable-next-line no-param-reassign
result.generationInfo = {
...result.generationInfo,
tokenUsage: {},
};
return result;
});
if (result.length) {
await runManager?.handleLLMNewToken(result[0].text);
}
return runManager?.handleLLMEnd({
generations: [result],
}, undefined, undefined, undefined, {
cached: true,
});
}
else {
// status === "rejected"
await runManager?.handleLLMError(promiseResult.reason, undefined, undefined, undefined, {
cached: true,
});
return Promise.reject(promiseResult.reason);
}
}));
const output = {
generations,
missingPromptIndices,
startedRunManagers: runManagers,
};
// This defines RUN_KEY as a non-enumerable property on the output object
// so that it is not serialized when the output is stringified, and so that
// it isnt included when listing the keys of the output object.
Object.defineProperty(output, outputs_js_1.RUN_KEY, {
value: runManagers
? { runIds: runManagers?.map((manager) => manager.runId) }
: undefined,
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) {
// parse call options
let parsedOptions;
if (Array.isArray(options)) {
parsedOptions = { stop: options };
}
else {
parsedOptions = options;
}
const baseMessages = messages.map((messageList) => messageList.map(index_js_1.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 !== undefined
? missingPromptIndices.map((i) => startedRunManagers?.[i])
: undefined);
await Promise.all(results.generations.map(async (generation, index) => {
const promptIndex = missingPromptIndices[index];
generations[promptIndex] = generation;
// Join all content into one string for the prompt index
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
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
invocationParams(_options) {
return {};
}
_modelType() {
return "base_chat_model";
}
/**
* @deprecated
* Return a json-like object representing this LLM.
*/
serialize() {
return {
...this.invocationParams(),
_type: this._llmType(),
_model: this._modelType(),
};
}
/**
* 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);
}
/**
* @deprecated Use .invoke() instead. Will be removed in 0.2.0.
*
* Makes a single call to the chat model.
* @param messages An array 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 a BaseMessage.
*/
async call(messages, options, callbacks) {
const result = await this.generate([messages.map(index_js_1.coerceMessageLikeToMessage)], options, callbacks);
const generations = result.generations;
return generations[0][0].message;
}
/**
* @deprecated Use .invoke() instead. Will be removed in 0.2.0.
*
* Makes a single call to the chat model with a prompt value.
* @param promptValue The value of the prompt.
* @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 a BaseMessage.
*/
async callPrompt(promptValue, options, callbacks) {
const promptMessages = promptValue.toChatMessages();
return this.call(promptMessages, options, callbacks);
}
/**
* @deprecated Use .invoke() instead. Will be removed in 0.2.0.
*
* Predicts the next message based on the input messages.
* @param messages An array 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 a BaseMessage.
*/
async predictMessages(messages, options, callbacks) {
return this.call(messages, options, callbacks);
}
/**
* @deprecated Use .invoke() instead. Will be removed in 0.2.0.
*
* Predicts the next message based on a text input.
* @param text The text input.
* @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 a string.
*/
async predict(text, options, callbacks) {
const message = new index_js_1.HumanMessage(text);
const result = await this.call([message], options, callbacks);
if (typeof result.content !== "string") {
throw new Error("Cannot use predict when output is not a string.");
}
return result.content;
}
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.`);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const schema = outputSchema;
const name = config?.name;
const description = schema.description ?? "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";
let tools;
if ((0, is_zod_schema_js_1.isZodSchema)(schema)) {
tools = [
{
type: "function",
function: {
name: functionName,
description,
parameters: (0, zod_to_json_schema_1.zodToJsonSchema)(schema),
},
},
];
}
else {
if ("name" in schema) {
functionName = schema.name;
}
tools = [
{
type: "function",
function: {
name: functionName,
description,
parameters: schema,
},
},
];
}
const llm = this.bindTools(tools);
const outputParser = base_js_2.RunnableLambda.from((input) => {
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;
});
if (!includeRaw) {
return llm.pipe(outputParser).withConfig({
runName: "StructuredOutput",
});
}
const parserAssign = passthrough_js_1.RunnablePassthrough.assign({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parsed: (input, config) => outputParser.invoke(input.raw, config),
});
const parserNone = passthrough_js_1.RunnablePassthrough.assign({
parsed: () => null,
});
const parsedWithFallback = parserAssign.withFallbacks({
fallbacks: [parserNone],
});
return base_js_2.RunnableSequence.from([
{
raw: llm,
},
parsedWithFallback,
]).withConfig({
runName: "StructuredOutputRunnable",
});
}
}
exports.BaseChatModel = BaseChatModel;
/**
* An abstract class that extends BaseChatModel and provides a simple
* implementation of _generate.
*/
class SimpleChatModel extends BaseChatModel {
async _generate(messages, options, runManager) {
const text = await this._call(messages, options, runManager);
const message = new index_js_1.AIMessage(text);
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,
},
],
};
}
}
exports.SimpleChatModel = SimpleChatModel;
;