@assistant-ui/react-langchain 0.0.22 → 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.
|
@@ -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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"}
|
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,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
|
};
|