@assistant-ui/react-langchain 0.0.20 → 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.
- package/dist/convertMessages.d.ts +10 -1
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +24 -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 +5 -5
- package/src/convertMessages.test.ts +175 -0
- package/src/convertMessages.ts +32 -11
- package/src/runtimeExtras.ts +1 -1
- package/src/types.ts +6 -0
- package/src/useStreamRuntime.test.tsx +12 -15
- package/src/runConfigToSubmitOptions.test.ts +0 -24
- package/src/streamingTiming.test.ts +0 -118
- package/src/useLangChainError.test.tsx +0 -48
- package/src/useLangChainInterrupts.test.tsx +0 -48
- package/src/useLangChainRespond.test.tsx +0 -49
- package/src/useLangChainRespondAll.test.tsx +0 -52
- package/src/useLangChainStream.test.tsx +0 -42
- package/src/useLangChainSubagents.test.tsx +0 -48
- package/src/useLangChainSubgraphs.test.tsx +0 -48
- package/src/useLangChainToolCalls.test.tsx +0 -48
|
@@ -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;
|
|
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"}
|
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, 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.
|
|
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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"}
|
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.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.2
|
|
33
|
-
"@assistant-ui/store": "^0.2
|
|
32
|
+
"@assistant-ui/core": "^0.3.2",
|
|
33
|
+
"@assistant-ui/store": "^0.3.2",
|
|
34
34
|
"assistant-cloud": "*",
|
|
35
|
-
"assistant-stream": "^0.3.
|
|
35
|
+
"assistant-stream": "^0.3.31"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@langchain/react": "^1.0.2",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"jsdom": "^29.1.1",
|
|
54
54
|
"react": "^19.2.8",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
56
|
+
"@assistant-ui/x-buildutils": "0.0.20"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public",
|
|
@@ -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(
|
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, 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) =>
|
|
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.
|
|
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
|
);
|
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 = {
|
|
@@ -103,15 +103,12 @@ const renderAui = (stream: MockStream) => {
|
|
|
103
103
|
};
|
|
104
104
|
|
|
105
105
|
const getText = (aui: ReturnType<typeof useAui>) =>
|
|
106
|
-
aui
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.map((part) => part.text)
|
|
113
|
-
.join(""),
|
|
114
|
-
);
|
|
106
|
+
aui.thread.getState().messages.map((m) =>
|
|
107
|
+
m.content
|
|
108
|
+
.filter((part) => part.type === "text")
|
|
109
|
+
.map((part) => part.text)
|
|
110
|
+
.join(""),
|
|
111
|
+
);
|
|
115
112
|
|
|
116
113
|
describe("useStreamRuntime staged messages", () => {
|
|
117
114
|
it("stages a new user message without submitting when startRun is false", async () => {
|
|
@@ -119,7 +116,7 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
119
116
|
const { auiResult } = renderAui(stream);
|
|
120
117
|
|
|
121
118
|
await act(async () => {
|
|
122
|
-
auiResult.current.thread
|
|
119
|
+
auiResult.current.thread.append({
|
|
123
120
|
role: "user",
|
|
124
121
|
content: [{ type: "text", text: "draft" }],
|
|
125
122
|
startRun: false,
|
|
@@ -141,7 +138,7 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
141
138
|
const { auiResult, rerender } = renderAui(stream);
|
|
142
139
|
|
|
143
140
|
await act(async () => {
|
|
144
|
-
auiResult.current.thread
|
|
141
|
+
auiResult.current.thread.append({
|
|
145
142
|
role: "user",
|
|
146
143
|
parentId: "u1",
|
|
147
144
|
content: [{ type: "text", text: "edited" }],
|
|
@@ -171,12 +168,12 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
171
168
|
const { auiResult, rerender } = renderAui(stream);
|
|
172
169
|
|
|
173
170
|
await act(async () => {
|
|
174
|
-
auiResult.current.thread
|
|
171
|
+
auiResult.current.thread.append({
|
|
175
172
|
role: "user",
|
|
176
173
|
content: [{ type: "text", text: "first staged" }],
|
|
177
174
|
startRun: false,
|
|
178
175
|
});
|
|
179
|
-
auiResult.current.thread
|
|
176
|
+
auiResult.current.thread.append({
|
|
180
177
|
role: "user",
|
|
181
178
|
content: [{ type: "text", text: "second staged" }],
|
|
182
179
|
startRun: false,
|
|
@@ -191,9 +188,9 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
191
188
|
]);
|
|
192
189
|
});
|
|
193
190
|
|
|
194
|
-
const firstStagedId = auiResult.current.thread
|
|
191
|
+
const firstStagedId = auiResult.current.thread.getState().messages[1]!.id;
|
|
195
192
|
await act(async () => {
|
|
196
|
-
await auiResult.current.thread
|
|
193
|
+
await auiResult.current.thread.startRun({
|
|
197
194
|
parentId: firstStagedId,
|
|
198
195
|
sourceId: null,
|
|
199
196
|
runConfig: {},
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { AppendMessage } from "@assistant-ui/core";
|
|
3
|
-
import { runConfigToSubmitOptions } from "./useStreamRuntime";
|
|
4
|
-
|
|
5
|
-
type RunConfig = AppendMessage["runConfig"];
|
|
6
|
-
|
|
7
|
-
describe("runConfigToSubmitOptions", () => {
|
|
8
|
-
it("returns undefined when runConfig is undefined", () => {
|
|
9
|
-
expect(runConfigToSubmitOptions(undefined)).toBeUndefined();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns undefined when custom is undefined", () => {
|
|
13
|
-
expect(
|
|
14
|
-
runConfigToSubmitOptions({ custom: undefined } as unknown as RunConfig),
|
|
15
|
-
).toBeUndefined();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("maps custom to config.configurable", () => {
|
|
19
|
-
const runConfig = { custom: { mode: "plan" } } as RunConfig;
|
|
20
|
-
expect(runConfigToSubmitOptions(runConfig)).toEqual({
|
|
21
|
-
config: { configurable: { mode: "plan" } },
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { LangChainBaseMessage } from "./types";
|
|
3
|
-
import { langChainStreamingTimingAccessors } from "./streamingTiming";
|
|
4
|
-
|
|
5
|
-
const ai = (
|
|
6
|
-
fields: Partial<LangChainBaseMessage> & { id: string; content: unknown },
|
|
7
|
-
): LangChainBaseMessage => ({
|
|
8
|
-
_getType: () => "ai",
|
|
9
|
-
...fields,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const human = (id: string, content: unknown): LangChainBaseMessage => ({
|
|
13
|
-
_getType: () => "human",
|
|
14
|
-
id,
|
|
15
|
-
content,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const { getAssistantMessageId, getTextLength, getToolCallCount } =
|
|
19
|
-
langChainStreamingTimingAccessors;
|
|
20
|
-
|
|
21
|
-
describe("langChainStreamingTimingAccessors", () => {
|
|
22
|
-
it("resolves the last assistant message id via _getType()", () => {
|
|
23
|
-
expect(
|
|
24
|
-
getAssistantMessageId([human("u1", "hi"), ai({ id: "a1", content: "" })]),
|
|
25
|
-
).toBe("a1");
|
|
26
|
-
expect(
|
|
27
|
-
getAssistantMessageId([
|
|
28
|
-
ai({ id: "a1", content: "" }),
|
|
29
|
-
human("u2", "hi"),
|
|
30
|
-
ai({ id: "a2", content: "" }),
|
|
31
|
-
]),
|
|
32
|
-
).toBe("a2");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("returns undefined when there is no assistant message", () => {
|
|
36
|
-
expect(getAssistantMessageId([human("u1", "hi")])).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("measures string content length", () => {
|
|
40
|
-
expect(getTextLength([ai({ id: "a1", content: "hello" })], "a1")).toBe(5);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("sums text and thinking lengths across content blocks", () => {
|
|
44
|
-
expect(
|
|
45
|
-
getTextLength(
|
|
46
|
-
[
|
|
47
|
-
ai({
|
|
48
|
-
id: "a1",
|
|
49
|
-
content: [
|
|
50
|
-
{ type: "thinking", thinking: "hmm" },
|
|
51
|
-
{ type: "text", text: "answer" },
|
|
52
|
-
],
|
|
53
|
-
}),
|
|
54
|
-
],
|
|
55
|
-
"a1",
|
|
56
|
-
),
|
|
57
|
-
).toBe("hmm".length + "answer".length);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("measures reasoning blocks (summary and reasoning fields)", () => {
|
|
61
|
-
// reasoning via summary_text entries, joined like contentToParts does.
|
|
62
|
-
expect(
|
|
63
|
-
getTextLength(
|
|
64
|
-
[
|
|
65
|
-
ai({
|
|
66
|
-
id: "a1",
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: "reasoning",
|
|
70
|
-
summary: [
|
|
71
|
-
{ type: "summary_text", text: "step one" },
|
|
72
|
-
{ type: "summary_text", text: "step two" },
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
"a1",
|
|
79
|
-
),
|
|
80
|
-
).toBe("step one\n\n\nstep two".length);
|
|
81
|
-
|
|
82
|
-
// reasoning via the bare `reasoning` field when no summary is present.
|
|
83
|
-
expect(
|
|
84
|
-
getTextLength(
|
|
85
|
-
[
|
|
86
|
-
ai({
|
|
87
|
-
id: "a2",
|
|
88
|
-
content: [{ type: "reasoning", reasoning: "deduced" }],
|
|
89
|
-
}),
|
|
90
|
-
],
|
|
91
|
-
"a2",
|
|
92
|
-
),
|
|
93
|
-
).toBe("deduced".length);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("counts tool_calls on the assistant message", () => {
|
|
97
|
-
expect(
|
|
98
|
-
getToolCallCount(
|
|
99
|
-
[
|
|
100
|
-
ai({
|
|
101
|
-
id: "a1",
|
|
102
|
-
content: "",
|
|
103
|
-
tool_calls: [
|
|
104
|
-
{ id: "t1", name: "search", args: {} },
|
|
105
|
-
{ id: "t2", name: "fetch", args: {} },
|
|
106
|
-
],
|
|
107
|
-
}),
|
|
108
|
-
],
|
|
109
|
-
"a1",
|
|
110
|
-
),
|
|
111
|
-
).toBe(2);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("returns 0 when the message is missing or not assistant", () => {
|
|
115
|
-
expect(getTextLength([human("u1", "hi")], "a1")).toBe(0);
|
|
116
|
-
expect(getToolCallCount([human("u1", "hi")], "a1")).toBe(0);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import {
|
|
6
|
-
createRunSelectorAgainst,
|
|
7
|
-
makeExtras,
|
|
8
|
-
type Selector,
|
|
9
|
-
} from "./__tests__/langChainTestUtils";
|
|
10
|
-
|
|
11
|
-
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
-
mockUseAuiState: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal();
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
useAuiState: ((selector: Selector) =>
|
|
20
|
-
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
-
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
import { useLangChainError } from "./hooks";
|
|
26
|
-
|
|
27
|
-
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
-
|
|
29
|
-
describe("useLangChainError", () => {
|
|
30
|
-
it("returns undefined when extras are absent", () => {
|
|
31
|
-
runSelectorAgainst(undefined);
|
|
32
|
-
const { result } = renderHook(() => useLangChainError());
|
|
33
|
-
expect(result.current).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns the error value when extras carry error", () => {
|
|
37
|
-
const error = new Error("boom");
|
|
38
|
-
runSelectorAgainst(makeExtras({ error }));
|
|
39
|
-
const { result } = renderHook(() => useLangChainError());
|
|
40
|
-
expect(result.current).toBe(error);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns undefined when extras carry no error", () => {
|
|
44
|
-
runSelectorAgainst(makeExtras({ error: undefined }));
|
|
45
|
-
const { result } = renderHook(() => useLangChainError());
|
|
46
|
-
expect(result.current).toBeUndefined();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import {
|
|
6
|
-
createRunSelectorAgainst,
|
|
7
|
-
makeExtras,
|
|
8
|
-
type Selector,
|
|
9
|
-
} from "./__tests__/langChainTestUtils";
|
|
10
|
-
|
|
11
|
-
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
-
mockUseAuiState: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal();
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
useAuiState: ((selector: Selector) =>
|
|
20
|
-
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
-
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
import { useLangChainInterrupts } from "./hooks";
|
|
26
|
-
|
|
27
|
-
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
-
|
|
29
|
-
describe("useLangChainInterrupts", () => {
|
|
30
|
-
it("returns an empty array when extras are absent", () => {
|
|
31
|
-
runSelectorAgainst(undefined);
|
|
32
|
-
const { result } = renderHook(() => useLangChainInterrupts());
|
|
33
|
-
expect(result.current).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns the pending interrupts from extras", () => {
|
|
37
|
-
const interrupts = [{ id: "i1", value: { q: "ok?" } }];
|
|
38
|
-
runSelectorAgainst(makeExtras({ interrupts }));
|
|
39
|
-
const { result } = renderHook(() => useLangChainInterrupts());
|
|
40
|
-
expect(result.current).toBe(interrupts);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns an empty array when extras carry no interrupts", () => {
|
|
44
|
-
runSelectorAgainst(makeExtras({ interrupts: undefined }));
|
|
45
|
-
const { result } = renderHook(() => useLangChainInterrupts());
|
|
46
|
-
expect(result.current).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import { makeExtras } from "./__tests__/langChainTestUtils";
|
|
6
|
-
|
|
7
|
-
const { mockUseAui } = vi.hoisted(() => ({
|
|
8
|
-
mockUseAui: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
12
|
-
const actual = await importOriginal();
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
useAui: (() => mockUseAui()) as unknown as typeof actual.useAui,
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
import { useLangChainRespond } from "./hooks";
|
|
20
|
-
|
|
21
|
-
describe("useLangChainRespond", () => {
|
|
22
|
-
it("forwards the response and options to stream.respond", async () => {
|
|
23
|
-
const respond = vi.fn().mockResolvedValue(undefined);
|
|
24
|
-
mockUseAui.mockReturnValue({
|
|
25
|
-
thread: () => ({
|
|
26
|
-
getState: () => ({ extras: makeExtras({ respond }) }),
|
|
27
|
-
}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const { result } = renderHook(() => useLangChainRespond());
|
|
31
|
-
await result.current({ approved: true }, { interruptId: "x" });
|
|
32
|
-
|
|
33
|
-
expect(respond).toHaveBeenCalledWith(
|
|
34
|
-
{ approved: true },
|
|
35
|
-
{ interruptId: "x" },
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("throws when extras are absent or unbranded", () => {
|
|
40
|
-
mockUseAui.mockReturnValue({
|
|
41
|
-
thread: () => ({ getState: () => ({ extras: { respond: vi.fn() } }) }),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const { result } = renderHook(() => useLangChainRespond());
|
|
45
|
-
expect(() => result.current({ approved: true })).toThrow(
|
|
46
|
-
"useStreamRuntime",
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import { makeExtras } from "./__tests__/langChainTestUtils";
|
|
6
|
-
|
|
7
|
-
const { mockUseAui } = vi.hoisted(() => ({
|
|
8
|
-
mockUseAui: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
12
|
-
const actual = await importOriginal();
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
useAui: (() => mockUseAui()) as unknown as typeof actual.useAui,
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
import { useLangChainRespondAll } from "./hooks";
|
|
20
|
-
|
|
21
|
-
describe("useLangChainRespondAll", () => {
|
|
22
|
-
it("forwards the responses and options to stream.respondAll", async () => {
|
|
23
|
-
const respondAll = vi.fn().mockResolvedValue(undefined);
|
|
24
|
-
mockUseAui.mockReturnValue({
|
|
25
|
-
thread: () => ({
|
|
26
|
-
getState: () => ({ extras: makeExtras({ respondAll }) }),
|
|
27
|
-
}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const { result } = renderHook(() => useLangChainRespondAll());
|
|
31
|
-
await result.current(
|
|
32
|
-
{ a: { approved: true }, b: { approved: false } },
|
|
33
|
-
{ metadata: { src: "ui" } },
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
expect(respondAll).toHaveBeenCalledWith(
|
|
37
|
-
{ a: { approved: true }, b: { approved: false } },
|
|
38
|
-
{ metadata: { src: "ui" } },
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("throws when extras are absent or unbranded", () => {
|
|
43
|
-
mockUseAui.mockReturnValue({
|
|
44
|
-
thread: () => ({ getState: () => ({ extras: { respondAll: vi.fn() } }) }),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const { result } = renderHook(() => useLangChainRespondAll());
|
|
48
|
-
expect(() => result.current({ a: { approved: true } })).toThrow(
|
|
49
|
-
"useStreamRuntime",
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import {
|
|
6
|
-
createRunSelectorAgainst,
|
|
7
|
-
makeExtras,
|
|
8
|
-
type Selector,
|
|
9
|
-
} from "./__tests__/langChainTestUtils";
|
|
10
|
-
|
|
11
|
-
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
-
mockUseAuiState: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal();
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
useAuiState: ((selector: Selector) =>
|
|
20
|
-
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
-
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
import { useLangChainStream } from "./hooks";
|
|
26
|
-
|
|
27
|
-
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
-
|
|
29
|
-
describe("useLangChainStream", () => {
|
|
30
|
-
it("returns undefined when extras are absent", () => {
|
|
31
|
-
runSelectorAgainst(undefined);
|
|
32
|
-
const { result } = renderHook(() => useLangChainStream());
|
|
33
|
-
expect(result.current).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns the stream handle from extras", () => {
|
|
37
|
-
const stream = { submit: () => {} };
|
|
38
|
-
runSelectorAgainst(makeExtras({ stream }));
|
|
39
|
-
const { result } = renderHook(() => useLangChainStream());
|
|
40
|
-
expect(result.current).toBe(stream);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import {
|
|
6
|
-
createRunSelectorAgainst,
|
|
7
|
-
makeExtras,
|
|
8
|
-
type Selector,
|
|
9
|
-
} from "./__tests__/langChainTestUtils";
|
|
10
|
-
|
|
11
|
-
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
-
mockUseAuiState: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal();
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
useAuiState: ((selector: Selector) =>
|
|
20
|
-
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
-
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
import { useLangChainSubagents } from "./hooks";
|
|
26
|
-
|
|
27
|
-
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
-
|
|
29
|
-
describe("useLangChainSubagents", () => {
|
|
30
|
-
it("returns the stable empty map when extras are absent", () => {
|
|
31
|
-
runSelectorAgainst(undefined);
|
|
32
|
-
const { result } = renderHook(() => useLangChainSubagents());
|
|
33
|
-
expect(result.current.size).toBe(0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns the subagents map from extras", () => {
|
|
37
|
-
const subagents = new Map([["researcher", { namespace: "researcher" }]]);
|
|
38
|
-
runSelectorAgainst(makeExtras({ subagents }));
|
|
39
|
-
const { result } = renderHook(() => useLangChainSubagents());
|
|
40
|
-
expect(result.current).toBe(subagents);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns the stable empty map when extras carry no subagents", () => {
|
|
44
|
-
runSelectorAgainst(makeExtras({ subagents: undefined }));
|
|
45
|
-
const { result } = renderHook(() => useLangChainSubagents());
|
|
46
|
-
expect(result.current.size).toBe(0);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import {
|
|
6
|
-
createRunSelectorAgainst,
|
|
7
|
-
makeExtras,
|
|
8
|
-
type Selector,
|
|
9
|
-
} from "./__tests__/langChainTestUtils";
|
|
10
|
-
|
|
11
|
-
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
-
mockUseAuiState: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal();
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
useAuiState: ((selector: Selector) =>
|
|
20
|
-
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
-
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
import { useLangChainSubgraphs } from "./hooks";
|
|
26
|
-
|
|
27
|
-
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
-
|
|
29
|
-
describe("useLangChainSubgraphs", () => {
|
|
30
|
-
it("returns the stable empty map when extras are absent", () => {
|
|
31
|
-
runSelectorAgainst(undefined);
|
|
32
|
-
const { result } = renderHook(() => useLangChainSubgraphs());
|
|
33
|
-
expect(result.current.size).toBe(0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns the subgraphs map from extras", () => {
|
|
37
|
-
const subgraphs = new Map([["planner", { namespace: "planner" }]]);
|
|
38
|
-
runSelectorAgainst(makeExtras({ subgraphs }));
|
|
39
|
-
const { result } = renderHook(() => useLangChainSubgraphs());
|
|
40
|
-
expect(result.current).toBe(subgraphs);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns the stable empty map when extras carry no subgraphs", () => {
|
|
44
|
-
runSelectorAgainst(makeExtras({ subgraphs: undefined }));
|
|
45
|
-
const { result } = renderHook(() => useLangChainSubgraphs());
|
|
46
|
-
expect(result.current.size).toBe(0);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { renderHook } from "@testing-library/react";
|
|
5
|
-
import {
|
|
6
|
-
createRunSelectorAgainst,
|
|
7
|
-
makeExtras,
|
|
8
|
-
type Selector,
|
|
9
|
-
} from "./__tests__/langChainTestUtils";
|
|
10
|
-
|
|
11
|
-
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
-
mockUseAuiState: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal();
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
useAuiState: ((selector: Selector) =>
|
|
20
|
-
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
-
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
import { useLangChainToolCalls } from "./hooks";
|
|
26
|
-
|
|
27
|
-
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
-
|
|
29
|
-
describe("useLangChainToolCalls", () => {
|
|
30
|
-
it("returns an empty array when extras are absent", () => {
|
|
31
|
-
runSelectorAgainst(undefined);
|
|
32
|
-
const { result } = renderHook(() => useLangChainToolCalls());
|
|
33
|
-
expect(result.current).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns the assembled tool calls from extras", () => {
|
|
37
|
-
const toolCalls = [{ id: "a", name: "search", args: { q: "x" } }];
|
|
38
|
-
runSelectorAgainst(makeExtras({ toolCalls }));
|
|
39
|
-
const { result } = renderHook(() => useLangChainToolCalls());
|
|
40
|
-
expect(result.current).toBe(toolCalls);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns an empty array when extras carry no tool calls", () => {
|
|
44
|
-
runSelectorAgainst(makeExtras({ toolCalls: undefined }));
|
|
45
|
-
const { result } = renderHook(() => useLangChainToolCalls());
|
|
46
|
-
expect(result.current).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
});
|