@assistant-ui/react-langchain 0.0.22 → 0.0.24
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/dist/convertMessages.d.ts +26 -3
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +58 -13
- package/dist/convertMessages.js.map +1 -1
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +1 -2
- package/dist/useStreamRuntime.js.map +1 -1
- package/package.json +5 -5
- package/src/convertMessages.test.ts +432 -5
- package/src/convertMessages.ts +80 -14
- package/src/useStreamRuntime.test.tsx +79 -1
- package/src/useStreamRuntime.ts +5 -6
|
@@ -12,10 +12,12 @@ declare const getMessageContent: (msg: AppendMessage) => string | ({
|
|
|
12
12
|
type: "text";
|
|
13
13
|
text: string;
|
|
14
14
|
image_url?: never;
|
|
15
|
-
|
|
15
|
+
id?: never;
|
|
16
16
|
mime_type?: never;
|
|
17
|
+
filename?: never;
|
|
17
18
|
metadata?: never;
|
|
18
19
|
source_type?: never;
|
|
20
|
+
url?: never;
|
|
19
21
|
data?: never;
|
|
20
22
|
} | {
|
|
21
23
|
type: "image_url";
|
|
@@ -23,32 +25,51 @@ declare const getMessageContent: (msg: AppendMessage) => string | ({
|
|
|
23
25
|
url: string;
|
|
24
26
|
};
|
|
25
27
|
text?: never;
|
|
26
|
-
|
|
28
|
+
id?: never;
|
|
27
29
|
mime_type?: never;
|
|
30
|
+
filename?: never;
|
|
28
31
|
metadata?: never;
|
|
29
32
|
source_type?: never;
|
|
33
|
+
url?: never;
|
|
34
|
+
data?: never;
|
|
35
|
+
} | {
|
|
36
|
+
type: "file";
|
|
37
|
+
id: string;
|
|
38
|
+
mime_type: string;
|
|
39
|
+
filename: string;
|
|
40
|
+
metadata: {
|
|
41
|
+
filename: string;
|
|
42
|
+
};
|
|
43
|
+
source_type: "id";
|
|
44
|
+
text?: never;
|
|
45
|
+
image_url?: never;
|
|
46
|
+
url?: never;
|
|
30
47
|
data?: never;
|
|
31
48
|
} | {
|
|
32
49
|
type: "file";
|
|
33
50
|
url: string;
|
|
34
51
|
mime_type: string;
|
|
52
|
+
filename: string;
|
|
35
53
|
metadata: {
|
|
36
54
|
filename: string;
|
|
37
55
|
};
|
|
38
56
|
source_type: "url";
|
|
39
57
|
text?: never;
|
|
40
58
|
image_url?: never;
|
|
59
|
+
id?: never;
|
|
41
60
|
data?: never;
|
|
42
61
|
} | {
|
|
43
62
|
type: "file";
|
|
44
63
|
data: string;
|
|
45
64
|
mime_type: string;
|
|
65
|
+
filename: string;
|
|
46
66
|
metadata: {
|
|
47
67
|
filename: string;
|
|
48
68
|
};
|
|
49
69
|
source_type: "base64";
|
|
50
70
|
text?: never;
|
|
51
71
|
image_url?: never;
|
|
72
|
+
id?: never;
|
|
52
73
|
url?: never;
|
|
53
74
|
} | {
|
|
54
75
|
type: "audio";
|
|
@@ -57,8 +78,10 @@ declare const getMessageContent: (msg: AppendMessage) => string | ({
|
|
|
57
78
|
source_type: "base64";
|
|
58
79
|
text?: never;
|
|
59
80
|
image_url?: never;
|
|
60
|
-
|
|
81
|
+
id?: never;
|
|
82
|
+
filename?: never;
|
|
61
83
|
metadata?: never;
|
|
84
|
+
url?: never;
|
|
62
85
|
})[];
|
|
63
86
|
//#endregion
|
|
64
87
|
export { convertLangChainBaseMessage, getMessageContent, getMessageType };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convertMessages.d.ts","names":[],"sources":["../src/convertMessages.ts"],"mappings":";;;;KAgBK,oCACH,4BAA4B;EAC1B,qBAAqB,YAAY;EACjC,gBAAgB,eAAe;;cAStB,iBAAkB,SAAS;
|
|
1
|
+
{"version":3,"file":"convertMessages.d.ts","names":[],"sources":["../src/convertMessages.ts"],"mappings":";;;;KAgBK,oCACH,4BAA4B;EAC1B,qBAAqB,YAAY;EACjC,gBAAgB,eAAe;;cAStB,iBAAkB,SAAS;cAiH3B,8BACX,SAAS,sBACT,WAAU,sCACT,4BAA4B;cA0GlB,oBAAqB,KAAK"}
|
package/dist/convertMessages.js
CHANGED
|
@@ -11,7 +11,7 @@ const getMessageType = (message) => {
|
|
|
11
11
|
if ("type" in message) return message.type;
|
|
12
12
|
throw new Error("Cannot determine message type");
|
|
13
13
|
};
|
|
14
|
-
const contentToParts = (content
|
|
14
|
+
const contentToParts = (content) => {
|
|
15
15
|
if (typeof content === "string") return [{
|
|
16
16
|
type: "text",
|
|
17
17
|
text: content
|
|
@@ -35,18 +35,17 @@ const contentToParts = (content, role) => {
|
|
|
35
35
|
type: "file",
|
|
36
36
|
filename: part.metadata?.filename ?? "file",
|
|
37
37
|
data: part.source_type === "url" ? part.url : part.source_type === "id" ? part.id : part.data,
|
|
38
|
-
mimeType: part.mime_type ?? "application/octet-stream"
|
|
38
|
+
mimeType: part.mime_type ?? "application/octet-stream",
|
|
39
|
+
...(part.source_type === "url" || part.source_type === "id") && { sourceType: part.source_type }
|
|
39
40
|
};
|
|
40
41
|
case "audio": {
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
if (!format) return null;
|
|
42
|
+
const mimeType = part.mime_type ?? "application/octet-stream";
|
|
43
|
+
const subtype = mimeType.startsWith("audio/") ? mimeType.slice(6) : void 0;
|
|
44
44
|
return {
|
|
45
|
-
type: "
|
|
46
|
-
audio:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
45
|
+
type: "file",
|
|
46
|
+
filename: subtype ? `audio.${subtype}` : "audio",
|
|
47
|
+
data: part.data,
|
|
48
|
+
mimeType
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
case "thinking": return {
|
|
@@ -63,6 +62,22 @@ const contentToParts = (content, role) => {
|
|
|
63
62
|
}
|
|
64
63
|
}).filter((p) => p !== null);
|
|
65
64
|
};
|
|
65
|
+
const hasVisibleText = (text) => typeof text === "string" && text.trim() !== "";
|
|
66
|
+
/**
|
|
67
|
+
* Audio output arrives outside the content array: providers leave `content`
|
|
68
|
+
* empty and put the spoken text in `additional_kwargs.audio.transcript`. The
|
|
69
|
+
* audio bytes stay behind because no provider reports their media type, and a
|
|
70
|
+
* streamed response carries raw PCM rather than a playable file.
|
|
71
|
+
*/
|
|
72
|
+
const withAudioTranscript = (parts, additionalKwargs) => {
|
|
73
|
+
const transcript = (additionalKwargs?.audio)?.transcript;
|
|
74
|
+
if (typeof transcript !== "string" || !hasVisibleText(transcript)) return parts;
|
|
75
|
+
if (parts.some((part) => part.type === "text" && hasVisibleText(part.text))) return parts;
|
|
76
|
+
return [...parts.filter((part) => part.type !== "text"), {
|
|
77
|
+
type: "text",
|
|
78
|
+
text: transcript
|
|
79
|
+
}];
|
|
80
|
+
};
|
|
66
81
|
const getCustomMetadata = (additionalKwargs) => additionalKwargs?.metadata ?? {};
|
|
67
82
|
const getStringContent = (content) => {
|
|
68
83
|
if (typeof content === "string") return content;
|
|
@@ -82,7 +97,7 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
|
|
|
82
97
|
case "human": return {
|
|
83
98
|
role: "user",
|
|
84
99
|
id: message.id,
|
|
85
|
-
content: contentToParts(message.content
|
|
100
|
+
content: contentToParts(message.content),
|
|
86
101
|
metadata: { custom: getCustomMetadata(message.additional_kwargs) }
|
|
87
102
|
};
|
|
88
103
|
case "ai": {
|
|
@@ -100,7 +115,7 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
|
|
|
100
115
|
role: "assistant",
|
|
101
116
|
id: message.id,
|
|
102
117
|
content: [
|
|
103
|
-
...contentToParts(message.content,
|
|
118
|
+
...withAudioTranscript(contentToParts(message.content), message.additional_kwargs),
|
|
104
119
|
...toolCallParts,
|
|
105
120
|
...uiDataParts
|
|
106
121
|
],
|
|
@@ -129,6 +144,19 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
|
|
|
129
144
|
};
|
|
130
145
|
}
|
|
131
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* Audio media types that reach a provider's audio input through the LangChain
|
|
149
|
+
* `audio` block. langchain-core derives OpenAI's `input_audio.format` by
|
|
150
|
+
* splitting `mime_type` on `/`, and that format is a wav-or-mp3 enum, so
|
|
151
|
+
* `audio/mpeg` passes the converter and is rejected at the provider.
|
|
152
|
+
*/
|
|
153
|
+
const audioBlockMimeTypes = /* @__PURE__ */ new Map([
|
|
154
|
+
["audio/mp3", "audio/mp3"],
|
|
155
|
+
["audio/mpeg", "audio/mp3"],
|
|
156
|
+
["audio/wav", "audio/wav"],
|
|
157
|
+
["audio/wave", "audio/wav"],
|
|
158
|
+
["audio/x-wav", "audio/wav"]
|
|
159
|
+
]);
|
|
132
160
|
const getMessageContent = (msg) => {
|
|
133
161
|
const allContent = [...msg.content, ...msg.attachments?.flatMap((a) => a.content) ?? []];
|
|
134
162
|
const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image" || part.type === "audio");
|
|
@@ -150,18 +178,35 @@ const getMessageContent = (msg) => {
|
|
|
150
178
|
};
|
|
151
179
|
case "file": {
|
|
152
180
|
const metadata = { filename: part.filename ?? "file" };
|
|
153
|
-
if (
|
|
181
|
+
if (part.sourceType === "id") return {
|
|
182
|
+
type: "file",
|
|
183
|
+
id: part.data,
|
|
184
|
+
mime_type: part.mimeType,
|
|
185
|
+
filename: metadata.filename,
|
|
186
|
+
metadata,
|
|
187
|
+
source_type: "id"
|
|
188
|
+
};
|
|
189
|
+
if (part.sourceType === "url" || httpUrlPattern.test(part.data)) return {
|
|
154
190
|
type: "file",
|
|
155
191
|
url: part.data,
|
|
156
192
|
mime_type: part.mimeType,
|
|
193
|
+
filename: metadata.filename,
|
|
157
194
|
metadata,
|
|
158
195
|
source_type: "url"
|
|
159
196
|
};
|
|
160
197
|
const parsed = parseDataUrl(part.data);
|
|
198
|
+
const audioMimeType = audioBlockMimeTypes.get((parsed?.mimeType ?? part.mimeType).toLowerCase());
|
|
199
|
+
if (audioMimeType) return {
|
|
200
|
+
type: "audio",
|
|
201
|
+
data: parsed?.data ?? part.data,
|
|
202
|
+
mime_type: audioMimeType,
|
|
203
|
+
source_type: "base64"
|
|
204
|
+
};
|
|
161
205
|
return {
|
|
162
206
|
type: "file",
|
|
163
207
|
data: parsed?.data ?? part.data,
|
|
164
208
|
mime_type: parsed?.mimeType ?? part.mimeType,
|
|
209
|
+
filename: metadata.filename,
|
|
165
210
|
metadata,
|
|
166
211
|
source_type: "base64"
|
|
167
212
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convertMessages.js","names":["parts","_exhaustiveCheck"],"sources":["../src/convertMessages.ts"],"sourcesContent":["\"use client\";\n\nimport type { useExternalMessageConverter } from \"@assistant-ui/core/react\";\nimport type {\n AppendMessage,\n DataMessagePart,\n MessageTiming,\n} from \"@assistant-ui/core\";\nimport { httpUrlPattern, parseDataUrl } from \"@assistant-ui/core/internal\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport type {\n LangChainBaseMessage,\n LangChainContentBlock,\n UIMessage,\n} from \"./types\";\n\ntype LangChainMessageConverterMetadata =\n useExternalMessageConverter.Metadata & {\n uiMessagesByParent?: Map<string, UIMessage[]>;\n messageTiming?: Record<string, MessageTiming>;\n };\n\nconst uiMessageToDataPart = (ui: UIMessage): DataMessagePart => ({\n type: \"data\",\n name: ui.name,\n data: ui.props,\n});\n\nexport const getMessageType = (message: LangChainBaseMessage): string => {\n if (typeof message._getType === \"function\") return message._getType();\n if (\"type\" in message)\n return (message as Record<string, unknown>).type as string;\n throw new Error(\"Cannot determine message type\");\n};\n\nconst contentToParts = (content: unknown, role: \"user\" | \"assistant\") => {\n if (typeof content === \"string\")\n return [{ type: \"text\" as const, text: content }];\n\n const parts = content as readonly LangChainContentBlock[];\n return parts\n .map((part) => {\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 };\n case \"audio\": {\n if (role !== \"user\") return null;\n const format =\n part.mime_type === \"audio/wav\"\n ? (\"wav\" as const)\n : part.mime_type === \"audio/mp3\"\n ? (\"mp3\" as const)\n : null;\n if (!format) return null;\n return {\n type: \"audio\" as const,\n audio: { data: part.data, format },\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?.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\") ??\n part.reasoning ??\n \"\",\n };\n case \"tool_use\":\n case \"input_json_delta\":\n return null;\n default:\n return null;\n }\n })\n .filter((p) => p !== null);\n};\n\nconst getCustomMetadata = (\n additionalKwargs: Record<string, unknown> | undefined,\n): Record<string, unknown> =>\n (additionalKwargs?.metadata as Record<string, unknown>) ?? {};\n\nconst getStringContent = (content: unknown): string => {\n if (typeof content === \"string\") return content;\n const parts = content as readonly LangChainContentBlock[];\n return parts\n .filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\");\n};\n\nexport const convertLangChainBaseMessage = (\n message: LangChainBaseMessage,\n metadata: LangChainMessageConverterMetadata = {},\n): useExternalMessageConverter.Message => {\n const type = getMessageType(message);\n\n switch (type) {\n case \"system\":\n return {\n role: \"system\",\n id: message.id,\n content: [{ type: \"text\", text: getStringContent(message.content) }],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"human\":\n return {\n role: \"user\",\n id: message.id,\n content: contentToParts(message.content, \"user\"),\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"ai\": {\n const toolCallParts =\n message.tool_calls?.map((tc) => ({\n type: \"tool-call\" as const,\n toolCallId: tc.id,\n toolName: tc.name,\n args: tc.args as ReadonlyJSONObject,\n argsText: JSON.stringify(tc.args),\n })) ?? [];\n\n const assistantStatus =\n typeof message.status === \"object\" ? message.status : undefined;\n\n const uiDataParts =\n (message.id\n ? metadata.uiMessagesByParent\n ?.get(message.id)\n ?.map(uiMessageToDataPart)\n : undefined) ?? [];\n\n const timing = metadata.messageTiming?.[message.id ?? \"\"];\n\n return {\n role: \"assistant\",\n id: message.id,\n content: [\n ...contentToParts(message.content, \"assistant\"),\n ...toolCallParts,\n ...uiDataParts,\n ],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n ...(timing && { timing }),\n },\n ...(assistantStatus && { status: assistantStatus }),\n };\n }\n\n case \"tool\":\n return {\n role: \"tool\",\n toolName: message.name ?? \"\",\n toolCallId: message.tool_call_id ?? \"\",\n result: message.content,\n artifact: message.artifact,\n isError: message.status === \"error\",\n };\n\n default:\n return {\n role: \"system\",\n id: message.id,\n content: [\n {\n type: \"text\",\n text:\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content),\n },\n ],\n };\n }\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 (httpUrlPattern.test(part.data)) {\n return {\n type: \"file\" as const,\n url: part.data,\n mime_type: part.mimeType,\n metadata,\n source_type: \"url\" as const,\n };\n }\n const parsed = parseDataUrl(part.data);\n return {\n type: \"file\" as const,\n data: parsed?.data ?? part.data,\n mime_type: parsed?.mimeType ?? part.mimeType,\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"convertMessages.js","names":["parts","_exhaustiveCheck"],"sources":["../src/convertMessages.ts"],"sourcesContent":["\"use client\";\n\nimport type { useExternalMessageConverter } from \"@assistant-ui/core/react\";\nimport type {\n AppendMessage,\n DataMessagePart,\n MessageTiming,\n} from \"@assistant-ui/core\";\nimport { httpUrlPattern, parseDataUrl } from \"@assistant-ui/core/internal\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport type {\n LangChainBaseMessage,\n LangChainContentBlock,\n UIMessage,\n} from \"./types\";\n\ntype LangChainMessageConverterMetadata =\n useExternalMessageConverter.Metadata & {\n uiMessagesByParent?: Map<string, UIMessage[]>;\n messageTiming?: Record<string, MessageTiming>;\n };\n\nconst uiMessageToDataPart = (ui: UIMessage): DataMessagePart => ({\n type: \"data\",\n name: ui.name,\n data: ui.props,\n});\n\nexport const getMessageType = (message: LangChainBaseMessage): string => {\n if (typeof message._getType === \"function\") return message._getType();\n if (\"type\" in message)\n return (message as Record<string, unknown>).type as string;\n throw new Error(\"Cannot determine message type\");\n};\n\nconst contentToParts = (content: unknown) => {\n if (typeof content === \"string\")\n return [{ type: \"text\" as const, text: content }];\n\n const parts = content as readonly LangChainContentBlock[];\n return parts\n .map((part) => {\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?.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\") ??\n part.reasoning ??\n \"\",\n };\n case \"tool_use\":\n case \"input_json_delta\":\n return null;\n default:\n return null;\n }\n })\n .filter((p) => p !== null);\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 */\nconst withAudioTranscript = (\n parts: ReturnType<typeof contentToParts>,\n additionalKwargs: Record<string, unknown> | undefined,\n): ReturnType<typeof contentToParts> => {\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\nconst getCustomMetadata = (\n additionalKwargs: Record<string, unknown> | undefined,\n): Record<string, unknown> =>\n (additionalKwargs?.metadata as Record<string, unknown>) ?? {};\n\nconst getStringContent = (content: unknown): string => {\n if (typeof content === \"string\") return content;\n const parts = content as readonly LangChainContentBlock[];\n return parts\n .filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\");\n};\n\nexport const convertLangChainBaseMessage = (\n message: LangChainBaseMessage,\n metadata: LangChainMessageConverterMetadata = {},\n): useExternalMessageConverter.Message => {\n const type = getMessageType(message);\n\n switch (type) {\n case \"system\":\n return {\n role: \"system\",\n id: message.id,\n content: [{ type: \"text\", text: getStringContent(message.content) }],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"human\":\n return {\n role: \"user\",\n id: message.id,\n content: contentToParts(message.content),\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"ai\": {\n const toolCallParts =\n message.tool_calls?.map((tc) => ({\n type: \"tool-call\" as const,\n toolCallId: tc.id,\n toolName: tc.name,\n args: tc.args as ReadonlyJSONObject,\n argsText: JSON.stringify(tc.args),\n })) ?? [];\n\n const assistantStatus =\n typeof message.status === \"object\" ? message.status : undefined;\n\n const uiDataParts =\n (message.id\n ? metadata.uiMessagesByParent\n ?.get(message.id)\n ?.map(uiMessageToDataPart)\n : undefined) ?? [];\n\n const timing = metadata.messageTiming?.[message.id ?? \"\"];\n\n return {\n role: \"assistant\",\n id: message.id,\n content: [\n ...withAudioTranscript(\n contentToParts(message.content),\n message.additional_kwargs,\n ),\n ...toolCallParts,\n ...uiDataParts,\n ],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n ...(timing && { timing }),\n },\n ...(assistantStatus && { status: assistantStatus }),\n };\n }\n\n case \"tool\":\n return {\n role: \"tool\",\n toolName: message.name ?? \"\",\n toolCallId: message.tool_call_id ?? \"\",\n result: message.content,\n artifact: message.artifact,\n isError: message.status === \"error\",\n };\n\n default:\n return {\n role: \"system\",\n id: message.id,\n content: [\n {\n type: \"text\",\n text:\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content),\n },\n ],\n };\n }\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 if (part.sourceType === \"url\" || httpUrlPattern.test(part.data)) {\n return {\n type: \"file\" as const,\n url: part.data,\n mime_type: part.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"url\" as const,\n };\n }\n const parsed = parseDataUrl(part.data);\n const audioMimeType = audioBlockMimeTypes.get(\n (parsed?.mimeType ?? part.mimeType).toLowerCase(),\n );\n if (audioMimeType) {\n return {\n type: \"audio\" as const,\n data: parsed?.data ?? part.data,\n mime_type: audioMimeType,\n source_type: \"base64\" as const,\n };\n }\n return {\n type: \"file\" as const,\n data: parsed?.data ?? part.data,\n mime_type: parsed?.mimeType ?? part.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"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;cAYA,0BACX,mBACC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;cAYA,0BACX,mBACC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;cAwZF,mBAAoB,YAAY,yDAAuB"}
|
package/dist/useStreamRuntime.js
CHANGED
|
@@ -57,8 +57,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
57
57
|
const messagesKey = options.messagesKey ?? "messages";
|
|
58
58
|
const uiStateKey = options.uiStateKey ?? "ui";
|
|
59
59
|
const externalId = useAuiState((s) => s.threadListItem.externalId);
|
|
60
|
-
options
|
|
61
|
-
const stream = useStream(options);
|
|
60
|
+
const stream = useStream(Object.assign({}, options, { threadId: externalId }));
|
|
62
61
|
const [stagedMessages, setStagedMessages] = useState(null);
|
|
63
62
|
const [toolStatuses, setToolStatuses] = useState({});
|
|
64
63
|
const hasExecutingTools = Object.values(toolStatuses).some((s) => s?.type === "executing");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\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 * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\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\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Mutate in place rather than `{ ...options, threadId }`: spreading\n // `UseStreamOptions` (a discriminated union on `transport`) into an object\n // literal merges both arms' transport types, breaking arm assignment.\n options.threadId = externalId;\n\n const stream = useStream(options);\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: stream.submit,\n respond: stream.respond,\n respondAll: stream.respondAll,\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n await stream.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n runConfigToSubmitOptions(msg.runConfig),\n );\n },\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await stream.submit({\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n });\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\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 * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\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\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: stream.submit,\n respond: stream.respond,\n respondAll: stream.respondAll,\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n await stream.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n runConfigToSubmitOptions(msg.runConfig),\n );\n },\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await stream.submit({\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n });\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-langchain",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "LangChain useStream adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"langchain",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@assistant-ui/core": "^0.3.
|
|
33
|
-
"@assistant-ui/store": "^0.3.
|
|
32
|
+
"@assistant-ui/core": "^0.3.6",
|
|
33
|
+
"@assistant-ui/store": "^0.3.4",
|
|
34
34
|
"assistant-cloud": "*",
|
|
35
|
-
"assistant-stream": "^0.3.
|
|
35
|
+
"assistant-stream": "^0.3.34"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@langchain/react": "^1.0.2",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"jsdom": "^29.1.1",
|
|
54
54
|
"react": "^19.2.8",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
56
|
+
"@assistant-ui/x-buildutils": "0.0.21"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public",
|
|
@@ -87,6 +87,7 @@ describe("convertLangChainBaseMessage file content parts", () => {
|
|
|
87
87
|
filename: "file.pdf",
|
|
88
88
|
data: "https://r2.example/u/abc/file.pdf",
|
|
89
89
|
mimeType: "application/pdf",
|
|
90
|
+
sourceType: "url",
|
|
90
91
|
},
|
|
91
92
|
]);
|
|
92
93
|
});
|
|
@@ -103,6 +104,7 @@ describe("convertLangChainBaseMessage file content parts", () => {
|
|
|
103
104
|
filename: "file",
|
|
104
105
|
data: "file-abc123",
|
|
105
106
|
mimeType: "application/octet-stream",
|
|
107
|
+
sourceType: "id",
|
|
106
108
|
},
|
|
107
109
|
]);
|
|
108
110
|
});
|
|
@@ -128,6 +130,7 @@ describe("getMessageContent file blocks", () => {
|
|
|
128
130
|
type: "file",
|
|
129
131
|
data: "ZmFrZQ==",
|
|
130
132
|
mime_type: "application/pdf",
|
|
133
|
+
filename: "a.pdf",
|
|
131
134
|
metadata: { filename: "a.pdf" },
|
|
132
135
|
source_type: "base64",
|
|
133
136
|
},
|
|
@@ -150,6 +153,7 @@ describe("getMessageContent file blocks", () => {
|
|
|
150
153
|
type: "file",
|
|
151
154
|
url: "https://r2.example/u/abc/file.pdf",
|
|
152
155
|
mime_type: "application/pdf",
|
|
156
|
+
filename: "file.pdf",
|
|
153
157
|
metadata: { filename: "file.pdf" },
|
|
154
158
|
source_type: "url",
|
|
155
159
|
},
|
|
@@ -173,6 +177,7 @@ describe("getMessageContent file blocks", () => {
|
|
|
173
177
|
type: "file",
|
|
174
178
|
data: "ZmFrZQ==",
|
|
175
179
|
mime_type: "application/pdf",
|
|
180
|
+
filename: "a.pdf",
|
|
176
181
|
metadata: { filename: "a.pdf" },
|
|
177
182
|
source_type: "base64",
|
|
178
183
|
},
|
|
@@ -195,11 +200,251 @@ describe("getMessageContent file blocks", () => {
|
|
|
195
200
|
type: "file",
|
|
196
201
|
data: "blob:https://app.example/123",
|
|
197
202
|
mime_type: "application/pdf",
|
|
203
|
+
filename: "a.pdf",
|
|
204
|
+
metadata: { filename: "a.pdf" },
|
|
205
|
+
source_type: "base64",
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("emits an id source block with the value in the id key for sourceType id", () => {
|
|
211
|
+
const content = getMessageContent(
|
|
212
|
+
appendMessage({
|
|
213
|
+
type: "file",
|
|
214
|
+
data: "flx::storage:file_object:abc",
|
|
215
|
+
mimeType: "application/pdf",
|
|
216
|
+
filename: "invoice.pdf",
|
|
217
|
+
sourceType: "id",
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(content).toEqual([
|
|
222
|
+
{ type: "text", text: " " },
|
|
223
|
+
{
|
|
224
|
+
type: "file",
|
|
225
|
+
id: "flx::storage:file_object:abc",
|
|
226
|
+
mime_type: "application/pdf",
|
|
227
|
+
filename: "invoice.pdf",
|
|
228
|
+
metadata: { filename: "invoice.pdf" },
|
|
229
|
+
source_type: "id",
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
expect(content[1]).not.toHaveProperty("data");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("lets sourceType url override sniffing for non-http data", () => {
|
|
236
|
+
const content = getMessageContent(
|
|
237
|
+
appendMessage({
|
|
238
|
+
type: "file",
|
|
239
|
+
data: "s3://bucket/key.pdf",
|
|
240
|
+
mimeType: "application/pdf",
|
|
241
|
+
filename: "key.pdf",
|
|
242
|
+
sourceType: "url",
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(content).toEqual([
|
|
247
|
+
{ type: "text", text: " " },
|
|
248
|
+
{
|
|
249
|
+
type: "file",
|
|
250
|
+
url: "s3://bucket/key.pdf",
|
|
251
|
+
mime_type: "application/pdf",
|
|
252
|
+
filename: "key.pdf",
|
|
253
|
+
metadata: { filename: "key.pdf" },
|
|
254
|
+
source_type: "url",
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("emits an id source block for attachment content parts", () => {
|
|
260
|
+
const content = getMessageContent({
|
|
261
|
+
content: [{ type: "text", text: "see attached" }],
|
|
262
|
+
attachments: [
|
|
263
|
+
{
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "file",
|
|
267
|
+
data: "file-abc123",
|
|
268
|
+
mimeType: "application/pdf",
|
|
269
|
+
filename: "a.pdf",
|
|
270
|
+
sourceType: "id",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
} as unknown as AppendMessage);
|
|
276
|
+
|
|
277
|
+
expect(content).toEqual([
|
|
278
|
+
{ type: "text", text: "see attached" },
|
|
279
|
+
{
|
|
280
|
+
type: "file",
|
|
281
|
+
id: "file-abc123",
|
|
282
|
+
mime_type: "application/pdf",
|
|
283
|
+
filename: "a.pdf",
|
|
284
|
+
metadata: { filename: "a.pdf" },
|
|
285
|
+
source_type: "id",
|
|
286
|
+
},
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("round-trips an id source block through both converters", () => {
|
|
291
|
+
const converted = convertLangChainBaseMessage(
|
|
292
|
+
humanMessage([
|
|
293
|
+
{
|
|
294
|
+
type: "file",
|
|
295
|
+
id: "file-abc123",
|
|
296
|
+
mime_type: "application/pdf",
|
|
297
|
+
source_type: "id",
|
|
298
|
+
metadata: { filename: "a.pdf" },
|
|
299
|
+
},
|
|
300
|
+
]),
|
|
301
|
+
{},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const content = getMessageContent(converted as unknown as AppendMessage);
|
|
305
|
+
|
|
306
|
+
expect(content).toEqual([
|
|
307
|
+
{ type: "text", text: " " },
|
|
308
|
+
{
|
|
309
|
+
type: "file",
|
|
310
|
+
id: "file-abc123",
|
|
311
|
+
mime_type: "application/pdf",
|
|
312
|
+
filename: "a.pdf",
|
|
198
313
|
metadata: { filename: "a.pdf" },
|
|
314
|
+
source_type: "id",
|
|
315
|
+
},
|
|
316
|
+
]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("emits an audio block for a base64 file part with an audio mime type", () => {
|
|
320
|
+
const content = getMessageContent(
|
|
321
|
+
appendMessage({
|
|
322
|
+
type: "file",
|
|
323
|
+
data: "c291bmQ=",
|
|
324
|
+
mimeType: "audio/mp3",
|
|
325
|
+
filename: "memo.mp3",
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(content).toEqual([
|
|
330
|
+
{ type: "text", text: " " },
|
|
331
|
+
{
|
|
332
|
+
type: "audio",
|
|
333
|
+
data: "c291bmQ=",
|
|
334
|
+
mime_type: "audio/mp3",
|
|
199
335
|
source_type: "base64",
|
|
200
336
|
},
|
|
201
337
|
]);
|
|
202
338
|
});
|
|
339
|
+
|
|
340
|
+
it("normalizes audio/mpeg and audio/x-wav to the accepted spellings", () => {
|
|
341
|
+
expect(
|
|
342
|
+
getMessageContent(
|
|
343
|
+
appendMessage({
|
|
344
|
+
type: "file",
|
|
345
|
+
data: "c291bmQ=",
|
|
346
|
+
mimeType: "audio/mpeg",
|
|
347
|
+
}),
|
|
348
|
+
)[1],
|
|
349
|
+
).toMatchObject({ type: "audio", mime_type: "audio/mp3" });
|
|
350
|
+
|
|
351
|
+
expect(
|
|
352
|
+
getMessageContent(
|
|
353
|
+
appendMessage({
|
|
354
|
+
type: "file",
|
|
355
|
+
data: "c291bmQ=",
|
|
356
|
+
mimeType: "audio/x-wav",
|
|
357
|
+
}),
|
|
358
|
+
)[1],
|
|
359
|
+
).toMatchObject({ type: "audio", mime_type: "audio/wav" });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("strips the data URL envelope from an audio file part", () => {
|
|
363
|
+
const content = getMessageContent(
|
|
364
|
+
appendMessage({
|
|
365
|
+
type: "file",
|
|
366
|
+
data: "data:audio/mpeg;base64,c291bmQ=",
|
|
367
|
+
mimeType: "audio/mp3",
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
expect(content[1]).toEqual({
|
|
372
|
+
type: "audio",
|
|
373
|
+
data: "c291bmQ=",
|
|
374
|
+
mime_type: "audio/mp3",
|
|
375
|
+
source_type: "base64",
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("keeps url and id audio references as file blocks", () => {
|
|
380
|
+
expect(
|
|
381
|
+
getMessageContent(
|
|
382
|
+
appendMessage({
|
|
383
|
+
type: "file",
|
|
384
|
+
data: "https://cdn.example.com/memo.mp3",
|
|
385
|
+
mimeType: "audio/mp3",
|
|
386
|
+
filename: "memo.mp3",
|
|
387
|
+
}),
|
|
388
|
+
)[1],
|
|
389
|
+
).toMatchObject({ type: "file", source_type: "url" });
|
|
390
|
+
|
|
391
|
+
expect(
|
|
392
|
+
getMessageContent(
|
|
393
|
+
appendMessage({
|
|
394
|
+
type: "file",
|
|
395
|
+
data: "file-abc123",
|
|
396
|
+
mimeType: "audio/mp3",
|
|
397
|
+
filename: "memo.mp3",
|
|
398
|
+
sourceType: "id",
|
|
399
|
+
}),
|
|
400
|
+
)[1],
|
|
401
|
+
).toMatchObject({ type: "file", source_type: "id" });
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("does not treat inherited object keys as audio media types", () => {
|
|
405
|
+
for (const mimeType of ["__proto__", "constructor"]) {
|
|
406
|
+
expect(
|
|
407
|
+
getMessageContent(
|
|
408
|
+
appendMessage({
|
|
409
|
+
type: "file",
|
|
410
|
+
data: "ZmFrZQ==",
|
|
411
|
+
mimeType,
|
|
412
|
+
filename: "a.bin",
|
|
413
|
+
}),
|
|
414
|
+
)[1],
|
|
415
|
+
).toMatchObject({ type: "file", mime_type: mimeType });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("detects audio from the data URL envelope when the declared type is generic", () => {
|
|
420
|
+
expect(
|
|
421
|
+
getMessageContent(
|
|
422
|
+
appendMessage({
|
|
423
|
+
type: "file",
|
|
424
|
+
data: "data:audio/mpeg;base64,c291bmQ=",
|
|
425
|
+
mimeType: "application/octet-stream",
|
|
426
|
+
}),
|
|
427
|
+
)[1],
|
|
428
|
+
).toEqual({
|
|
429
|
+
type: "audio",
|
|
430
|
+
data: "c291bmQ=",
|
|
431
|
+
mime_type: "audio/mp3",
|
|
432
|
+
source_type: "base64",
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("leaves non-audio file parts as file blocks", () => {
|
|
437
|
+
expect(
|
|
438
|
+
getMessageContent(
|
|
439
|
+
appendMessage({
|
|
440
|
+
type: "file",
|
|
441
|
+
data: "ZmFrZQ==",
|
|
442
|
+
mimeType: "application/pdf",
|
|
443
|
+
filename: "a.pdf",
|
|
444
|
+
}),
|
|
445
|
+
)[1],
|
|
446
|
+
).toMatchObject({ type: "file", mime_type: "application/pdf" });
|
|
447
|
+
});
|
|
203
448
|
});
|
|
204
449
|
|
|
205
450
|
describe("getMessageContent audio and data parts", () => {
|
|
@@ -309,6 +554,18 @@ describe("getMessageContent audio and data parts", () => {
|
|
|
309
554
|
});
|
|
310
555
|
|
|
311
556
|
describe("contentToParts audio blocks", () => {
|
|
557
|
+
const inboundAudioPart = (block: Record<string, unknown>) => {
|
|
558
|
+
const result = convertLangChainBaseMessage(
|
|
559
|
+
{
|
|
560
|
+
_getType: () => "human",
|
|
561
|
+
id: "h1",
|
|
562
|
+
content: [{ type: "audio", data: "c291bmQ=", ...block }],
|
|
563
|
+
},
|
|
564
|
+
{},
|
|
565
|
+
);
|
|
566
|
+
return contentOf(result)[0];
|
|
567
|
+
};
|
|
568
|
+
|
|
312
569
|
it("converts an inbound base64 audio block back to an audio part", () => {
|
|
313
570
|
const result = convertLangChainBaseMessage(
|
|
314
571
|
{
|
|
@@ -328,11 +585,18 @@ describe("contentToParts audio blocks", () => {
|
|
|
328
585
|
|
|
329
586
|
expect(result).toMatchObject({
|
|
330
587
|
role: "user",
|
|
331
|
-
content: [
|
|
588
|
+
content: [
|
|
589
|
+
{
|
|
590
|
+
type: "file",
|
|
591
|
+
filename: "audio.mp3",
|
|
592
|
+
data: "c291bmQ=",
|
|
593
|
+
mimeType: "audio/mp3",
|
|
594
|
+
},
|
|
595
|
+
],
|
|
332
596
|
});
|
|
333
597
|
});
|
|
334
598
|
|
|
335
|
-
it("
|
|
599
|
+
it("keeps an inbound audio block whose mime type has no wire format", () => {
|
|
336
600
|
const result = convertLangChainBaseMessage(
|
|
337
601
|
{
|
|
338
602
|
_getType: () => "human",
|
|
@@ -349,10 +613,20 @@ describe("contentToParts audio blocks", () => {
|
|
|
349
613
|
{},
|
|
350
614
|
);
|
|
351
615
|
|
|
352
|
-
expect(result).toMatchObject({
|
|
616
|
+
expect(result).toMatchObject({
|
|
617
|
+
role: "user",
|
|
618
|
+
content: [
|
|
619
|
+
{
|
|
620
|
+
type: "file",
|
|
621
|
+
filename: "audio.ogg",
|
|
622
|
+
data: "b2dn",
|
|
623
|
+
mimeType: "audio/ogg",
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
});
|
|
353
627
|
});
|
|
354
628
|
|
|
355
|
-
it("
|
|
629
|
+
it("keeps an audio block on an assistant message", () => {
|
|
356
630
|
const result = convertLangChainBaseMessage(
|
|
357
631
|
{
|
|
358
632
|
_getType: () => "ai",
|
|
@@ -372,7 +646,61 @@ describe("contentToParts audio blocks", () => {
|
|
|
372
646
|
|
|
373
647
|
expect(result).toMatchObject({
|
|
374
648
|
role: "assistant",
|
|
375
|
-
content: [
|
|
649
|
+
content: [
|
|
650
|
+
{
|
|
651
|
+
type: "file",
|
|
652
|
+
filename: "audio.mp3",
|
|
653
|
+
data: "c291bmQ=",
|
|
654
|
+
mimeType: "audio/mp3",
|
|
655
|
+
},
|
|
656
|
+
{ type: "text", text: "done" },
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
it("round-trips an audio file part through both converters", () => {
|
|
661
|
+
const outbound = getMessageContent({
|
|
662
|
+
content: [
|
|
663
|
+
{
|
|
664
|
+
type: "file",
|
|
665
|
+
data: "data:audio/mpeg;base64,c291bmQ=",
|
|
666
|
+
mimeType: "audio/mpeg",
|
|
667
|
+
filename: "memo.mp3",
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
} as unknown as AppendMessage);
|
|
671
|
+
|
|
672
|
+
const inbound = convertLangChainBaseMessage(
|
|
673
|
+
{
|
|
674
|
+
_getType: () => "human",
|
|
675
|
+
id: "h1",
|
|
676
|
+
content: outbound as never,
|
|
677
|
+
},
|
|
678
|
+
{},
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
expect(contentOf(inbound)).toEqual([
|
|
682
|
+
{ type: "text", text: " " },
|
|
683
|
+
{
|
|
684
|
+
type: "file",
|
|
685
|
+
filename: "audio.mp3",
|
|
686
|
+
data: "c291bmQ=",
|
|
687
|
+
mimeType: "audio/mp3",
|
|
688
|
+
},
|
|
689
|
+
]);
|
|
690
|
+
|
|
691
|
+
expect(
|
|
692
|
+
getMessageContent({
|
|
693
|
+
content: contentOf(inbound),
|
|
694
|
+
} as unknown as AppendMessage),
|
|
695
|
+
).toEqual(outbound);
|
|
696
|
+
});
|
|
697
|
+
it("names an inbound audio attachment from its media subtype", () => {
|
|
698
|
+
expect(inboundAudioPart({ mime_type: "audio/wav" })).toMatchObject({
|
|
699
|
+
filename: "audio.wav",
|
|
700
|
+
});
|
|
701
|
+
expect(inboundAudioPart({})).toMatchObject({
|
|
702
|
+
filename: "audio",
|
|
703
|
+
mimeType: "application/octet-stream",
|
|
376
704
|
});
|
|
377
705
|
});
|
|
378
706
|
});
|
|
@@ -521,3 +849,102 @@ describe("convertLangChainBaseMessage image content parts", () => {
|
|
|
521
849
|
expect(contentOf(result)).toEqual([]);
|
|
522
850
|
});
|
|
523
851
|
});
|
|
852
|
+
|
|
853
|
+
describe("convertLangChainBaseMessage audio transcripts", () => {
|
|
854
|
+
const audioMessage = (
|
|
855
|
+
content: unknown,
|
|
856
|
+
audio: unknown,
|
|
857
|
+
): LangChainBaseMessage => ({
|
|
858
|
+
_getType: () => "ai",
|
|
859
|
+
id: "msg-3",
|
|
860
|
+
content,
|
|
861
|
+
additional_kwargs: { audio },
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("surfaces the transcript when the provider leaves content empty", () => {
|
|
865
|
+
const result = convertLangChainBaseMessage(
|
|
866
|
+
audioMessage("", {
|
|
867
|
+
id: "audio_1",
|
|
868
|
+
data: "UklGRg==",
|
|
869
|
+
expires_at: 1,
|
|
870
|
+
transcript: "the secret number is four seven two",
|
|
871
|
+
}),
|
|
872
|
+
{},
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
expect(contentOf(result)).toEqual([
|
|
876
|
+
{ type: "text", text: "the secret number is four seven two" },
|
|
877
|
+
]);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("treats a whitespace-only placeholder as no text", () => {
|
|
881
|
+
const result = convertLangChainBaseMessage(
|
|
882
|
+
audioMessage([{ type: "text", text: " " }], {
|
|
883
|
+
transcript: "spoken words",
|
|
884
|
+
}),
|
|
885
|
+
{},
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
expect(contentOf(result)).toEqual([{ type: "text", text: "spoken words" }]);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("does not throw on a non-spec text block whose text is missing or not a string", () => {
|
|
892
|
+
for (const block of [{ type: "text" }, { type: "text", text: 42 }]) {
|
|
893
|
+
const result = convertLangChainBaseMessage(
|
|
894
|
+
audioMessage([block], { transcript: "spoken words" }),
|
|
895
|
+
{},
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
expect(contentOf(result)).toEqual([
|
|
899
|
+
{ type: "text", text: "spoken words" },
|
|
900
|
+
]);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("keeps non-text parts when it substitutes the transcript", () => {
|
|
905
|
+
const result = convertLangChainBaseMessage(
|
|
906
|
+
audioMessage(
|
|
907
|
+
[
|
|
908
|
+
{ type: "text", text: "" },
|
|
909
|
+
{
|
|
910
|
+
type: "image_url",
|
|
911
|
+
image_url: { url: "https://example.com/a.png" },
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
{ transcript: "spoken words" },
|
|
915
|
+
),
|
|
916
|
+
{},
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
expect(contentOf(result)).toEqual([
|
|
920
|
+
{ type: "image", image: "https://example.com/a.png" },
|
|
921
|
+
{ type: "text", text: "spoken words" },
|
|
922
|
+
]);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it("leaves existing text alone so the transcript is not duplicated", () => {
|
|
926
|
+
const result = convertLangChainBaseMessage(
|
|
927
|
+
audioMessage([{ type: "text", text: "written answer" }], {
|
|
928
|
+
transcript: "written answer",
|
|
929
|
+
}),
|
|
930
|
+
{},
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
expect(contentOf(result)).toEqual([
|
|
934
|
+
{ type: "text", text: "written answer" },
|
|
935
|
+
]);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it("ignores an absent, blank, or non-string transcript", () => {
|
|
939
|
+
for (const audio of [
|
|
940
|
+
undefined,
|
|
941
|
+
{},
|
|
942
|
+
{ transcript: "" },
|
|
943
|
+
{ transcript: " " },
|
|
944
|
+
{ transcript: 42 },
|
|
945
|
+
]) {
|
|
946
|
+
const result = convertLangChainBaseMessage(audioMessage("", audio), {});
|
|
947
|
+
expect(contentOf(result)).toEqual([{ type: "text", text: "" }]);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
});
|
package/src/convertMessages.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const getMessageType = (message: LangChainBaseMessage): string => {
|
|
|
33
33
|
throw new Error("Cannot determine message type");
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
const contentToParts = (content: unknown
|
|
36
|
+
const contentToParts = (content: unknown) => {
|
|
37
37
|
if (typeof content === "string")
|
|
38
38
|
return [{ type: "text" as const, text: content }];
|
|
39
39
|
|
|
@@ -64,19 +64,20 @@ const contentToParts = (content: unknown, role: "user" | "assistant") => {
|
|
|
64
64
|
? part.id
|
|
65
65
|
: part.data,
|
|
66
66
|
mimeType: part.mime_type ?? "application/octet-stream",
|
|
67
|
+
...((part.source_type === "url" || part.source_type === "id") && {
|
|
68
|
+
sourceType: part.source_type,
|
|
69
|
+
}),
|
|
67
70
|
};
|
|
68
71
|
case "audio": {
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: part.mime_type === "audio/mp3"
|
|
74
|
-
? ("mp3" as const)
|
|
75
|
-
: null;
|
|
76
|
-
if (!format) return null;
|
|
72
|
+
const mimeType = part.mime_type ?? "application/octet-stream";
|
|
73
|
+
const subtype = mimeType.startsWith("audio/")
|
|
74
|
+
? mimeType.slice("audio/".length)
|
|
75
|
+
: undefined;
|
|
77
76
|
return {
|
|
78
|
-
type: "
|
|
79
|
-
|
|
77
|
+
type: "file" as const,
|
|
78
|
+
filename: subtype ? `audio.${subtype}` : "audio",
|
|
79
|
+
data: part.data,
|
|
80
|
+
mimeType,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
case "thinking":
|
|
@@ -99,6 +100,31 @@ const contentToParts = (content: unknown, role: "user" | "assistant") => {
|
|
|
99
100
|
.filter((p) => p !== null);
|
|
100
101
|
};
|
|
101
102
|
|
|
103
|
+
const hasVisibleText = (text: unknown): boolean =>
|
|
104
|
+
typeof text === "string" && text.trim() !== "";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Audio output arrives outside the content array: providers leave `content`
|
|
108
|
+
* empty and put the spoken text in `additional_kwargs.audio.transcript`. The
|
|
109
|
+
* audio bytes stay behind because no provider reports their media type, and a
|
|
110
|
+
* streamed response carries raw PCM rather than a playable file.
|
|
111
|
+
*/
|
|
112
|
+
const withAudioTranscript = (
|
|
113
|
+
parts: ReturnType<typeof contentToParts>,
|
|
114
|
+
additionalKwargs: Record<string, unknown> | undefined,
|
|
115
|
+
): ReturnType<typeof contentToParts> => {
|
|
116
|
+
const audio = additionalKwargs?.audio as { transcript?: unknown } | undefined;
|
|
117
|
+
const transcript = audio?.transcript;
|
|
118
|
+
if (typeof transcript !== "string" || !hasVisibleText(transcript))
|
|
119
|
+
return parts;
|
|
120
|
+
if (parts.some((part) => part.type === "text" && hasVisibleText(part.text)))
|
|
121
|
+
return parts;
|
|
122
|
+
return [
|
|
123
|
+
...parts.filter((part) => part.type !== "text"),
|
|
124
|
+
{ type: "text" as const, text: transcript },
|
|
125
|
+
];
|
|
126
|
+
};
|
|
127
|
+
|
|
102
128
|
const getCustomMetadata = (
|
|
103
129
|
additionalKwargs: Record<string, unknown> | undefined,
|
|
104
130
|
): Record<string, unknown> =>
|
|
@@ -134,7 +160,7 @@ export const convertLangChainBaseMessage = (
|
|
|
134
160
|
return {
|
|
135
161
|
role: "user",
|
|
136
162
|
id: message.id,
|
|
137
|
-
content: contentToParts(message.content
|
|
163
|
+
content: contentToParts(message.content),
|
|
138
164
|
metadata: {
|
|
139
165
|
custom: getCustomMetadata(message.additional_kwargs),
|
|
140
166
|
},
|
|
@@ -166,7 +192,10 @@ export const convertLangChainBaseMessage = (
|
|
|
166
192
|
role: "assistant",
|
|
167
193
|
id: message.id,
|
|
168
194
|
content: [
|
|
169
|
-
...
|
|
195
|
+
...withAudioTranscript(
|
|
196
|
+
contentToParts(message.content),
|
|
197
|
+
message.additional_kwargs,
|
|
198
|
+
),
|
|
170
199
|
...toolCallParts,
|
|
171
200
|
...uiDataParts,
|
|
172
201
|
],
|
|
@@ -205,6 +234,20 @@ export const convertLangChainBaseMessage = (
|
|
|
205
234
|
}
|
|
206
235
|
};
|
|
207
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Audio media types that reach a provider's audio input through the LangChain
|
|
239
|
+
* `audio` block. langchain-core derives OpenAI's `input_audio.format` by
|
|
240
|
+
* splitting `mime_type` on `/`, and that format is a wav-or-mp3 enum, so
|
|
241
|
+
* `audio/mpeg` passes the converter and is rejected at the provider.
|
|
242
|
+
*/
|
|
243
|
+
const audioBlockMimeTypes = new Map<string, "audio/mp3" | "audio/wav">([
|
|
244
|
+
["audio/mp3", "audio/mp3"],
|
|
245
|
+
["audio/mpeg", "audio/mp3"],
|
|
246
|
+
["audio/wav", "audio/wav"],
|
|
247
|
+
["audio/wave", "audio/wav"],
|
|
248
|
+
["audio/x-wav", "audio/wav"],
|
|
249
|
+
]);
|
|
250
|
+
|
|
208
251
|
export const getMessageContent = (msg: AppendMessage) => {
|
|
209
252
|
const allContent = [
|
|
210
253
|
...msg.content,
|
|
@@ -229,20 +272,43 @@ export const getMessageContent = (msg: AppendMessage) => {
|
|
|
229
272
|
return { type: "image_url" as const, image_url: { url: part.image } };
|
|
230
273
|
case "file": {
|
|
231
274
|
const metadata = { filename: part.filename ?? "file" };
|
|
232
|
-
if (
|
|
275
|
+
if (part.sourceType === "id") {
|
|
276
|
+
return {
|
|
277
|
+
type: "file" as const,
|
|
278
|
+
id: part.data,
|
|
279
|
+
mime_type: part.mimeType,
|
|
280
|
+
filename: metadata.filename,
|
|
281
|
+
metadata,
|
|
282
|
+
source_type: "id" as const,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (part.sourceType === "url" || httpUrlPattern.test(part.data)) {
|
|
233
286
|
return {
|
|
234
287
|
type: "file" as const,
|
|
235
288
|
url: part.data,
|
|
236
289
|
mime_type: part.mimeType,
|
|
290
|
+
filename: metadata.filename,
|
|
237
291
|
metadata,
|
|
238
292
|
source_type: "url" as const,
|
|
239
293
|
};
|
|
240
294
|
}
|
|
241
295
|
const parsed = parseDataUrl(part.data);
|
|
296
|
+
const audioMimeType = audioBlockMimeTypes.get(
|
|
297
|
+
(parsed?.mimeType ?? part.mimeType).toLowerCase(),
|
|
298
|
+
);
|
|
299
|
+
if (audioMimeType) {
|
|
300
|
+
return {
|
|
301
|
+
type: "audio" as const,
|
|
302
|
+
data: parsed?.data ?? part.data,
|
|
303
|
+
mime_type: audioMimeType,
|
|
304
|
+
source_type: "base64" as const,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
242
307
|
return {
|
|
243
308
|
type: "file" as const,
|
|
244
309
|
data: parsed?.data ?? part.data,
|
|
245
310
|
mime_type: parsed?.mimeType ?? part.mimeType,
|
|
311
|
+
filename: metadata.filename,
|
|
246
312
|
metadata,
|
|
247
313
|
source_type: "base64" as const,
|
|
248
314
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import { act, render, renderHook, waitFor } from "@testing-library/react";
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
6
|
+
import type {
|
|
7
|
+
AssistantRuntime,
|
|
8
|
+
RemoteThreadListAdapter,
|
|
9
|
+
} from "@assistant-ui/core";
|
|
6
10
|
import { useAui } from "@assistant-ui/store";
|
|
7
11
|
import type { LangChainBaseMessage } from "./types";
|
|
8
12
|
import type { ReactNode } from "react";
|
|
@@ -110,6 +114,80 @@ const getText = (aui: ReturnType<typeof useAui>) =>
|
|
|
110
114
|
.join(""),
|
|
111
115
|
);
|
|
112
116
|
|
|
117
|
+
const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
|
|
118
|
+
list: vi.fn(async () => ({
|
|
119
|
+
threads: [
|
|
120
|
+
{
|
|
121
|
+
status: "regular" as const,
|
|
122
|
+
remoteId: "thread-a",
|
|
123
|
+
externalId: "thread-a",
|
|
124
|
+
title: "Thread A",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
status: "regular" as const,
|
|
128
|
+
remoteId: "thread-b",
|
|
129
|
+
externalId: "thread-b",
|
|
130
|
+
title: "Thread B",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
})),
|
|
134
|
+
initialize: vi.fn(async () => ({
|
|
135
|
+
remoteId: "thread-new",
|
|
136
|
+
externalId: "thread-new",
|
|
137
|
+
})),
|
|
138
|
+
rename: vi.fn(async () => {}),
|
|
139
|
+
archive: vi.fn(async () => {}),
|
|
140
|
+
unarchive: vi.fn(async () => {}),
|
|
141
|
+
delete: vi.fn(async () => {}),
|
|
142
|
+
generateTitle: vi.fn(async () => new ReadableStream()),
|
|
143
|
+
fetch: vi.fn(async (threadId) => ({
|
|
144
|
+
status: "regular" as const,
|
|
145
|
+
remoteId: threadId,
|
|
146
|
+
externalId: threadId,
|
|
147
|
+
})),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("useStreamRuntime thread options", () => {
|
|
151
|
+
it("keeps stream options isolated between mounted threads", async () => {
|
|
152
|
+
mockUseStream.mockReturnValue(createMockStream());
|
|
153
|
+
const capture: { runtime: AssistantRuntime | null } = { runtime: null };
|
|
154
|
+
const threadListAdapter = makeThreadListAdapter();
|
|
155
|
+
|
|
156
|
+
const TestRuntime = () => {
|
|
157
|
+
const runtime = useStreamRuntime({
|
|
158
|
+
apiUrl: "/api",
|
|
159
|
+
unstable_threadListAdapter: threadListAdapter,
|
|
160
|
+
} as never);
|
|
161
|
+
capture.runtime = runtime;
|
|
162
|
+
return <AssistantRuntimeProvider runtime={runtime} />;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const view = render(<TestRuntime />);
|
|
166
|
+
|
|
167
|
+
await act(async () => {
|
|
168
|
+
await capture.runtime!.threads.switchToThread("thread-a");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const threadAOptions = mockUseStream.mock.calls
|
|
172
|
+
.map(([options]) => options as { threadId?: string | null })
|
|
173
|
+
.findLast((options) => options.threadId === "thread-a");
|
|
174
|
+
expect(threadAOptions).toBeDefined();
|
|
175
|
+
|
|
176
|
+
await act(async () => {
|
|
177
|
+
await capture.runtime!.threads.switchToThread("thread-b");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const threadBOptions = mockUseStream.mock.calls
|
|
181
|
+
.map(([options]) => options as { threadId?: string | null })
|
|
182
|
+
.findLast((options) => options.threadId === "thread-b");
|
|
183
|
+
expect(threadBOptions).toBeDefined();
|
|
184
|
+
expect(threadAOptions).not.toBe(threadBOptions);
|
|
185
|
+
expect(threadAOptions?.threadId).toBe("thread-a");
|
|
186
|
+
|
|
187
|
+
view.unmount();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
113
191
|
describe("useStreamRuntime staged messages", () => {
|
|
114
192
|
it("stages a new user message without submitting when startRun is false", async () => {
|
|
115
193
|
const stream = createMockStream([message("u1", "human", "earlier")]);
|
package/src/useStreamRuntime.ts
CHANGED
|
@@ -125,12 +125,11 @@ const useStreamThreadRuntime = (
|
|
|
125
125
|
const externalId = useAuiState((s) => s.threadListItem.externalId) as
|
|
126
126
|
| string
|
|
127
127
|
| null;
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const stream = useStream(options);
|
|
128
|
+
// Object.assign preserves the discriminated transport union; object spread
|
|
129
|
+
// collapses its arms and no longer satisfies UseStreamOptions.
|
|
130
|
+
const stream = useStream(
|
|
131
|
+
Object.assign({}, options, { threadId: externalId }),
|
|
132
|
+
);
|
|
134
133
|
const [stagedMessages, setStagedMessages] = useState<
|
|
135
134
|
LangChainBaseMessage[] | null
|
|
136
135
|
>(null);
|