@langchain/core
Version:
Core LangChain.js abstractions and schemas
284 lines (283 loc) • 10.3 kB
JavaScript
import { addLangChainErrorFields } from "../errors/index.js";
import { _isToolCall } from "../tools/utils.js";
import { AIMessage, AIMessageChunk } from "./ai.js";
import { isBaseMessage, _isMessageFieldWithRole, } from "./base.js";
import { ChatMessage, ChatMessageChunk, } from "./chat.js";
import { FunctionMessage, FunctionMessageChunk, } from "./function.js";
import { HumanMessage, HumanMessageChunk } from "./human.js";
import { SystemMessage, SystemMessageChunk } from "./system.js";
import { ToolMessage, } from "./tool.js";
function _coerceToolCall(toolCall) {
if (_isToolCall(toolCall)) {
return toolCall;
}
else if (typeof toolCall.id === "string" &&
toolCall.type === "function" &&
typeof toolCall.function === "object" &&
toolCall.function !== null &&
"arguments" in toolCall.function &&
typeof toolCall.function.arguments === "string" &&
"name" in toolCall.function &&
typeof toolCall.function.name === "string") {
// Handle OpenAI tool call format
return {
id: toolCall.id,
args: JSON.parse(toolCall.function.arguments),
name: toolCall.function.name,
type: "tool_call",
};
}
else {
// TODO: Throw an error?
return toolCall;
}
}
function isSerializedConstructor(x) {
return (typeof x === "object" &&
x != null &&
x.lc === 1 &&
Array.isArray(x.id) &&
x.kwargs != null &&
typeof x.kwargs === "object");
}
function _constructMessageFromParams(params) {
let type;
let rest;
// Support serialized messages
if (isSerializedConstructor(params)) {
const className = params.id.at(-1);
if (className === "HumanMessage" || className === "HumanMessageChunk") {
type = "user";
}
else if (className === "AIMessage" || className === "AIMessageChunk") {
type = "assistant";
}
else if (className === "SystemMessage" ||
className === "SystemMessageChunk") {
type = "system";
}
else if (className === "FunctionMessage" ||
className === "FunctionMessageChunk") {
type = "function";
}
else if (className === "ToolMessage" ||
className === "ToolMessageChunk") {
type = "tool";
}
else {
type = "unknown";
}
rest = params.kwargs;
}
else {
const { type: extractedType, ...otherParams } = params;
type = extractedType;
rest = otherParams;
}
if (type === "human" || type === "user") {
return new HumanMessage(rest);
}
else if (type === "ai" || type === "assistant") {
const { tool_calls: rawToolCalls, ...other } = rest;
if (!Array.isArray(rawToolCalls)) {
return new AIMessage(rest);
}
const tool_calls = rawToolCalls.map(_coerceToolCall);
return new AIMessage({ ...other, tool_calls });
}
else if (type === "system") {
return new SystemMessage(rest);
}
else if (type === "developer") {
return new SystemMessage({
...rest,
additional_kwargs: {
...rest.additional_kwargs,
__openai_role__: "developer",
},
});
}
else if (type === "tool" && "tool_call_id" in rest) {
return new ToolMessage({
...rest,
content: rest.content,
tool_call_id: rest.tool_call_id,
name: rest.name,
});
}
else {
const error = addLangChainErrorFields(new Error(`Unable to coerce message from array: only human, AI, system, developer, or tool message coercion is currently supported.\n\nReceived: ${JSON.stringify(params, null, 2)}`), "MESSAGE_COERCION_FAILURE");
throw error;
}
}
export function coerceMessageLikeToMessage(messageLike) {
if (typeof messageLike === "string") {
return new HumanMessage(messageLike);
}
else if (isBaseMessage(messageLike)) {
return messageLike;
}
if (Array.isArray(messageLike)) {
const [type, content] = messageLike;
return _constructMessageFromParams({ type, content });
}
else if (_isMessageFieldWithRole(messageLike)) {
const { role: type, ...rest } = messageLike;
return _constructMessageFromParams({ ...rest, type });
}
else {
return _constructMessageFromParams(messageLike);
}
}
/**
* This function is used by memory classes to get a string representation
* of the chat message history, based on the message content and role.
*/
export function getBufferString(messages, humanPrefix = "Human", aiPrefix = "AI") {
const string_messages = [];
for (const m of messages) {
let role;
if (m._getType() === "human") {
role = humanPrefix;
}
else if (m._getType() === "ai") {
role = aiPrefix;
}
else if (m._getType() === "system") {
role = "System";
}
else if (m._getType() === "function") {
role = "Function";
}
else if (m._getType() === "tool") {
role = "Tool";
}
else if (m._getType() === "generic") {
role = m.role;
}
else {
throw new Error(`Got unsupported message type: ${m._getType()}`);
}
const nameStr = m.name ? `${m.name}, ` : "";
const readableContent = typeof m.content === "string"
? m.content
: JSON.stringify(m.content, null, 2);
string_messages.push(`${role}: ${nameStr}${readableContent}`);
}
return string_messages.join("\n");
}
/**
* Maps messages from an older format (V1) to the current `StoredMessage`
* format. If the message is already in the `StoredMessage` format, it is
* returned as is. Otherwise, it transforms the V1 message into a
* `StoredMessage`. This function is important for maintaining
* compatibility with older message formats.
*/
function mapV1MessageToStoredMessage(message) {
// TODO: Remove this mapper when we deprecate the old message format.
if (message.data !== undefined) {
return message;
}
else {
const v1Message = message;
return {
type: v1Message.type,
data: {
content: v1Message.text,
role: v1Message.role,
name: undefined,
tool_call_id: undefined,
},
};
}
}
export function mapStoredMessageToChatMessage(message) {
const storedMessage = mapV1MessageToStoredMessage(message);
switch (storedMessage.type) {
case "human":
return new HumanMessage(storedMessage.data);
case "ai":
return new AIMessage(storedMessage.data);
case "system":
return new SystemMessage(storedMessage.data);
case "function":
if (storedMessage.data.name === undefined) {
throw new Error("Name must be defined for function messages");
}
return new FunctionMessage(storedMessage.data);
case "tool":
if (storedMessage.data.tool_call_id === undefined) {
throw new Error("Tool call ID must be defined for tool messages");
}
return new ToolMessage(storedMessage.data);
case "generic": {
if (storedMessage.data.role === undefined) {
throw new Error("Role must be defined for chat messages");
}
return new ChatMessage(storedMessage.data);
}
default:
throw new Error(`Got unexpected type: ${storedMessage.type}`);
}
}
/**
* Transforms an array of `StoredMessage` instances into an array of
* `BaseMessage` instances. It uses the `mapV1MessageToStoredMessage`
* function to ensure all messages are in the `StoredMessage` format, then
* creates new instances of the appropriate `BaseMessage` subclass based
* on the type of each message. This function is used to prepare stored
* messages for use in a chat context.
*/
export function mapStoredMessagesToChatMessages(messages) {
return messages.map(mapStoredMessageToChatMessage);
}
/**
* Transforms an array of `BaseMessage` instances into an array of
* `StoredMessage` instances. It does this by calling the `toDict` method
* on each `BaseMessage`, which returns a `StoredMessage`. This function
* is used to prepare chat messages for storage.
*/
export function mapChatMessagesToStoredMessages(messages) {
return messages.map((message) => message.toDict());
}
export function convertToChunk(message) {
const type = message._getType();
if (type === "human") {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return new HumanMessageChunk({ ...message });
}
else if (type === "ai") {
let aiChunkFields = {
...message,
};
if ("tool_calls" in aiChunkFields) {
aiChunkFields = {
...aiChunkFields,
tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({
...tc,
type: "tool_call_chunk",
index: undefined,
args: JSON.stringify(tc.args),
})),
};
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return new AIMessageChunk({ ...aiChunkFields });
}
else if (type === "system") {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return new SystemMessageChunk({ ...message });
}
else if (type === "function") {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return new FunctionMessageChunk({ ...message });
// eslint-disable-next-line @typescript-eslint/no-use-before-define
}
else if (ChatMessage.isInstance(message)) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return new ChatMessageChunk({ ...message });
}
else {
throw new Error("Unknown message type.");
}
}