@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
157 lines (156 loc) • 5.54 kB
JavaScript
import { AIMessage, AIMessageChunk, HumanMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
//#region src/stream/assembled-to-message.ts
/**
* Convert the protocol-native {@link AssembledMessage} (a namespace +
* content-block bag) into a class instance from
* `@langchain/core/messages`.
*
* The v2 messages channel carries `role` on the `message-start`
* event, but `MessageAssembler` drops it by the time it produces an
* `AssembledMessage`. The selector-side caller therefore captures the
* role separately (via the assembler's per-update hook, see
* `projections/messages.ts`) and passes it in.
*
* The conversion is intentionally lossless in the limit: when the
* message finishes we emit the same BaseMessage shape that a
* `values.messages` entry would round-trip to via
* {@link tryCoerceMessageLikeToMessage}. Mid-stream, partial blocks
* (e.g. tool_call_chunk) are folded into the same shape so the UI
* can render an incrementally-completing message.
*/
/**
* Produce a `BaseMessage` class instance from an in-progress or
* finished assembled message. Safe to call repeatedly across deltas —
* each call returns a new instance whose content reflects the
* currently-observed blocks.
*/
function assembledToBaseMessage(input) {
const { id, role, blocks, namespace, node, runId, toolCallId, metadata, usage, finishMetadata, finishReason } = input;
const textContent = extractContentString(blocks);
const toolCalls = extractToolCalls(blocks);
const toolCallChunks = extractToolCallChunks(blocks);
const additionalKwargs = {
...namespace != null ? { namespace } : {},
...node != null ? { node } : {},
...runId != null ? { run_id: runId } : {},
...metadata != null ? { metadata } : {},
...usage != null ? { usage } : {}
};
const hasAdditionalKwargs = Object.keys(additionalKwargs).length > 0;
const responseMetadata = {
...finishReason != null ? { finish_reason: finishReason } : {},
...finishMetadata
};
switch (role) {
case "human": return new HumanMessage({
...id != null ? { id } : {},
content: textContent,
...hasAdditionalKwargs ? { additional_kwargs: additionalKwargs } : {},
...Object.keys(responseMetadata).length > 0 ? { response_metadata: responseMetadata } : {}
});
case "system": return new SystemMessage({
...id != null ? { id } : {},
content: textContent,
...hasAdditionalKwargs ? { additional_kwargs: additionalKwargs } : {},
...Object.keys(responseMetadata).length > 0 ? { response_metadata: responseMetadata } : {}
});
case "tool": return new ToolMessage({
...id != null ? { id } : {},
content: textContent,
tool_call_id: toolCallId ?? "",
...hasAdditionalKwargs ? { additional_kwargs: additionalKwargs } : {},
...Object.keys(responseMetadata).length > 0 ? { response_metadata: responseMetadata } : {}
});
default: {
const payload = {
...id != null ? { id } : {},
content: cloneContentBlocks(blocks),
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
...toolCallChunks.length > 0 ? { tool_call_chunks: toolCallChunks } : {},
...hasAdditionalKwargs ? { additional_kwargs: additionalKwargs } : {},
usage_metadata: normalizeUsage(usage),
response_metadata: {
...responseMetadata,
output_version: "v1"
}
};
return toolCallChunks.length > 0 ? new AIMessageChunk(payload) : new AIMessage(payload);
}
}
}
/**
* Convenience: given the raw assembled message + the role captured
* from `message-start`, produce a `BaseMessage` with the same id.
*/
function assembledMessageToBaseMessage(assembled, role, extras = {}) {
return assembledToBaseMessage({
id: assembled.id,
role,
blocks: assembled.blocks,
namespace: assembled.namespace,
node: assembled.node,
runId: assembled.runId,
toolCallId: extras.toolCallId,
metadata: assembled.metadata,
usage: assembled.usage,
finishMetadata: assembled.finishMetadata,
finishReason: assembled.finishReason
});
}
function normalizeUsage(usage) {
if (usage == null) return void 0;
return {
...usage,
input_tokens: usage.input_tokens ?? 0,
output_tokens: usage.output_tokens ?? 0,
total_tokens: usage.total_tokens ?? 0
};
}
function extractContentString(blocks) {
let out = "";
for (const block of blocks) if (block.type === "text" && typeof block.text === "string") out += block.text;
return out;
}
function cloneContentBlocks(blocks) {
return blocks.map((block) => structuredClone(block));
}
function extractToolCalls(blocks) {
const out = [];
for (const block of blocks) {
if (block.type !== "tool_call" && block.type !== "tool_use") continue;
const tc = block;
out.push({
id: tc.id ?? "",
name: tc.name ?? "",
args: normalizeToolCallArgs(tc.args ?? tc.input),
type: "tool_call"
});
}
return out;
}
function extractToolCallChunks(blocks) {
const out = [];
for (const block of blocks) {
if (block.type !== "tool_call_chunk") continue;
const tc = block;
out.push({
id: tc.id,
name: tc.name,
args: tc.args,
index: tc.index,
type: "tool_call_chunk"
});
}
return out;
}
function normalizeToolCallArgs(value) {
if (value != null && typeof value === "object" && !Array.isArray(value)) return value;
if (typeof value === "string" && value.length > 0) try {
const parsed = JSON.parse(value);
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
} catch {}
return {};
}
//#endregion
export { assembledMessageToBaseMessage, assembledToBaseMessage };
//# sourceMappingURL=assembled-to-message.js.map