@assistant-ui/react-langchain 0.0.27 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/convertMessages.d.ts +2 -76
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +20 -153
- package/dist/convertMessages.js.map +1 -1
- package/dist/converter.d.ts +180 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +213 -0
- package/dist/converter.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/streamingTiming.d.ts +1 -2
- package/dist/streamingTiming.d.ts.map +1 -1
- package/dist/streamingTiming.js +2 -36
- package/dist/streamingTiming.js.map +1 -1
- package/dist/types.d.ts +2 -55
- package/dist/types.d.ts.map +1 -1
- package/dist/useStreamRuntime.d.ts +2 -8
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +23 -44
- package/dist/useStreamRuntime.js.map +1 -1
- package/package.json +24 -10
- package/src/convertMessages.test.ts +217 -1
- package/src/convertMessages.ts +43 -213
- package/src/converter.ts +372 -0
- package/src/streamingTiming.test.tsx +37 -0
- package/src/streamingTiming.ts +5 -68
- package/src/types.ts +1 -39
- package/src/useStreamRuntime.test.tsx +74 -1
- package/src/useStreamRuntime.ts +47 -54
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { parseDataUrl, resolveFilePartSource } from "@assistant-ui/core/internal";
|
|
2
|
+
//#region src/converter.ts
|
|
3
|
+
const convertLangChainContentBlock = (part) => {
|
|
4
|
+
switch (part.type) {
|
|
5
|
+
case "text":
|
|
6
|
+
case "text_delta": return {
|
|
7
|
+
type: "text",
|
|
8
|
+
text: part.text
|
|
9
|
+
};
|
|
10
|
+
case "image_url": {
|
|
11
|
+
const image = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
12
|
+
if (!image) return null;
|
|
13
|
+
return {
|
|
14
|
+
type: "image",
|
|
15
|
+
image
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
case "file": return {
|
|
19
|
+
type: "file",
|
|
20
|
+
filename: part.metadata?.filename ?? "file",
|
|
21
|
+
data: part.source_type === "url" ? part.url : part.source_type === "id" ? part.id : part.data,
|
|
22
|
+
mimeType: part.mime_type ?? "application/octet-stream",
|
|
23
|
+
...(part.source_type === "url" || part.source_type === "id") && { sourceType: part.source_type }
|
|
24
|
+
};
|
|
25
|
+
case "audio": {
|
|
26
|
+
const mimeType = part.mime_type ?? "application/octet-stream";
|
|
27
|
+
const subtype = mimeType.startsWith("audio/") ? mimeType.slice(6) : void 0;
|
|
28
|
+
return {
|
|
29
|
+
type: "file",
|
|
30
|
+
filename: subtype ? `audio.${subtype}` : "audio",
|
|
31
|
+
data: part.data,
|
|
32
|
+
mimeType
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
case "thinking": return {
|
|
36
|
+
type: "reasoning",
|
|
37
|
+
text: part.thinking
|
|
38
|
+
};
|
|
39
|
+
case "reasoning": return {
|
|
40
|
+
type: "reasoning",
|
|
41
|
+
text: part.summary && part.summary.length > 0 ? part.summary.map((s) => s?.text ?? "").join("\n\n\n") : part.reasoning ?? ""
|
|
42
|
+
};
|
|
43
|
+
case "tool_use":
|
|
44
|
+
case "input_json_delta": return null;
|
|
45
|
+
default: return;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const hasVisibleText = (text) => typeof text === "string" && text.trim() !== "";
|
|
49
|
+
/**
|
|
50
|
+
* Audio output arrives outside the content array: providers leave `content`
|
|
51
|
+
* empty and put the spoken text in `additional_kwargs.audio.transcript`. The
|
|
52
|
+
* audio bytes stay behind because no provider reports their media type, and a
|
|
53
|
+
* streamed response carries raw PCM rather than a playable file.
|
|
54
|
+
*/
|
|
55
|
+
const withAudioTranscript = (parts, additionalKwargs) => {
|
|
56
|
+
const transcript = (additionalKwargs?.audio)?.transcript;
|
|
57
|
+
if (typeof transcript !== "string" || !hasVisibleText(transcript)) return parts;
|
|
58
|
+
if (parts.some((part) => part.type === "text" && hasVisibleText(part.text))) return parts;
|
|
59
|
+
return [...parts.filter((part) => part.type !== "text"), {
|
|
60
|
+
type: "text",
|
|
61
|
+
text: transcript
|
|
62
|
+
}];
|
|
63
|
+
};
|
|
64
|
+
const getCustomMetadata = (additionalKwargs) => additionalKwargs?.metadata ?? {};
|
|
65
|
+
const uiMessageToDataPart = (ui) => ({
|
|
66
|
+
type: "data",
|
|
67
|
+
name: ui.name,
|
|
68
|
+
data: ui.props
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Audio media types that reach a provider's audio input through the LangChain
|
|
72
|
+
* `audio` block. langchain-core derives OpenAI's `input_audio.format` by
|
|
73
|
+
* splitting `mime_type` on `/`, and that format is a wav-or-mp3 enum, so
|
|
74
|
+
* `audio/mpeg` passes the converter and is rejected at the provider.
|
|
75
|
+
*/
|
|
76
|
+
const audioBlockMimeTypes = /* @__PURE__ */ new Map([
|
|
77
|
+
["audio/mp3", "audio/mp3"],
|
|
78
|
+
["audio/mpeg", "audio/mp3"],
|
|
79
|
+
["audio/wav", "audio/wav"],
|
|
80
|
+
["audio/wave", "audio/wav"],
|
|
81
|
+
["audio/x-wav", "audio/wav"]
|
|
82
|
+
]);
|
|
83
|
+
const getMessageContent = (msg) => {
|
|
84
|
+
const allContent = [...msg.content, ...msg.attachments?.flatMap((a) => a.content) ?? []];
|
|
85
|
+
const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image" || part.type === "audio");
|
|
86
|
+
const hasText = allContent.some((part) => part.type === "text");
|
|
87
|
+
if (hasNonText && !hasText) allContent.unshift({
|
|
88
|
+
type: "text",
|
|
89
|
+
text: " "
|
|
90
|
+
});
|
|
91
|
+
const content = allContent.flatMap((part) => {
|
|
92
|
+
const type = part.type;
|
|
93
|
+
switch (type) {
|
|
94
|
+
case "text": return {
|
|
95
|
+
type: "text",
|
|
96
|
+
text: part.text
|
|
97
|
+
};
|
|
98
|
+
case "image": return {
|
|
99
|
+
type: "image_url",
|
|
100
|
+
image_url: { url: part.image }
|
|
101
|
+
};
|
|
102
|
+
case "file": {
|
|
103
|
+
const metadata = { filename: part.filename ?? "file" };
|
|
104
|
+
if (part.sourceType === "id") return {
|
|
105
|
+
type: "file",
|
|
106
|
+
id: part.data,
|
|
107
|
+
mime_type: part.mimeType,
|
|
108
|
+
filename: metadata.filename,
|
|
109
|
+
metadata,
|
|
110
|
+
source_type: "id"
|
|
111
|
+
};
|
|
112
|
+
const source = resolveFilePartSource(part);
|
|
113
|
+
if (source.kind === "url") return {
|
|
114
|
+
type: "file",
|
|
115
|
+
url: source.url,
|
|
116
|
+
mime_type: part.mimeType,
|
|
117
|
+
filename: metadata.filename,
|
|
118
|
+
metadata,
|
|
119
|
+
source_type: "url"
|
|
120
|
+
};
|
|
121
|
+
const audioMimeType = audioBlockMimeTypes.get(source.mimeType.toLowerCase());
|
|
122
|
+
if (audioMimeType) return {
|
|
123
|
+
type: "audio",
|
|
124
|
+
data: source.data,
|
|
125
|
+
mime_type: audioMimeType,
|
|
126
|
+
source_type: "base64"
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
type: "file",
|
|
130
|
+
data: source.data,
|
|
131
|
+
mime_type: source.mimeType,
|
|
132
|
+
filename: metadata.filename,
|
|
133
|
+
metadata,
|
|
134
|
+
source_type: "base64"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "audio": return {
|
|
138
|
+
type: "audio",
|
|
139
|
+
data: parseDataUrl(part.audio.data)?.data ?? part.audio.data,
|
|
140
|
+
mime_type: `audio/${part.audio.format}`,
|
|
141
|
+
source_type: "base64"
|
|
142
|
+
};
|
|
143
|
+
case "data": return [];
|
|
144
|
+
case "tool-call": throw new Error("Tool call appends are not supported.");
|
|
145
|
+
default: throw new Error(`Unsupported append message part type: ${type}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (content.length === 1 && content[0]?.type === "text") return content[0].text ?? "";
|
|
149
|
+
return content;
|
|
150
|
+
};
|
|
151
|
+
const reasoningTextLength = (part) => {
|
|
152
|
+
if (part.summary && part.summary.length > 0) return part.summary.map((s) => s?.text ?? "").join("\n\n\n").length;
|
|
153
|
+
return part.reasoning?.length ?? 0;
|
|
154
|
+
};
|
|
155
|
+
const createLangChainStreamingTimingAccessors = (getType) => {
|
|
156
|
+
const findAiMessage = (messages, messageId) => messages.find((message) => getType(message) === "ai" && message.id === messageId);
|
|
157
|
+
const getTextLength = (messages, messageId) => {
|
|
158
|
+
const message = findAiMessage(messages, messageId);
|
|
159
|
+
if (!message) return 0;
|
|
160
|
+
const content = message.content;
|
|
161
|
+
if (typeof content === "string") return content.length;
|
|
162
|
+
if (!Array.isArray(content)) return 0;
|
|
163
|
+
let len = 0;
|
|
164
|
+
for (const part of content) switch (part.type) {
|
|
165
|
+
case "text":
|
|
166
|
+
case "text_delta":
|
|
167
|
+
if (typeof part.text === "string") len += part.text.length;
|
|
168
|
+
break;
|
|
169
|
+
case "thinking":
|
|
170
|
+
if (typeof part.thinking === "string") len += part.thinking.length;
|
|
171
|
+
break;
|
|
172
|
+
case "reasoning": len += reasoningTextLength(part);
|
|
173
|
+
}
|
|
174
|
+
return len;
|
|
175
|
+
};
|
|
176
|
+
const getToolCallCount = (messages, messageId) => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;
|
|
177
|
+
const getAssistantMessageId = (messages) => {
|
|
178
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
179
|
+
const message = messages[i];
|
|
180
|
+
if (message && getType(message) === "ai" && message.id) return message.id;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
getAssistantMessageId,
|
|
185
|
+
getTextLength,
|
|
186
|
+
getToolCallCount
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the assistant message a `UIMessage` belongs to: the parent id comes
|
|
191
|
+
* from `metadata.message_id` (Python SDK) or `metadata.id` (JS SDK).
|
|
192
|
+
*/
|
|
193
|
+
const getUIMessageParentId = (ui) => ui.metadata?.message_id ?? ui.metadata?.id;
|
|
194
|
+
/**
|
|
195
|
+
* Group the graph's accumulated `UIMessage`s by the assistant message they
|
|
196
|
+
* belong to. Non-array state and entries without a parent link are dropped.
|
|
197
|
+
*/
|
|
198
|
+
const groupUIMessagesByParent = (value) => {
|
|
199
|
+
const map = /* @__PURE__ */ new Map();
|
|
200
|
+
if (!Array.isArray(value)) return map;
|
|
201
|
+
for (const ui of value) {
|
|
202
|
+
const parentId = getUIMessageParentId(ui);
|
|
203
|
+
if (!parentId) continue;
|
|
204
|
+
const existing = map.get(parentId);
|
|
205
|
+
if (existing) existing.push(ui);
|
|
206
|
+
else map.set(parentId, [ui]);
|
|
207
|
+
}
|
|
208
|
+
return map;
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
export { convertLangChainContentBlock, createLangChainStreamingTimingAccessors, getCustomMetadata, getMessageContent, getUIMessageParentId, groupUIMessagesByParent, uiMessageToDataPart, withAudioTranscript };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=converter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.js","names":["_exhaustiveCheck"],"sources":["../src/converter.ts"],"sourcesContent":["import type {\n AppendMessage,\n DataMessagePart,\n ThreadAssistantMessage,\n ThreadUserMessage,\n} from \"@assistant-ui/core\";\nimport {\n parseDataUrl,\n resolveFilePartSource,\n} from \"@assistant-ui/core/internal\";\nimport type { StreamingTimingAccessors } from \"@assistant-ui/core/react\";\n\n/** Known content block types from @langchain/core messages. */\nexport type LangChainContentBlock =\n | { type: \"text\"; text: string }\n | { type: \"text_delta\"; text: string }\n | { type: \"image_url\"; image_url: string | { url?: string } }\n | { type: \"thinking\"; thinking: string }\n | {\n type: \"reasoning\";\n summary?: Array<{ type: \"summary_text\"; text?: string }>;\n reasoning?: string;\n }\n | {\n type: \"file\";\n data: string;\n mime_type: string;\n source_type?: \"base64\";\n metadata?: { filename?: string };\n }\n | {\n type: \"file\";\n url: string;\n mime_type?: string;\n source_type: \"url\";\n metadata?: { filename?: string };\n }\n | {\n type: \"file\";\n id: string;\n mime_type?: string;\n source_type: \"id\";\n metadata?: { filename?: string };\n }\n | {\n type: \"audio\";\n data: string;\n mime_type: string;\n source_type: \"base64\";\n }\n | { type: \"tool_use\" | \"input_json_delta\" };\n\ntype ConvertedContentPart =\n | ThreadUserMessage[\"content\"][number]\n | ThreadAssistantMessage[\"content\"][number];\n\nexport const convertLangChainContentBlock = (\n part: LangChainContentBlock,\n): ConvertedContentPart | null | undefined => {\n const type = part.type;\n switch (type) {\n case \"text\":\n case \"text_delta\":\n return { type: \"text\" as const, text: part.text };\n case \"image_url\": {\n const image =\n typeof part.image_url === \"string\"\n ? part.image_url\n : part.image_url?.url;\n if (!image) return null;\n return { type: \"image\" as const, image };\n }\n case \"file\":\n return {\n type: \"file\" as const,\n filename: part.metadata?.filename ?? \"file\",\n data:\n part.source_type === \"url\"\n ? part.url\n : part.source_type === \"id\"\n ? part.id\n : part.data,\n mimeType: part.mime_type ?? \"application/octet-stream\",\n ...((part.source_type === \"url\" || part.source_type === \"id\") && {\n sourceType: part.source_type,\n }),\n };\n case \"audio\": {\n const mimeType = part.mime_type ?? \"application/octet-stream\";\n const subtype = mimeType.startsWith(\"audio/\")\n ? mimeType.slice(\"audio/\".length)\n : undefined;\n return {\n type: \"file\" as const,\n filename: subtype ? `audio.${subtype}` : \"audio\",\n data: part.data,\n mimeType,\n };\n }\n case \"thinking\":\n return { type: \"reasoning\" as const, text: part.thinking };\n case \"reasoning\":\n return {\n type: \"reasoning\" as const,\n text:\n part.summary && part.summary.length > 0\n ? part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\")\n : (part.reasoning ?? \"\"),\n };\n case \"tool_use\":\n case \"input_json_delta\":\n return null;\n default:\n return undefined;\n }\n};\n\nconst hasVisibleText = (text: unknown): boolean =>\n typeof text === \"string\" && text.trim() !== \"\";\n\n/**\n * Audio output arrives outside the content array: providers leave `content`\n * empty and put the spoken text in `additional_kwargs.audio.transcript`. The\n * audio bytes stay behind because no provider reports their media type, and a\n * streamed response carries raw PCM rather than a playable file.\n */\nexport const withAudioTranscript = <T extends { type: string; text?: unknown }>(\n parts: readonly T[],\n additionalKwargs: Record<string, unknown> | undefined,\n): readonly (T | { type: \"text\"; text: string })[] => {\n const audio = additionalKwargs?.audio as { transcript?: unknown } | undefined;\n const transcript = audio?.transcript;\n if (typeof transcript !== \"string\" || !hasVisibleText(transcript))\n return parts;\n if (parts.some((part) => part.type === \"text\" && hasVisibleText(part.text)))\n return parts;\n return [\n ...parts.filter((part) => part.type !== \"text\"),\n { type: \"text\" as const, text: transcript },\n ];\n};\n\nexport const getCustomMetadata = (\n additionalKwargs: Record<string, unknown> | undefined,\n): Record<string, unknown> =>\n (additionalKwargs?.metadata as Record<string, unknown>) ?? {};\n\nexport const uiMessageToDataPart = <\n TUIMessage extends { name: string; props: Record<string, unknown> },\n>(\n ui: TUIMessage,\n): DataMessagePart => ({\n type: \"data\",\n name: ui.name,\n data: ui.props,\n});\n\n/**\n * Audio media types that reach a provider's audio input through the LangChain\n * `audio` block. langchain-core derives OpenAI's `input_audio.format` by\n * splitting `mime_type` on `/`, and that format is a wav-or-mp3 enum, so\n * `audio/mpeg` passes the converter and is rejected at the provider.\n */\nconst audioBlockMimeTypes = new Map<string, \"audio/mp3\" | \"audio/wav\">([\n [\"audio/mp3\", \"audio/mp3\"],\n [\"audio/mpeg\", \"audio/mp3\"],\n [\"audio/wav\", \"audio/wav\"],\n [\"audio/wave\", \"audio/wav\"],\n [\"audio/x-wav\", \"audio/wav\"],\n]);\n\nexport const getMessageContent = (msg: AppendMessage) => {\n const allContent = [\n ...msg.content,\n ...(msg.attachments?.flatMap((a) => a.content) ?? []),\n ];\n\n const hasNonText = allContent.some(\n (part) =>\n part.type === \"file\" || part.type === \"image\" || part.type === \"audio\",\n );\n const hasText = allContent.some((part) => part.type === \"text\");\n if (hasNonText && !hasText) {\n allContent.unshift({ type: \"text\", text: \" \" });\n }\n\n const content = allContent.flatMap((part) => {\n const type = part.type;\n switch (type) {\n case \"text\":\n return { type: \"text\" as const, text: part.text };\n case \"image\":\n return { type: \"image_url\" as const, image_url: { url: part.image } };\n case \"file\": {\n const metadata = { filename: part.filename ?? \"file\" };\n if (part.sourceType === \"id\") {\n return {\n type: \"file\" as const,\n id: part.data,\n mime_type: part.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"id\" as const,\n };\n }\n const source = resolveFilePartSource(part);\n if (source.kind === \"url\") {\n return {\n type: \"file\" as const,\n url: source.url,\n mime_type: part.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"url\" as const,\n };\n }\n const audioMimeType = audioBlockMimeTypes.get(\n source.mimeType.toLowerCase(),\n );\n if (audioMimeType) {\n return {\n type: \"audio\" as const,\n data: source.data,\n mime_type: audioMimeType,\n source_type: \"base64\" as const,\n };\n }\n return {\n type: \"file\" as const,\n data: source.data,\n mime_type: source.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"base64\" as const,\n };\n }\n case \"audio\": {\n const parsed = parseDataUrl(part.audio.data);\n return {\n type: \"audio\" as const,\n data: parsed?.data ?? part.audio.data,\n mime_type: `audio/${part.audio.format}`,\n source_type: \"base64\" as const,\n };\n }\n case \"data\":\n return [];\n case \"tool-call\":\n throw new Error(\"Tool call appends are not supported.\");\n default: {\n const _exhaustiveCheck: \"reasoning\" | \"source\" | \"generative-ui\" = type;\n throw new Error(\n `Unsupported append message part type: ${_exhaustiveCheck}`,\n );\n }\n }\n });\n\n if (content.length === 1 && content[0]?.type === \"text\") {\n return content[0].text ?? \"\";\n }\n return content;\n};\n\nconst reasoningTextLength = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): number => {\n if (part.summary && part.summary.length > 0)\n return part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\").length;\n return part.reasoning?.length ?? 0;\n};\n\nexport const createLangChainStreamingTimingAccessors = <\n TMessage extends {\n id?: string | undefined;\n content?: unknown;\n tool_calls?: readonly unknown[] | undefined;\n },\n>(\n getType: (message: TMessage) => string,\n): StreamingTimingAccessors<TMessage> => {\n const findAiMessage = (\n messages: readonly TMessage[],\n messageId: string,\n ): TMessage | undefined =>\n messages.find(\n (message) => getType(message) === \"ai\" && message.id === messageId,\n );\n\n const getTextLength = (\n messages: readonly TMessage[],\n messageId: string,\n ): number => {\n const message = findAiMessage(messages, messageId);\n if (!message) return 0;\n const content = message.content;\n if (typeof content === \"string\") return content.length;\n if (!Array.isArray(content)) return 0;\n let len = 0;\n for (const part of content as readonly LangChainContentBlock[]) {\n switch (part.type) {\n case \"text\":\n case \"text_delta\":\n if (typeof part.text === \"string\") len += part.text.length;\n break;\n case \"thinking\":\n if (typeof part.thinking === \"string\") len += part.thinking.length;\n break;\n case \"reasoning\":\n len += reasoningTextLength(part);\n break;\n }\n }\n return len;\n };\n\n const getToolCallCount = (\n messages: readonly TMessage[],\n messageId: string,\n ): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\n const getAssistantMessageId = (\n messages: readonly TMessage[],\n ): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages[i];\n if (message && getType(message) === \"ai\" && message.id) return message.id;\n }\n return undefined;\n };\n\n return {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\n};\n\n/**\n * Resolve the assistant message a `UIMessage` belongs to: the parent id comes\n * from `metadata.message_id` (Python SDK) or `metadata.id` (JS SDK).\n */\nexport const getUIMessageParentId = (ui: {\n metadata?: { message_id?: string; id?: string } | undefined;\n}): string | undefined => ui.metadata?.message_id ?? ui.metadata?.id;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n */\nexport const groupUIMessagesByParent = <\n T extends {\n metadata?: { message_id?: string; id?: string } | undefined;\n },\n>(\n value: unknown,\n): Map<string, T[]> => {\n const map = new Map<string, T[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as T[]) {\n const parentId = getUIMessageParentId(ui);\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LangChainContentBlock } from "./converter.js";
|
|
2
|
+
import { LangChainBaseMessage, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions } from "./types.js";
|
|
2
3
|
import { convertLangChainBaseMessage } from "./convertMessages.js";
|
|
3
4
|
import { useLangChainError, useLangChainInterruptState, useLangChainInterrupts, useLangChainRespond, useLangChainRespondAll, useLangChainSend, useLangChainSendCommand, useLangChainState, useLangChainStream, useLangChainSubagents, useLangChainSubgraphs, useLangChainSubmit, useLangChainToolCalls } from "./hooks.js";
|
|
4
5
|
import { useStreamRuntime } from "./useStreamRuntime.js";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { LangChainBaseMessage } from "./types.js";
|
|
2
|
-
import { StreamingTimingAccessors } from "@assistant-ui/core/react";
|
|
3
2
|
import { MessageTiming } from "@assistant-ui/core";
|
|
4
3
|
//#region src/streamingTiming.d.ts
|
|
5
|
-
declare const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage>;
|
|
4
|
+
declare const langChainStreamingTimingAccessors: import("@assistant-ui/core").StreamingTimingAccessors<LangChainBaseMessage>;
|
|
6
5
|
/**
|
|
7
6
|
* Tracks per-message streaming timing for LangChain messages. Delegates to
|
|
8
7
|
* the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingTiming.d.ts","names":[],"sources":["../src/streamingTiming.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"streamingTiming.d.ts","names":[],"sources":["../src/streamingTiming.ts"],"mappings":";;;cAQa,gEAAiC,yBAAA;;;;;;;cASjC,8BACX,mBAAmB,wBACnB,uBACC,eAAe"}
|
package/dist/streamingTiming.js
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { createLangChainStreamingTimingAccessors } from "./converter.js";
|
|
2
3
|
import { getMessageType } from "./convertMessages.js";
|
|
3
4
|
import { useStreamingTiming } from "@assistant-ui/core/react";
|
|
4
5
|
//#region src/streamingTiming.ts
|
|
5
|
-
const
|
|
6
|
-
const reasoningTextLength = (part) => {
|
|
7
|
-
if (part.summary && part.summary.length > 0) return part.summary.map((s) => s?.text ?? "").join("\n\n\n").length;
|
|
8
|
-
return part.reasoning?.length ?? 0;
|
|
9
|
-
};
|
|
10
|
-
const getTextLength = (messages, messageId) => {
|
|
11
|
-
const m = findAiMessage(messages, messageId);
|
|
12
|
-
if (!m) return 0;
|
|
13
|
-
const content = m.content;
|
|
14
|
-
if (typeof content === "string") return content.length;
|
|
15
|
-
if (!Array.isArray(content)) return 0;
|
|
16
|
-
let len = 0;
|
|
17
|
-
for (const part of content) switch (part.type) {
|
|
18
|
-
case "text":
|
|
19
|
-
case "text_delta":
|
|
20
|
-
if (typeof part.text === "string") len += part.text.length;
|
|
21
|
-
break;
|
|
22
|
-
case "thinking":
|
|
23
|
-
if (typeof part.thinking === "string") len += part.thinking.length;
|
|
24
|
-
break;
|
|
25
|
-
case "reasoning": len += reasoningTextLength(part);
|
|
26
|
-
}
|
|
27
|
-
return len;
|
|
28
|
-
};
|
|
29
|
-
const getToolCallCount = (messages, messageId) => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;
|
|
30
|
-
const getAssistantMessageId = (messages) => {
|
|
31
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
32
|
-
const m = messages[i];
|
|
33
|
-
if (m && getMessageType(m) === "ai" && m.id) return m.id;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const langChainStreamingTimingAccessors = {
|
|
37
|
-
getAssistantMessageId,
|
|
38
|
-
getTextLength,
|
|
39
|
-
getToolCallCount
|
|
40
|
-
};
|
|
6
|
+
const langChainStreamingTimingAccessors = createLangChainStreamingTimingAccessors(getMessageType);
|
|
41
7
|
/**
|
|
42
8
|
* Tracks per-message streaming timing for LangChain messages. Delegates to
|
|
43
9
|
* the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport {
|
|
1
|
+
{"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport { useStreamingTiming } from \"@assistant-ui/core/react\";\nimport { createLangChainStreamingTimingAccessors } from \"./converter\";\nimport { getMessageType } from \"./convertMessages\";\nimport type { LangChainBaseMessage } from \"./types\";\n\nexport const langChainStreamingTimingAccessors =\n createLangChainStreamingTimingAccessors<LangChainBaseMessage>(getMessageType);\n\n/**\n * Tracks per-message streaming timing for LangChain messages. Delegates to\n * the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,\n * adapted to the `LangChainBaseMessage` shape (`_getType() -> \"ai\"`, content\n * blocks including text/thinking/reasoning, `tool_calls`).\n */\nexport const useLangChainStreamingTiming = (\n messages: readonly LangChainBaseMessage[],\n isRunning: boolean,\n): Record<string, MessageTiming> =>\n useStreamingTiming(messages, isRunning, langChainStreamingTimingAccessors);\n"],"mappings":";;;;;AAQA,MAAa,oCACX,wCAA8D,cAAc;;;;;;;AAQ9E,MAAa,+BACX,UACA,cAEA,mBAAmB,UAAU,WAAW,iCAAiC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,61 +1,8 @@
|
|
|
1
|
+
import { LangChainContentBlock } from "./converter.js";
|
|
1
2
|
import { AttachmentAdapter, DictationAdapter, ExternalStoreSharedOptions, FeedbackAdapter, MessageStatus, RealtimeVoiceAdapter, RemoteThreadListAdapter, SpeechSynthesisAdapter } from "@assistant-ui/core";
|
|
2
3
|
import { AnyStream, AssembledToolCall, SubagentDiscoverySnapshot, SubgraphDiscoverySnapshot, UseStreamOptions } from "@langchain/react";
|
|
3
4
|
import { AssistantCloud } from "assistant-cloud";
|
|
4
5
|
//#region src/types.d.ts
|
|
5
|
-
/** Known content block types from @langchain/core messages. */
|
|
6
|
-
type LangChainContentBlock = {
|
|
7
|
-
type: "text";
|
|
8
|
-
text: string;
|
|
9
|
-
} | {
|
|
10
|
-
type: "text_delta";
|
|
11
|
-
text: string;
|
|
12
|
-
} | {
|
|
13
|
-
type: "image_url";
|
|
14
|
-
image_url: string | {
|
|
15
|
-
url?: string;
|
|
16
|
-
};
|
|
17
|
-
} | {
|
|
18
|
-
type: "thinking";
|
|
19
|
-
thinking: string;
|
|
20
|
-
} | {
|
|
21
|
-
type: "reasoning";
|
|
22
|
-
summary?: Array<{
|
|
23
|
-
type: "summary_text";
|
|
24
|
-
text?: string;
|
|
25
|
-
}>;
|
|
26
|
-
reasoning?: string;
|
|
27
|
-
} | {
|
|
28
|
-
type: "file";
|
|
29
|
-
data: string;
|
|
30
|
-
mime_type: string;
|
|
31
|
-
source_type?: "base64";
|
|
32
|
-
metadata?: {
|
|
33
|
-
filename?: string;
|
|
34
|
-
};
|
|
35
|
-
} | {
|
|
36
|
-
type: "file";
|
|
37
|
-
url: string;
|
|
38
|
-
mime_type?: string;
|
|
39
|
-
source_type: "url";
|
|
40
|
-
metadata?: {
|
|
41
|
-
filename?: string;
|
|
42
|
-
};
|
|
43
|
-
} | {
|
|
44
|
-
type: "file";
|
|
45
|
-
id: string;
|
|
46
|
-
mime_type?: string;
|
|
47
|
-
source_type: "id";
|
|
48
|
-
metadata?: {
|
|
49
|
-
filename?: string;
|
|
50
|
-
};
|
|
51
|
-
} | {
|
|
52
|
-
type: "audio";
|
|
53
|
-
data: string;
|
|
54
|
-
mime_type: string;
|
|
55
|
-
source_type: "base64";
|
|
56
|
-
} | {
|
|
57
|
-
type: "tool_use" | "input_json_delta";
|
|
58
|
-
};
|
|
59
6
|
type LangChainToolCall = {
|
|
60
7
|
id: string;
|
|
61
8
|
name: string;
|
|
@@ -183,5 +130,5 @@ type LangChainRuntimeExtraOptions = ExternalStoreSharedOptions & {
|
|
|
183
130
|
};
|
|
184
131
|
type UseStreamRuntimeOptions = UseStreamOptions extends (infer O) ? O extends UseStreamOptions ? O & LangChainRuntimeExtraOptions : never : never;
|
|
185
132
|
//#endregion
|
|
186
|
-
export { LangChainBaseMessage, LangChainContentBlock, LangChainRuntimeExtraOptions, LangChainRuntimeExtras, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions };
|
|
133
|
+
export { LangChainBaseMessage, type LangChainContentBlock, LangChainRuntimeExtraOptions, LangChainRuntimeExtras, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions };
|
|
187
134
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAqBY;EACV;EACA;EACA,MAAM;;;;;;;;;KAUI,UACV,+BACA,eAAe,0BAA0B;EAEzC;EACA;EACA,MAAM;EACN,OAAO;EACP;IACE;IACA;IACA;IACA;IACA;IACA;KACC;;;;;;;KAQO;EACV;EACA;;;;;;;;;KAUU;EACV;EACA;EACA;EACA;EACA,oBAAoB;;EAEpB,sBAAsB;;EAEtB;;EAEA,SAAS;;EAET;;KAGU;EACV;IAAa;;EACb;IAAuB;IAAa;;EACpC,oBAAoB;EACpB,WAAW,oBAAoB;EAC/B,WAAW,oBAAoB;EAC/B,QAAQ;EACR;EACA,SACE,QAAQ,4CACR,UAAU,4BACP;EACL,UACE,mBACA,UAAU,4BACP;EACL,aACE,eAAe,yBACf,UAAU,4BACP;EACL,QAAQ;EACR;;KAGU,+BAA+B;;;;;;;;;EASzC,qBAAqB;EACrB,QAAQ;EACR;IAEM,cAAc;IACd,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,WAAW;;;;;;;;EASjB;;;;;EAKA;;;;;EAKA,6BAA6B;;EAE7B,gBAAgB;IAAU;;;EAE1B,WAAW,qBAAqB;;;;;;;EAOhC;;KAOU,0BAA0B,gCAA+B,KACjE,UAAU,mBACR,IAAI"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="@assistant-ui/core/store" />
|
|
2
|
-
import {
|
|
2
|
+
import { groupUIMessagesByParent } from "./converter.js";
|
|
3
|
+
import { UseStreamRuntimeOptions } from "./types.js";
|
|
3
4
|
import { AppendMessage } from "@assistant-ui/core";
|
|
4
5
|
//#region src/useStreamRuntime.d.ts
|
|
5
6
|
declare const runConfigToSubmitOptions: (runConfig: AppendMessage["runConfig"]) => {
|
|
@@ -7,13 +8,6 @@ declare const runConfigToSubmitOptions: (runConfig: AppendMessage["runConfig"])
|
|
|
7
8
|
configurable: Record<string, unknown>;
|
|
8
9
|
};
|
|
9
10
|
} | undefined;
|
|
10
|
-
/**
|
|
11
|
-
* Group the graph's accumulated `UIMessage`s by the assistant message they
|
|
12
|
-
* belong to. Non-array state and entries without a parent link are dropped.
|
|
13
|
-
* The parent id comes from `metadata.message_id` (Python SDK) or
|
|
14
|
-
* `metadata.id` (JS SDK).
|
|
15
|
-
*/
|
|
16
|
-
declare const groupUIMessagesByParent: (value: unknown) => Map<string, UIMessage[]>;
|
|
17
11
|
/**
|
|
18
12
|
* Creates an assistant-ui runtime backed by LangChain's `useStream` hook.
|
|
19
13
|
* Accepts the same options as `useStream` from `@langchain/react`, plus
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;;cAoDa,2BACX,WAAW;;kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqjBA,mBAAoB,YAAY,yDAAuB"}
|
package/dist/useStreamRuntime.js
CHANGED
|
@@ -1,44 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { getMessageContent, groupUIMessagesByParent } from "./converter.js";
|
|
3
|
+
import { convertLangChainBaseMessage, getMessageType } from "./convertMessages.js";
|
|
3
4
|
import { langChainExtras } from "./runtimeExtras.js";
|
|
4
5
|
import { foldUIUpdates, mergeUIMessages } from "./uiMessages.js";
|
|
5
6
|
import { resolveForkCheckpoint } from "./resolveForkCheckpoint.js";
|
|
6
7
|
import { useLangChainStreamingTiming } from "./streamingTiming.js";
|
|
8
|
+
import { createCloudThreadListAdapterCreateFallback, createToolCallCancellationStub, scanPendingToolCalls } from "@assistant-ui/core/internal";
|
|
7
9
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
8
10
|
import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
9
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react";
|
|
10
12
|
import { generateId, getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
11
13
|
import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
|
|
12
14
|
//#region src/useStreamRuntime.ts
|
|
13
15
|
const UI_CUSTOM_CHANNELS = ["custom"];
|
|
14
16
|
const runConfigToSubmitOptions = (runConfig) => runConfig?.custom ? { config: { configurable: runConfig.custom } } : void 0;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
const groupUIMessagesByParent = (value) => {
|
|
22
|
-
const map = /* @__PURE__ */ new Map();
|
|
23
|
-
if (!Array.isArray(value)) return map;
|
|
24
|
-
for (const ui of value) {
|
|
25
|
-
const parentId = ui.metadata?.message_id ?? ui.metadata?.id;
|
|
26
|
-
if (!parentId) continue;
|
|
27
|
-
const existing = map.get(parentId);
|
|
28
|
-
if (existing) existing.push(ui);
|
|
29
|
-
else map.set(parentId, [ui]);
|
|
30
|
-
}
|
|
31
|
-
return map;
|
|
32
|
-
};
|
|
33
|
-
const getPendingToolCalls = (messages) => {
|
|
34
|
-
const pending = /* @__PURE__ */ new Map();
|
|
35
|
-
for (const m of messages) {
|
|
36
|
-
const type = getMessageType(m);
|
|
37
|
-
if (type === "ai") for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);
|
|
38
|
-
else if (type === "tool" && m.tool_call_id) pending.delete(m.tool_call_id);
|
|
39
|
-
}
|
|
40
|
-
return [...pending.values()];
|
|
41
|
-
};
|
|
17
|
+
const getPendingToolCalls = (messages) => scanPendingToolCalls(messages, (message) => {
|
|
18
|
+
const type = getMessageType(message);
|
|
19
|
+
if (type === "ai") return { toolCalls: message.tool_calls ?? [] };
|
|
20
|
+
if (type === "tool" && message.tool_call_id) return { toolCallId: message.tool_call_id };
|
|
21
|
+
}, (toolCall) => toolCall.id);
|
|
42
22
|
const toStagedHumanMessage = (msg, id = generateId()) => ({
|
|
43
23
|
id,
|
|
44
24
|
_getType: () => "human",
|
|
@@ -89,7 +69,9 @@ const useStreamThreadRuntime = (options) => {
|
|
|
89
69
|
isRunning: effectiveIsRunning
|
|
90
70
|
});
|
|
91
71
|
const streamRef = useRef(stream);
|
|
92
|
-
|
|
72
|
+
useInsertionEffect(() => {
|
|
73
|
+
streamRef.current = stream;
|
|
74
|
+
}, [stream]);
|
|
93
75
|
const activeRunConfigRef = useRef(void 0);
|
|
94
76
|
const runConfigByMessageIdRef = useRef(/* @__PURE__ */ new Map());
|
|
95
77
|
const activeThreadIdRef = useRef(externalId);
|
|
@@ -126,9 +108,13 @@ const useStreamThreadRuntime = (options) => {
|
|
|
126
108
|
}
|
|
127
109
|
}, [stream.messages]);
|
|
128
110
|
const visibleMessagesRef = useRef(visibleMessages);
|
|
129
|
-
|
|
111
|
+
useInsertionEffect(() => {
|
|
112
|
+
visibleMessagesRef.current = visibleMessages;
|
|
113
|
+
}, [visibleMessages]);
|
|
130
114
|
const threadMessagesRef = useRef(threadMessages);
|
|
131
|
-
|
|
115
|
+
useInsertionEffect(() => {
|
|
116
|
+
threadMessagesRef.current = threadMessages;
|
|
117
|
+
}, [threadMessages]);
|
|
132
118
|
const stagedMessagesRef = useRef(/* @__PURE__ */ new Map());
|
|
133
119
|
const stagedBaseMessagesRef = useRef(null);
|
|
134
120
|
useEffect(() => {
|
|
@@ -218,7 +204,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
218
204
|
]);
|
|
219
205
|
return useExternalStoreRuntime({
|
|
220
206
|
...pickExternalStoreSharedOptions(options),
|
|
221
|
-
isRunning:
|
|
207
|
+
isRunning: stream.isLoading,
|
|
222
208
|
isLoading: stream.isThreadLoading,
|
|
223
209
|
messages: threadMessages,
|
|
224
210
|
adapters,
|
|
@@ -233,13 +219,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
233
219
|
const stagedMessageId = stageUserMessage(msg, true).id;
|
|
234
220
|
setActiveRunConfig(msg.runConfig);
|
|
235
221
|
const content = getMessageContent(msg);
|
|
236
|
-
const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map(
|
|
237
|
-
type: "tool",
|
|
238
|
-
name: t.name,
|
|
239
|
-
tool_call_id: t.id,
|
|
240
|
-
content: JSON.stringify({ cancelled: true }),
|
|
241
|
-
status: "error"
|
|
242
|
-
})) : [];
|
|
222
|
+
const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map(createToolCallCancellationStub) : [];
|
|
243
223
|
try {
|
|
244
224
|
const { externalId } = await aui.threadListItem.initialize();
|
|
245
225
|
await streamRef.current.submit({ [messagesKey]: [...cancellations, {
|
|
@@ -364,16 +344,15 @@ const useStreamThreadRuntime = (options) => {
|
|
|
364
344
|
*/
|
|
365
345
|
const useStreamRuntime = (rawOptions) => {
|
|
366
346
|
const { cloud, unstable_threadListAdapter, create, delete: deleteFn, onThreadIdChange, ...options } = rawOptions;
|
|
367
|
-
const
|
|
368
|
-
optionsRef.current = options;
|
|
347
|
+
const aui = useAui();
|
|
369
348
|
const cloudAdapter = useCloudThreadListAdapter({
|
|
370
349
|
cloud,
|
|
371
|
-
create,
|
|
350
|
+
create: createCloudThreadListAdapterCreateFallback(create, aui.threadListItem),
|
|
372
351
|
delete: deleteFn
|
|
373
352
|
});
|
|
374
353
|
return useRemoteThreadListRuntime({
|
|
375
354
|
runtimeHook: function RuntimeHook() {
|
|
376
|
-
return useStreamThreadRuntime(
|
|
355
|
+
return useStreamThreadRuntime(options);
|
|
377
356
|
},
|
|
378
357
|
adapter: unstable_threadListAdapter ?? cloudAdapter,
|
|
379
358
|
allowNesting: true,
|