@assistant-ui/react-langchain 0.0.21 → 0.0.22

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.
@@ -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[]>;
@@ -50,6 +50,15 @@ declare const getMessageContent: (msg: AppendMessage) => string | ({
50
50
  text?: never;
51
51
  image_url?: never;
52
52
  url?: never;
53
+ } | {
54
+ type: "audio";
55
+ data: string;
56
+ mime_type: string;
57
+ source_type: "base64";
58
+ text?: never;
59
+ image_url?: never;
60
+ url?: never;
61
+ metadata?: never;
53
62
  })[];
54
63
  //#endregion
55
64
  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;cAyE3B,8BACX,SAAS,sBACT,WAAU,sCACT,4BAA4B;cAyFlB,oBAAqB,KAAK"}
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;cAuF3B,8BACX,SAAS,sBACT,WAAU,sCACT,4BAA4B;cAyFlB,oBAAqB,KAAK"}
@@ -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, role) => {
15
15
  if (typeof content === "string") return [{
16
16
  type: "text",
17
17
  text: content
@@ -37,6 +37,18 @@ const contentToParts = (content) => {
37
37
  data: part.source_type === "url" ? part.url : part.source_type === "id" ? part.id : part.data,
38
38
  mimeType: part.mime_type ?? "application/octet-stream"
39
39
  };
40
+ case "audio": {
41
+ if (role !== "user") return null;
42
+ const format = part.mime_type === "audio/wav" ? "wav" : part.mime_type === "audio/mp3" ? "mp3" : null;
43
+ if (!format) return null;
44
+ return {
45
+ type: "audio",
46
+ audio: {
47
+ data: part.data,
48
+ format
49
+ }
50
+ };
51
+ }
40
52
  case "thinking": return {
41
53
  type: "reasoning",
42
54
  text: part.thinking
@@ -70,7 +82,7 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
70
82
  case "human": return {
71
83
  role: "user",
72
84
  id: message.id,
73
- content: contentToParts(message.content),
85
+ content: contentToParts(message.content, "user"),
74
86
  metadata: { custom: getCustomMetadata(message.additional_kwargs) }
75
87
  };
76
88
  case "ai": {
@@ -88,7 +100,7 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
88
100
  role: "assistant",
89
101
  id: message.id,
90
102
  content: [
91
- ...contentToParts(message.content),
103
+ ...contentToParts(message.content, "assistant"),
92
104
  ...toolCallParts,
93
105
  ...uiDataParts
94
106
  ],
@@ -119,13 +131,13 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
119
131
  };
120
132
  const getMessageContent = (msg) => {
121
133
  const allContent = [...msg.content, ...msg.attachments?.flatMap((a) => a.content) ?? []];
122
- const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image");
134
+ const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image" || part.type === "audio");
123
135
  const hasText = allContent.some((part) => part.type === "text");
124
136
  if (hasNonText && !hasText) allContent.unshift({
125
137
  type: "text",
126
138
  text: " "
127
139
  });
128
- const content = allContent.map((part) => {
140
+ const content = allContent.flatMap((part) => {
129
141
  const type = part.type;
130
142
  switch (type) {
131
143
  case "text": return {
@@ -154,6 +166,13 @@ const getMessageContent = (msg) => {
154
166
  source_type: "base64"
155
167
  };
156
168
  }
169
+ case "audio": return {
170
+ type: "audio",
171
+ data: parseDataUrl(part.audio.data)?.data ?? part.audio.data,
172
+ mime_type: `audio/${part.audio.format}`,
173
+ source_type: "base64"
174
+ };
175
+ case "data": return [];
157
176
  case "tool-call": throw new Error("Tool call appends are not supported.");
158
177
  default: throw new Error(`Unsupported append message part type: ${type}`);
159
178
  }
@@ -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, 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,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/internal").RuntimeExtras<LangChainRuntimeExtras>;
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,uDAAe,cAAA"}
1
+ {"version":3,"file":"runtimeExtras.d.ts","names":[],"sources":["../src/runtimeExtras.ts"],"mappings":";;cAGa,oDAAe,cAAA"}
@@ -1,4 +1,4 @@
1
- import { createRuntimeExtras } from "@assistant-ui/core/internal";
1
+ import { createRuntimeExtras } from "@assistant-ui/core/react";
2
2
  //#region src/runtimeExtras.ts
3
3
  const langChainExtras = createRuntimeExtras("useStreamRuntime");
4
4
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"runtimeExtras.js","names":[],"sources":["../src/runtimeExtras.ts"],"sourcesContent":["import { createRuntimeExtras } from \"@assistant-ui/core/internal\";\nimport type { LangChainRuntimeExtras } from \"./types\";\n\nexport const langChainExtras =\n createRuntimeExtras<LangChainRuntimeExtras>(\"useStreamRuntime\");\n"],"mappings":";;AAGA,MAAa,kBACX,oBAA4C,kBAAkB"}
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
@@ -48,6 +48,11 @@ type LangChainContentBlock = {
48
48
  metadata?: {
49
49
  filename?: string;
50
50
  };
51
+ } | {
52
+ type: "audio";
53
+ data: string;
54
+ mime_type: string;
55
+ source_type: "base64";
51
56
  } | {
52
57
  type: "tool_use" | "input_json_delta";
53
58
  };
@@ -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;;;EAEb;;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"}
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"}
@@ -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.21",
3
+ "version": "0.0.22",
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.0",
33
- "@assistant-ui/store": "^0.3.0",
32
+ "@assistant-ui/core": "^0.3.2",
33
+ "@assistant-ui/store": "^0.3.2",
34
34
  "assistant-cloud": "*",
35
- "assistant-stream": "^0.3.29"
35
+ "assistant-stream": "^0.3.31"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@langchain/react": "^1.0.2",
@@ -202,6 +202,181 @@ describe("getMessageContent file blocks", () => {
202
202
  });
203
203
  });
204
204
 
205
+ describe("getMessageContent audio and data parts", () => {
206
+ const appendMessage = (...parts: Record<string, unknown>[]) =>
207
+ ({ content: parts }) as unknown as AppendMessage;
208
+
209
+ it("emits a base64 audio block with the format MIME type for audio parts", () => {
210
+ const content = getMessageContent(
211
+ appendMessage({
212
+ type: "audio",
213
+ audio: { data: "c291bmQ=", format: "mp3" },
214
+ }),
215
+ );
216
+
217
+ expect(content).toEqual([
218
+ { type: "text", text: " " },
219
+ {
220
+ type: "audio",
221
+ data: "c291bmQ=",
222
+ mime_type: "audio/mp3",
223
+ source_type: "base64",
224
+ },
225
+ ]);
226
+ });
227
+
228
+ it("strips a data URL envelope from audio data", () => {
229
+ const content = getMessageContent(
230
+ appendMessage({
231
+ type: "audio",
232
+ audio: { data: "data:audio/wav;base64,d2F2", format: "wav" },
233
+ }),
234
+ );
235
+
236
+ expect(content).toEqual([
237
+ { type: "text", text: " " },
238
+ {
239
+ type: "audio",
240
+ data: "d2F2",
241
+ mime_type: "audio/wav",
242
+ source_type: "base64",
243
+ },
244
+ ]);
245
+ });
246
+
247
+ it("keeps the format-derived MIME when a data URL carries a divergent one", () => {
248
+ const content = getMessageContent(
249
+ appendMessage({
250
+ type: "audio",
251
+ audio: { data: "data:audio/mpeg;base64,c291bmQ=", format: "mp3" },
252
+ }),
253
+ );
254
+
255
+ expect(content).toEqual([
256
+ { type: "text", text: " " },
257
+ {
258
+ type: "audio",
259
+ data: "c291bmQ=",
260
+ mime_type: "audio/mp3",
261
+ source_type: "base64",
262
+ },
263
+ ]);
264
+ });
265
+
266
+ it("does not prepend a second placeholder when text accompanies audio", () => {
267
+ const content = getMessageContent(
268
+ appendMessage(
269
+ { type: "text", text: "listen" },
270
+ { type: "audio", audio: { data: "d2F2", format: "wav" } },
271
+ ),
272
+ );
273
+
274
+ expect(content).toEqual([
275
+ { type: "text", text: "listen" },
276
+ {
277
+ type: "audio",
278
+ data: "d2F2",
279
+ mime_type: "audio/wav",
280
+ source_type: "base64",
281
+ },
282
+ ]);
283
+ });
284
+
285
+ it("drops data parts while keeping the rest of the message", () => {
286
+ const content = getMessageContent(
287
+ appendMessage(
288
+ { type: "text", text: "hi" },
289
+ { type: "data", name: "chart", data: { values: [1, 2] } },
290
+ ),
291
+ );
292
+
293
+ expect(content).toBe("hi");
294
+ });
295
+
296
+ it("returns empty content for a data-only message", () => {
297
+ const content = getMessageContent(
298
+ appendMessage({ type: "data", name: "chart", data: { values: [1, 2] } }),
299
+ );
300
+
301
+ expect(content).toEqual([]);
302
+ });
303
+
304
+ it("still throws on assistant-only part types", () => {
305
+ expect(() =>
306
+ getMessageContent(appendMessage({ type: "reasoning", text: "hmm" })),
307
+ ).toThrow("Unsupported append message part type: reasoning");
308
+ });
309
+ });
310
+
311
+ describe("contentToParts audio blocks", () => {
312
+ it("converts an inbound base64 audio block back to an audio part", () => {
313
+ const result = convertLangChainBaseMessage(
314
+ {
315
+ _getType: () => "human",
316
+ id: "h1",
317
+ content: [
318
+ {
319
+ type: "audio",
320
+ data: "c291bmQ=",
321
+ mime_type: "audio/mp3",
322
+ source_type: "base64",
323
+ },
324
+ ],
325
+ },
326
+ {},
327
+ );
328
+
329
+ expect(result).toMatchObject({
330
+ role: "user",
331
+ content: [{ type: "audio", audio: { data: "c291bmQ=", format: "mp3" } }],
332
+ });
333
+ });
334
+
335
+ it("drops an inbound audio block with an unrepresentable mime type", () => {
336
+ const result = convertLangChainBaseMessage(
337
+ {
338
+ _getType: () => "human",
339
+ id: "h1",
340
+ content: [
341
+ {
342
+ type: "audio",
343
+ data: "b2dn",
344
+ mime_type: "audio/ogg",
345
+ source_type: "base64",
346
+ },
347
+ ],
348
+ },
349
+ {},
350
+ );
351
+
352
+ expect(result).toMatchObject({ role: "user", content: [] });
353
+ });
354
+
355
+ it("drops an audio block on an assistant message", () => {
356
+ const result = convertLangChainBaseMessage(
357
+ {
358
+ _getType: () => "ai",
359
+ id: "ai-1",
360
+ content: [
361
+ {
362
+ type: "audio",
363
+ data: "c291bmQ=",
364
+ mime_type: "audio/mp3",
365
+ source_type: "base64",
366
+ },
367
+ { type: "text", text: "done" },
368
+ ],
369
+ },
370
+ {},
371
+ );
372
+
373
+ expect(result).toMatchObject({
374
+ role: "assistant",
375
+ content: [{ type: "text", text: "done" }],
376
+ });
377
+ });
378
+ });
379
+
205
380
  describe("convertLangChainBaseMessage reasoning content parts", () => {
206
381
  it("joins summary parts into a single reasoning part", () => {
207
382
  const result = convertLangChainBaseMessage(
@@ -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, role: "user" | "assistant") => {
37
37
  if (typeof content === "string")
38
38
  return [{ type: "text" as const, text: content }];
39
39
 
@@ -65,6 +65,20 @@ const contentToParts = (content: unknown) => {
65
65
  : part.data,
66
66
  mimeType: part.mime_type ?? "application/octet-stream",
67
67
  };
68
+ case "audio": {
69
+ if (role !== "user") return null;
70
+ const format =
71
+ part.mime_type === "audio/wav"
72
+ ? ("wav" as const)
73
+ : part.mime_type === "audio/mp3"
74
+ ? ("mp3" as const)
75
+ : null;
76
+ if (!format) return null;
77
+ return {
78
+ type: "audio" as const,
79
+ audio: { data: part.data, format },
80
+ };
81
+ }
68
82
  case "thinking":
69
83
  return { type: "reasoning" as const, text: part.thinking };
70
84
  case "reasoning":
@@ -120,7 +134,7 @@ export const convertLangChainBaseMessage = (
120
134
  return {
121
135
  role: "user",
122
136
  id: message.id,
123
- content: contentToParts(message.content),
137
+ content: contentToParts(message.content, "user"),
124
138
  metadata: {
125
139
  custom: getCustomMetadata(message.additional_kwargs),
126
140
  },
@@ -152,7 +166,7 @@ export const convertLangChainBaseMessage = (
152
166
  role: "assistant",
153
167
  id: message.id,
154
168
  content: [
155
- ...contentToParts(message.content),
169
+ ...contentToParts(message.content, "assistant"),
156
170
  ...toolCallParts,
157
171
  ...uiDataParts,
158
172
  ],
@@ -198,14 +212,15 @@ export const getMessageContent = (msg: AppendMessage) => {
198
212
  ];
199
213
 
200
214
  const hasNonText = allContent.some(
201
- (part) => part.type === "file" || part.type === "image",
215
+ (part) =>
216
+ part.type === "file" || part.type === "image" || part.type === "audio",
202
217
  );
203
218
  const hasText = allContent.some((part) => part.type === "text");
204
219
  if (hasNonText && !hasText) {
205
220
  allContent.unshift({ type: "text", text: " " });
206
221
  }
207
222
 
208
- const content = allContent.map((part) => {
223
+ const content = allContent.flatMap((part) => {
209
224
  const type = part.type;
210
225
  switch (type) {
211
226
  case "text":
@@ -232,15 +247,21 @@ export const getMessageContent = (msg: AppendMessage) => {
232
247
  source_type: "base64" as const,
233
248
  };
234
249
  }
250
+ case "audio": {
251
+ const parsed = parseDataUrl(part.audio.data);
252
+ return {
253
+ type: "audio" as const,
254
+ data: parsed?.data ?? part.audio.data,
255
+ mime_type: `audio/${part.audio.format}`,
256
+ source_type: "base64" as const,
257
+ };
258
+ }
259
+ case "data":
260
+ return [];
235
261
  case "tool-call":
236
262
  throw new Error("Tool call appends are not supported.");
237
263
  default: {
238
- const _exhaustiveCheck:
239
- | "reasoning"
240
- | "source"
241
- | "audio"
242
- | "data"
243
- | "generative-ui" = type;
264
+ const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
244
265
  throw new Error(
245
266
  `Unsupported append message part type: ${_exhaustiveCheck}`,
246
267
  );
@@ -1,4 +1,4 @@
1
- import { createRuntimeExtras } from "@assistant-ui/core/internal";
1
+ import { createRuntimeExtras } from "@assistant-ui/core/react";
2
2
  import type { LangChainRuntimeExtras } from "./types";
3
3
 
4
4
  export const langChainExtras =
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 = {