@assistant-ui/react-langchain 0.0.21 → 0.0.23
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 +35 -3
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +69 -5
- package/dist/convertMessages.js.map +1 -1
- package/dist/runtimeExtras.d.ts +1 -1
- package/dist/runtimeExtras.d.ts.map +1 -1
- package/dist/runtimeExtras.js +1 -1
- package/dist/runtimeExtras.js.map +1 -1
- package/dist/streamingTiming.d.ts +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +1 -1
- package/package.json +4 -4
- package/src/convertMessages.test.ts +602 -0
- package/src/convertMessages.ts +97 -10
- package/src/runtimeExtras.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LangChainBaseMessage, UIMessage } from "./types.js";
|
|
2
|
-
import { AppendMessage, MessageTiming } from "@assistant-ui/core";
|
|
3
2
|
import { useExternalMessageConverter } from "@assistant-ui/core/react";
|
|
3
|
+
import { AppendMessage, MessageTiming } from "@assistant-ui/core";
|
|
4
4
|
//#region src/convertMessages.d.ts
|
|
5
5
|
type LangChainMessageConverterMetadata = useExternalMessageConverter.Metadata & {
|
|
6
6
|
uiMessagesByParent?: Map<string, UIMessage[]>;
|
|
@@ -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,62 @@ 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;
|
|
73
|
+
url?: never;
|
|
74
|
+
} | {
|
|
75
|
+
type: "audio";
|
|
76
|
+
data: string;
|
|
77
|
+
mime_type: string;
|
|
78
|
+
source_type: "base64";
|
|
79
|
+
text?: never;
|
|
80
|
+
image_url?: never;
|
|
81
|
+
id?: never;
|
|
82
|
+
filename?: never;
|
|
83
|
+
metadata?: never;
|
|
52
84
|
url?: never;
|
|
53
85
|
})[];
|
|
54
86
|
//#endregion
|
|
@@ -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
|
@@ -35,8 +35,19 @@ const contentToParts = (content) => {
|
|
|
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
|
};
|
|
41
|
+
case "audio": {
|
|
42
|
+
const mimeType = part.mime_type ?? "application/octet-stream";
|
|
43
|
+
const subtype = mimeType.startsWith("audio/") ? mimeType.slice(6) : void 0;
|
|
44
|
+
return {
|
|
45
|
+
type: "file",
|
|
46
|
+
filename: subtype ? `audio.${subtype}` : "audio",
|
|
47
|
+
data: part.data,
|
|
48
|
+
mimeType
|
|
49
|
+
};
|
|
50
|
+
}
|
|
40
51
|
case "thinking": return {
|
|
41
52
|
type: "reasoning",
|
|
42
53
|
text: part.thinking
|
|
@@ -51,6 +62,22 @@ const contentToParts = (content) => {
|
|
|
51
62
|
}
|
|
52
63
|
}).filter((p) => p !== null);
|
|
53
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
|
+
};
|
|
54
81
|
const getCustomMetadata = (additionalKwargs) => additionalKwargs?.metadata ?? {};
|
|
55
82
|
const getStringContent = (content) => {
|
|
56
83
|
if (typeof content === "string") return content;
|
|
@@ -88,7 +115,7 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
|
|
|
88
115
|
role: "assistant",
|
|
89
116
|
id: message.id,
|
|
90
117
|
content: [
|
|
91
|
-
...contentToParts(message.content),
|
|
118
|
+
...withAudioTranscript(contentToParts(message.content), message.additional_kwargs),
|
|
92
119
|
...toolCallParts,
|
|
93
120
|
...uiDataParts
|
|
94
121
|
],
|
|
@@ -117,15 +144,28 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
|
|
|
117
144
|
};
|
|
118
145
|
}
|
|
119
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
|
+
]);
|
|
120
160
|
const getMessageContent = (msg) => {
|
|
121
161
|
const allContent = [...msg.content, ...msg.attachments?.flatMap((a) => a.content) ?? []];
|
|
122
|
-
const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image");
|
|
162
|
+
const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image" || part.type === "audio");
|
|
123
163
|
const hasText = allContent.some((part) => part.type === "text");
|
|
124
164
|
if (hasNonText && !hasText) allContent.unshift({
|
|
125
165
|
type: "text",
|
|
126
166
|
text: " "
|
|
127
167
|
});
|
|
128
|
-
const content = allContent.
|
|
168
|
+
const content = allContent.flatMap((part) => {
|
|
129
169
|
const type = part.type;
|
|
130
170
|
switch (type) {
|
|
131
171
|
case "text": return {
|
|
@@ -138,22 +178,46 @@ const getMessageContent = (msg) => {
|
|
|
138
178
|
};
|
|
139
179
|
case "file": {
|
|
140
180
|
const metadata = { filename: part.filename ?? "file" };
|
|
141
|
-
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 {
|
|
142
190
|
type: "file",
|
|
143
191
|
url: part.data,
|
|
144
192
|
mime_type: part.mimeType,
|
|
193
|
+
filename: metadata.filename,
|
|
145
194
|
metadata,
|
|
146
195
|
source_type: "url"
|
|
147
196
|
};
|
|
148
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
|
+
};
|
|
149
205
|
return {
|
|
150
206
|
type: "file",
|
|
151
207
|
data: parsed?.data ?? part.data,
|
|
152
208
|
mime_type: parsed?.mimeType ?? part.mimeType,
|
|
209
|
+
filename: metadata.filename,
|
|
153
210
|
metadata,
|
|
154
211
|
source_type: "base64"
|
|
155
212
|
};
|
|
156
213
|
}
|
|
214
|
+
case "audio": return {
|
|
215
|
+
type: "audio",
|
|
216
|
+
data: parseDataUrl(part.audio.data)?.data ?? part.audio.data,
|
|
217
|
+
mime_type: `audio/${part.audio.format}`,
|
|
218
|
+
source_type: "base64"
|
|
219
|
+
};
|
|
220
|
+
case "data": return [];
|
|
157
221
|
case "tool-call": throw new Error("Tool call appends are not supported.");
|
|
158
222
|
default: throw new Error(`Unsupported append message part type: ${type}`);
|
|
159
223
|
}
|
|
@@ -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) => {\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 \"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),\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),\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) => part.type === \"file\" || part.type === \"image\",\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.map((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 \"tool-call\":\n throw new Error(\"Tool call appends are not supported.\");\n default: {\n const _exhaustiveCheck:\n | \"reasoning\"\n | \"source\"\n | \"audio\"\n | \"data\"\n | \"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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"}
|
package/dist/runtimeExtras.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LangChainRuntimeExtras } from "./types.js";
|
|
2
2
|
//#region src/runtimeExtras.d.ts
|
|
3
|
-
declare const langChainExtras: import("@assistant-ui/core/
|
|
3
|
+
declare const langChainExtras: import("@assistant-ui/core/react").RuntimeExtras<LangChainRuntimeExtras>;
|
|
4
4
|
//#endregion
|
|
5
5
|
export { langChainExtras };
|
|
6
6
|
//# sourceMappingURL=runtimeExtras.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimeExtras.d.ts","names":[],"sources":["../src/runtimeExtras.ts"],"mappings":";;cAGa,
|
|
1
|
+
{"version":3,"file":"runtimeExtras.d.ts","names":[],"sources":["../src/runtimeExtras.ts"],"mappings":";;cAGa,oDAAe,cAAA"}
|
package/dist/runtimeExtras.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimeExtras.js","names":[],"sources":["../src/runtimeExtras.ts"],"sourcesContent":["import { createRuntimeExtras } from \"@assistant-ui/core/
|
|
1
|
+
{"version":3,"file":"runtimeExtras.js","names":[],"sources":["../src/runtimeExtras.ts"],"sourcesContent":["import { createRuntimeExtras } from \"@assistant-ui/core/react\";\nimport type { LangChainRuntimeExtras } from \"./types\";\n\nexport const langChainExtras =\n createRuntimeExtras<LangChainRuntimeExtras>(\"useStreamRuntime\");\n"],"mappings":";;AAGA,MAAa,kBACX,oBAA4C,kBAAkB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LangChainBaseMessage } from "./types.js";
|
|
2
|
-
import { MessageTiming } from "@assistant-ui/core";
|
|
3
2
|
import { StreamingTimingAccessors } from "@assistant-ui/core/react";
|
|
3
|
+
import { MessageTiming } from "@assistant-ui/core";
|
|
4
4
|
//#region src/streamingTiming.d.ts
|
|
5
5
|
declare const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage>;
|
|
6
6
|
/**
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAoBY;EACN;EAAc;;EACd;EAAoB;;EACpB;EAAmB;IAAsB;;;EACzC;EAAkB;;EAElB;EACA,UAAU;IAAQ;IAAsB;;EACxC;;EAGA;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;EACA;IAAa;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAoBY;EACN;EAAc;;EACd;EAAoB;;EACpB;EAAmB;IAAsB;;;EACzC;EAAkB;;EAElB;EACA,UAAU;IAAQ;IAAsB;;EACxC;;EAGA;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;;EAEA;;KAEM;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"}
|
package/dist/useStreamRuntime.js
CHANGED
|
@@ -5,9 +5,9 @@ import { foldUIUpdates, mergeUIMessages } from "./uiMessages.js";
|
|
|
5
5
|
import { resolveForkCheckpoint } from "./resolveForkCheckpoint.js";
|
|
6
6
|
import { useLangChainStreamingTiming } from "./streamingTiming.js";
|
|
7
7
|
import { useAuiState } from "@assistant-ui/store";
|
|
8
|
+
import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
8
9
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
10
|
import { generateId, getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
10
|
-
import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
11
11
|
import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
|
|
12
12
|
//#region src/useStreamRuntime.ts
|
|
13
13
|
const UI_CUSTOM_CHANNELS = ["custom"];
|
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.23",
|
|
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.4",
|
|
33
|
+
"@assistant-ui/store": "^0.3.3",
|
|
34
34
|
"assistant-cloud": "*",
|
|
35
|
-
"assistant-stream": "^0.3.
|
|
35
|
+
"assistant-stream": "^0.3.32"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@langchain/react": "^1.0.2",
|
|
@@ -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,509 @@ 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
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe("getMessageContent audio and data parts", () => {
|
|
451
|
+
const appendMessage = (...parts: Record<string, unknown>[]) =>
|
|
452
|
+
({ content: parts }) as unknown as AppendMessage;
|
|
453
|
+
|
|
454
|
+
it("emits a base64 audio block with the format MIME type for audio parts", () => {
|
|
455
|
+
const content = getMessageContent(
|
|
456
|
+
appendMessage({
|
|
457
|
+
type: "audio",
|
|
458
|
+
audio: { data: "c291bmQ=", format: "mp3" },
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
expect(content).toEqual([
|
|
463
|
+
{ type: "text", text: " " },
|
|
464
|
+
{
|
|
465
|
+
type: "audio",
|
|
466
|
+
data: "c291bmQ=",
|
|
467
|
+
mime_type: "audio/mp3",
|
|
468
|
+
source_type: "base64",
|
|
469
|
+
},
|
|
470
|
+
]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("strips a data URL envelope from audio data", () => {
|
|
474
|
+
const content = getMessageContent(
|
|
475
|
+
appendMessage({
|
|
476
|
+
type: "audio",
|
|
477
|
+
audio: { data: "data:audio/wav;base64,d2F2", format: "wav" },
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
expect(content).toEqual([
|
|
482
|
+
{ type: "text", text: " " },
|
|
483
|
+
{
|
|
484
|
+
type: "audio",
|
|
485
|
+
data: "d2F2",
|
|
486
|
+
mime_type: "audio/wav",
|
|
487
|
+
source_type: "base64",
|
|
488
|
+
},
|
|
489
|
+
]);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("keeps the format-derived MIME when a data URL carries a divergent one", () => {
|
|
493
|
+
const content = getMessageContent(
|
|
494
|
+
appendMessage({
|
|
495
|
+
type: "audio",
|
|
496
|
+
audio: { data: "data:audio/mpeg;base64,c291bmQ=", format: "mp3" },
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
expect(content).toEqual([
|
|
501
|
+
{ type: "text", text: " " },
|
|
502
|
+
{
|
|
503
|
+
type: "audio",
|
|
504
|
+
data: "c291bmQ=",
|
|
505
|
+
mime_type: "audio/mp3",
|
|
506
|
+
source_type: "base64",
|
|
507
|
+
},
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("does not prepend a second placeholder when text accompanies audio", () => {
|
|
512
|
+
const content = getMessageContent(
|
|
513
|
+
appendMessage(
|
|
514
|
+
{ type: "text", text: "listen" },
|
|
515
|
+
{ type: "audio", audio: { data: "d2F2", format: "wav" } },
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(content).toEqual([
|
|
520
|
+
{ type: "text", text: "listen" },
|
|
521
|
+
{
|
|
522
|
+
type: "audio",
|
|
523
|
+
data: "d2F2",
|
|
524
|
+
mime_type: "audio/wav",
|
|
525
|
+
source_type: "base64",
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("drops data parts while keeping the rest of the message", () => {
|
|
531
|
+
const content = getMessageContent(
|
|
532
|
+
appendMessage(
|
|
533
|
+
{ type: "text", text: "hi" },
|
|
534
|
+
{ type: "data", name: "chart", data: { values: [1, 2] } },
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
expect(content).toBe("hi");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("returns empty content for a data-only message", () => {
|
|
542
|
+
const content = getMessageContent(
|
|
543
|
+
appendMessage({ type: "data", name: "chart", data: { values: [1, 2] } }),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
expect(content).toEqual([]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("still throws on assistant-only part types", () => {
|
|
550
|
+
expect(() =>
|
|
551
|
+
getMessageContent(appendMessage({ type: "reasoning", text: "hmm" })),
|
|
552
|
+
).toThrow("Unsupported append message part type: reasoning");
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
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
|
+
|
|
569
|
+
it("converts an inbound base64 audio block back to an audio part", () => {
|
|
570
|
+
const result = convertLangChainBaseMessage(
|
|
571
|
+
{
|
|
572
|
+
_getType: () => "human",
|
|
573
|
+
id: "h1",
|
|
574
|
+
content: [
|
|
575
|
+
{
|
|
576
|
+
type: "audio",
|
|
577
|
+
data: "c291bmQ=",
|
|
578
|
+
mime_type: "audio/mp3",
|
|
579
|
+
source_type: "base64",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
},
|
|
583
|
+
{},
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
expect(result).toMatchObject({
|
|
587
|
+
role: "user",
|
|
588
|
+
content: [
|
|
589
|
+
{
|
|
590
|
+
type: "file",
|
|
591
|
+
filename: "audio.mp3",
|
|
592
|
+
data: "c291bmQ=",
|
|
593
|
+
mimeType: "audio/mp3",
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("keeps an inbound audio block whose mime type has no wire format", () => {
|
|
600
|
+
const result = convertLangChainBaseMessage(
|
|
601
|
+
{
|
|
602
|
+
_getType: () => "human",
|
|
603
|
+
id: "h1",
|
|
604
|
+
content: [
|
|
605
|
+
{
|
|
606
|
+
type: "audio",
|
|
607
|
+
data: "b2dn",
|
|
608
|
+
mime_type: "audio/ogg",
|
|
609
|
+
source_type: "base64",
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
{},
|
|
614
|
+
);
|
|
615
|
+
|
|
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
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("keeps an audio block on an assistant message", () => {
|
|
630
|
+
const result = convertLangChainBaseMessage(
|
|
631
|
+
{
|
|
632
|
+
_getType: () => "ai",
|
|
633
|
+
id: "ai-1",
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: "audio",
|
|
637
|
+
data: "c291bmQ=",
|
|
638
|
+
mime_type: "audio/mp3",
|
|
639
|
+
source_type: "base64",
|
|
640
|
+
},
|
|
641
|
+
{ type: "text", text: "done" },
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
{},
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
expect(result).toMatchObject({
|
|
648
|
+
role: "assistant",
|
|
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",
|
|
704
|
+
});
|
|
705
|
+
});
|
|
203
706
|
});
|
|
204
707
|
|
|
205
708
|
describe("convertLangChainBaseMessage reasoning content parts", () => {
|
|
@@ -346,3 +849,102 @@ describe("convertLangChainBaseMessage image content parts", () => {
|
|
|
346
849
|
expect(contentOf(result)).toEqual([]);
|
|
347
850
|
});
|
|
348
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
|
@@ -64,7 +64,22 @@ const contentToParts = (content: unknown) => {
|
|
|
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
|
};
|
|
71
|
+
case "audio": {
|
|
72
|
+
const mimeType = part.mime_type ?? "application/octet-stream";
|
|
73
|
+
const subtype = mimeType.startsWith("audio/")
|
|
74
|
+
? mimeType.slice("audio/".length)
|
|
75
|
+
: undefined;
|
|
76
|
+
return {
|
|
77
|
+
type: "file" as const,
|
|
78
|
+
filename: subtype ? `audio.${subtype}` : "audio",
|
|
79
|
+
data: part.data,
|
|
80
|
+
mimeType,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
68
83
|
case "thinking":
|
|
69
84
|
return { type: "reasoning" as const, text: part.thinking };
|
|
70
85
|
case "reasoning":
|
|
@@ -85,6 +100,31 @@ const contentToParts = (content: unknown) => {
|
|
|
85
100
|
.filter((p) => p !== null);
|
|
86
101
|
};
|
|
87
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
|
+
|
|
88
128
|
const getCustomMetadata = (
|
|
89
129
|
additionalKwargs: Record<string, unknown> | undefined,
|
|
90
130
|
): Record<string, unknown> =>
|
|
@@ -152,7 +192,10 @@ export const convertLangChainBaseMessage = (
|
|
|
152
192
|
role: "assistant",
|
|
153
193
|
id: message.id,
|
|
154
194
|
content: [
|
|
155
|
-
...
|
|
195
|
+
...withAudioTranscript(
|
|
196
|
+
contentToParts(message.content),
|
|
197
|
+
message.additional_kwargs,
|
|
198
|
+
),
|
|
156
199
|
...toolCallParts,
|
|
157
200
|
...uiDataParts,
|
|
158
201
|
],
|
|
@@ -191,6 +234,20 @@ export const convertLangChainBaseMessage = (
|
|
|
191
234
|
}
|
|
192
235
|
};
|
|
193
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
|
+
|
|
194
251
|
export const getMessageContent = (msg: AppendMessage) => {
|
|
195
252
|
const allContent = [
|
|
196
253
|
...msg.content,
|
|
@@ -198,14 +255,15 @@ export const getMessageContent = (msg: AppendMessage) => {
|
|
|
198
255
|
];
|
|
199
256
|
|
|
200
257
|
const hasNonText = allContent.some(
|
|
201
|
-
(part) =>
|
|
258
|
+
(part) =>
|
|
259
|
+
part.type === "file" || part.type === "image" || part.type === "audio",
|
|
202
260
|
);
|
|
203
261
|
const hasText = allContent.some((part) => part.type === "text");
|
|
204
262
|
if (hasNonText && !hasText) {
|
|
205
263
|
allContent.unshift({ type: "text", text: " " });
|
|
206
264
|
}
|
|
207
265
|
|
|
208
|
-
const content = allContent.
|
|
266
|
+
const content = allContent.flatMap((part) => {
|
|
209
267
|
const type = part.type;
|
|
210
268
|
switch (type) {
|
|
211
269
|
case "text":
|
|
@@ -214,33 +272,62 @@ export const getMessageContent = (msg: AppendMessage) => {
|
|
|
214
272
|
return { type: "image_url" as const, image_url: { url: part.image } };
|
|
215
273
|
case "file": {
|
|
216
274
|
const metadata = { filename: part.filename ?? "file" };
|
|
217
|
-
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)) {
|
|
218
286
|
return {
|
|
219
287
|
type: "file" as const,
|
|
220
288
|
url: part.data,
|
|
221
289
|
mime_type: part.mimeType,
|
|
290
|
+
filename: metadata.filename,
|
|
222
291
|
metadata,
|
|
223
292
|
source_type: "url" as const,
|
|
224
293
|
};
|
|
225
294
|
}
|
|
226
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
|
+
}
|
|
227
307
|
return {
|
|
228
308
|
type: "file" as const,
|
|
229
309
|
data: parsed?.data ?? part.data,
|
|
230
310
|
mime_type: parsed?.mimeType ?? part.mimeType,
|
|
311
|
+
filename: metadata.filename,
|
|
231
312
|
metadata,
|
|
232
313
|
source_type: "base64" as const,
|
|
233
314
|
};
|
|
234
315
|
}
|
|
316
|
+
case "audio": {
|
|
317
|
+
const parsed = parseDataUrl(part.audio.data);
|
|
318
|
+
return {
|
|
319
|
+
type: "audio" as const,
|
|
320
|
+
data: parsed?.data ?? part.audio.data,
|
|
321
|
+
mime_type: `audio/${part.audio.format}`,
|
|
322
|
+
source_type: "base64" as const,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case "data":
|
|
326
|
+
return [];
|
|
235
327
|
case "tool-call":
|
|
236
328
|
throw new Error("Tool call appends are not supported.");
|
|
237
329
|
default: {
|
|
238
|
-
const _exhaustiveCheck:
|
|
239
|
-
| "reasoning"
|
|
240
|
-
| "source"
|
|
241
|
-
| "audio"
|
|
242
|
-
| "data"
|
|
243
|
-
| "generative-ui" = type;
|
|
330
|
+
const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
|
|
244
331
|
throw new Error(
|
|
245
332
|
`Unsupported append message part type: ${_exhaustiveCheck}`,
|
|
246
333
|
);
|
package/src/runtimeExtras.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -49,6 +49,12 @@ export type LangChainContentBlock =
|
|
|
49
49
|
source_type: "id";
|
|
50
50
|
metadata?: { filename?: string };
|
|
51
51
|
}
|
|
52
|
+
| {
|
|
53
|
+
type: "audio";
|
|
54
|
+
data: string;
|
|
55
|
+
mime_type: string;
|
|
56
|
+
source_type: "base64";
|
|
57
|
+
}
|
|
52
58
|
| { type: "tool_use" | "input_json_delta" };
|
|
53
59
|
|
|
54
60
|
export type LangChainToolCall = {
|