@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.0
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/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/aiSDKFormatAdapter.js +17 -0
- package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.js +36 -0
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
- package/dist/aiSDKExtras.d.ts +13 -0
- package/dist/aiSDKExtras.d.ts.map +1 -0
- package/dist/aiSDKExtras.js +6 -0
- package/dist/aiSDKExtras.js.map +1 -0
- package/dist/converters/convertMessage.d.ts +44 -0
- package/dist/converters/convertMessage.d.ts.map +1 -0
- package/dist/converters/convertMessage.js +465 -0
- package/dist/converters/convertMessage.js.map +1 -0
- package/dist/converters/toCreateMessage.d.ts +4 -0
- package/dist/converters/toCreateMessage.d.ts.map +1 -0
- package/dist/converters/toCreateMessage.js +65 -0
- package/dist/converters/toCreateMessage.js.map +1 -0
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +16 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +19 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/model-context/injectInteractableContext.d.ts +37 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.js +54 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -0
- package/dist/model-context/injectQuoteContext.d.ts +25 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
- package/dist/model-context/injectQuoteContext.js +51 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -0
- package/dist/runtime/AISDKChat.d.ts +13 -0
- package/dist/runtime/AISDKChat.d.ts.map +1 -0
- package/dist/runtime/AISDKChat.js +34 -0
- package/dist/runtime/AISDKChat.js.map +1 -0
- package/dist/runtime/AISDKThreads.d.ts +24 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -0
- package/dist/runtime/AISDKThreads.js +92 -0
- package/dist/runtime/AISDKThreads.js.map +1 -0
- package/dist/runtime/DynamicChatTransport.d.ts +50 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +153 -0
- package/dist/runtime/DynamicChatTransport.js.map +1 -0
- package/dist/runtime/getResumableAdapter.d.ts +5 -0
- package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
- package/dist/runtime/getResumableAdapter.js +11 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +122 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +95 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
- package/dist/runtime/useAISDKRuntime.js +754 -0
- package/dist/runtime/useAISDKRuntime.js.map +1 -0
- package/dist/runtime/useChatRuntime.d.ts +17 -0
- package/dist/runtime/useChatRuntime.d.ts.map +1 -0
- package/dist/runtime/useChatRuntime.js +50 -0
- package/dist/runtime/useChatRuntime.js.map +1 -0
- package/dist/runtime/useChatSnapshot.d.ts +6 -0
- package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
- package/dist/runtime/useChatSnapshot.js +62 -0
- package/dist/runtime/useChatSnapshot.js.map +1 -0
- package/dist/runtime/useChatThread.d.ts +121 -0
- package/dist/runtime/useChatThread.d.ts.map +1 -0
- package/dist/runtime/useChatThread.js +255 -0
- package/dist/runtime/useChatThread.js.map +1 -0
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +26 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts +12 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -0
- package/dist/runtime/useExternalHistory.js +556 -0
- package/dist/runtime/useExternalHistory.js.map +1 -0
- package/dist/runtime/useMessageQueue.d.ts +14 -0
- package/dist/runtime/useMessageQueue.d.ts.map +1 -0
- package/dist/runtime/useMessageQueue.js +142 -0
- package/dist/runtime/useMessageQueue.js.map +1 -0
- package/dist/runtime/useResourceCleanup.d.ts +2 -0
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
- package/dist/runtime/useResourceCleanup.js +47 -0
- package/dist/runtime/useResourceCleanup.js.map +1 -0
- package/dist/runtime/useStreamingTiming.d.ts +12 -0
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
- package/dist/runtime/useStreamingTiming.js +36 -0
- package/dist/runtime/useStreamingTiming.js.map +1 -0
- package/dist/tools/generativeTools.d.ts +71 -0
- package/dist/tools/generativeTools.d.ts.map +1 -0
- package/dist/tools/generativeTools.js +258 -0
- package/dist/tools/generativeTools.js.map +1 -0
- package/dist/tools/mcp-stdio.node.d.ts +2 -0
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.node.js +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.js +10 -0
- package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
- package/dist/transport/AssistantChatTransport.d.ts +20 -0
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
- package/dist/transport/AssistantChatTransport.js +151 -0
- package/dist/transport/AssistantChatTransport.js.map +1 -0
- package/dist/transport/resumable.d.ts +36 -0
- package/dist/transport/resumable.d.ts.map +1 -0
- package/dist/transport/resumable.js +112 -0
- package/dist/transport/resumable.js.map +1 -0
- package/dist/usage.d.ts +24 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +121 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/getVercelAIMessages.d.ts +4 -0
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
- package/dist/utils/getVercelAIMessages.js +8 -0
- package/dist/utils/getVercelAIMessages.js.map +1 -0
- package/dist/utils/sliceMessagesUntil.d.ts +3 -0
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
- package/dist/utils/sliceMessagesUntil.js +10 -0
- package/dist/utils/sliceMessagesUntil.js.map +1 -0
- package/package.json +96 -3
- package/src/__tests__/standalone.e2e.ts +118 -0
- package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
- package/src/adapters/aiSDKFormatAdapter.ts +18 -0
- package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
- package/src/adapters/vercelAttachmentAdapter.ts +36 -0
- package/src/aiSDKExtras.ts +31 -0
- package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
- package/src/converters/convertMessage.test.ts +2428 -0
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +919 -0
- package/src/converters/toCreateMessage.test.ts +716 -0
- package/src/converters/toCreateMessage.ts +110 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/hooks.test.ts +168 -0
- package/src/hooks.ts +26 -0
- package/src/index.ts +29 -0
- package/src/model-context/injectInteractableContext.test.ts +167 -0
- package/src/model-context/injectInteractableContext.ts +64 -0
- package/src/model-context/injectQuoteContext.test.ts +33 -0
- package/src/model-context/injectQuoteContext.ts +63 -0
- package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
- package/src/runtime/AISDKChat.integration.test.tsx +148 -0
- package/src/runtime/AISDKChat.test.ts +241 -0
- package/src/runtime/AISDKChat.ts +43 -0
- package/src/runtime/AISDKThreads.test.ts +811 -0
- package/src/runtime/AISDKThreads.ts +194 -0
- package/src/runtime/DynamicChatTransport.test.ts +228 -0
- package/src/runtime/DynamicChatTransport.ts +281 -0
- package/src/runtime/__tests__/controlled-transport.ts +70 -0
- package/src/runtime/__tests__/tap.ts +80 -0
- package/src/runtime/__tests__/vue.ts +46 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +187 -0
- package/src/runtime/toolHistoryCodec.ts +245 -0
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
- package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
- package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
- package/src/runtime/useAISDKRuntime.test.ts +2345 -0
- package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
- package/src/runtime/useAISDKRuntime.ts +1503 -0
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatOnTap.test.ts +60 -0
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
- package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
- package/src/runtime/useChatRuntime.native.test.ts +147 -0
- package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
- package/src/runtime/useChatRuntime.test.ts +619 -0
- package/src/runtime/useChatRuntime.ts +80 -0
- package/src/runtime/useChatSnapshot.test.ts +60 -0
- package/src/runtime/useChatSnapshot.ts +86 -0
- package/src/runtime/useChatThread.binding.test.tsx +140 -0
- package/src/runtime/useChatThread.test.ts +993 -0
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +533 -0
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +4187 -0
- package/src/runtime/useExternalHistory.ts +902 -0
- package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
- package/src/runtime/useMessageQueue.test.tsx +256 -0
- package/src/runtime/useMessageQueue.ts +189 -0
- package/src/runtime/useResourceCleanup.ts +58 -0
- package/src/runtime/useStreamingTiming.test.tsx +96 -0
- package/src/runtime/useStreamingTiming.ts +69 -0
- package/src/tools/generativeTools.test.ts +1058 -0
- package/src/tools/generativeTools.ts +491 -0
- package/src/tools/mcp-stdio.node.ts +1 -0
- package/src/tools/mcp-stdio.unsupported.ts +12 -0
- package/src/transport/AssistantChatTransport.test.ts +631 -0
- package/src/transport/AssistantChatTransport.ts +236 -0
- package/src/transport/resumable.test.ts +262 -0
- package/src/transport/resumable.ts +162 -0
- package/src/usage.test.ts +204 -0
- package/src/usage.ts +179 -0
- package/src/utils/getVercelAIMessages.ts +11 -0
- package/src/utils/sliceMessagesUntil.test.ts +51 -0
- package/src/utils/sliceMessagesUntil.ts +16 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AppendMessage } from "@ulearning/ai-core";
|
|
2
|
+
import {
|
|
3
|
+
resolveFileMediaType,
|
|
4
|
+
resolveFilePartSource,
|
|
5
|
+
resolveImageMediaType,
|
|
6
|
+
toMediaWireUrl,
|
|
7
|
+
} from "@ulearning/ai-core/internal";
|
|
8
|
+
import type {
|
|
9
|
+
CreateUIMessage,
|
|
10
|
+
UIDataTypes,
|
|
11
|
+
UIMessage,
|
|
12
|
+
UIMessagePart,
|
|
13
|
+
UITools,
|
|
14
|
+
} from "ai";
|
|
15
|
+
|
|
16
|
+
type InputPart = AppendMessage["content"][number] & {
|
|
17
|
+
readonly contentType?: string | undefined;
|
|
18
|
+
readonly filename?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const toCreateMessage = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
22
|
+
message: AppendMessage,
|
|
23
|
+
): CreateUIMessage<UI_MESSAGE> => {
|
|
24
|
+
const inputParts: InputPart[] = [
|
|
25
|
+
...message.content,
|
|
26
|
+
...(message.attachments?.flatMap((a) =>
|
|
27
|
+
a.content.map((c) => ({
|
|
28
|
+
...c,
|
|
29
|
+
filename: a.name,
|
|
30
|
+
contentType: a.contentType,
|
|
31
|
+
})),
|
|
32
|
+
) ?? []),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const parts = inputParts.map((part): UIMessagePart<UIDataTypes, UITools> => {
|
|
36
|
+
switch (part.type) {
|
|
37
|
+
case "text":
|
|
38
|
+
return {
|
|
39
|
+
type: "text",
|
|
40
|
+
text: part.text,
|
|
41
|
+
...(part.providerMetadata != null && {
|
|
42
|
+
providerMetadata: part.providerMetadata,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
case "image": {
|
|
46
|
+
const mediaType = resolveImageMediaType(part.image, part.contentType);
|
|
47
|
+
return {
|
|
48
|
+
type: "file",
|
|
49
|
+
url: toMediaWireUrl(part.image, mediaType),
|
|
50
|
+
...(part.filename && { filename: part.filename }),
|
|
51
|
+
mediaType,
|
|
52
|
+
...(part.providerMetadata != null && {
|
|
53
|
+
providerMetadata: part.providerMetadata,
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
case "file": {
|
|
58
|
+
// `mimeType` is a plain string, and an adapter reading `file.type` on a
|
|
59
|
+
// file the OS cannot type yields "". Same ladder as images: declared,
|
|
60
|
+
// then the envelope, then the floor.
|
|
61
|
+
const mediaType = resolveFileMediaType(part.data, part.mimeType);
|
|
62
|
+
return {
|
|
63
|
+
type: "file",
|
|
64
|
+
// An `id` reference is an opaque provider handle, not base64, and
|
|
65
|
+
// this adapter has no way to send one. Left unwrapped so it fails
|
|
66
|
+
// loudly upstream rather than shipping a corrupt payload.
|
|
67
|
+
url:
|
|
68
|
+
part.sourceType === "id"
|
|
69
|
+
? part.data
|
|
70
|
+
: toMediaWireUrl(part.data, mediaType),
|
|
71
|
+
mediaType,
|
|
72
|
+
...(part.filename && { filename: part.filename }),
|
|
73
|
+
...(part.providerMetadata != null && {
|
|
74
|
+
providerMetadata: part.providerMetadata,
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case "audio": {
|
|
79
|
+
// A data URL's own media type wins over `mediaType` downstream, so the
|
|
80
|
+
// envelope is rebuilt from the typed format rather than forwarded.
|
|
81
|
+
const mediaType = `audio/${part.audio.format}`;
|
|
82
|
+
const data = part.audio.data;
|
|
83
|
+
const source = resolveFilePartSource({ data, mimeType: mediaType });
|
|
84
|
+
return {
|
|
85
|
+
type: "file",
|
|
86
|
+
url:
|
|
87
|
+
source.kind === "url"
|
|
88
|
+
? source.url
|
|
89
|
+
: `data:${mediaType};base64,${source.data}`,
|
|
90
|
+
mediaType,
|
|
91
|
+
...(part.filename && { filename: part.filename }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case "data":
|
|
95
|
+
return {
|
|
96
|
+
type: `data-${part.name}`,
|
|
97
|
+
data: part.data,
|
|
98
|
+
...(part.id !== undefined && { id: part.id }),
|
|
99
|
+
};
|
|
100
|
+
default:
|
|
101
|
+
throw new Error(`Unsupported part type: ${part.type}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
role: message.role,
|
|
107
|
+
parts,
|
|
108
|
+
metadata: message.metadata,
|
|
109
|
+
} satisfies CreateUIMessage<UIMessage> as CreateUIMessage<UI_MESSAGE>;
|
|
110
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolApprovalAnswer } from "@ulearning/ai-core";
|
|
2
|
+
|
|
3
|
+
export const normalizeToolApprovalAnswers = (
|
|
4
|
+
answers: unknown,
|
|
5
|
+
): Readonly<Record<string, ToolApprovalAnswer>> | undefined => {
|
|
6
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers))
|
|
7
|
+
return undefined;
|
|
8
|
+
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(answers).flatMap(([id, value]) => {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
|
+
return [];
|
|
13
|
+
const { optionIds, text } = value as Record<string, unknown>;
|
|
14
|
+
const answer: ToolApprovalAnswer = {
|
|
15
|
+
...(Array.isArray(optionIds) && {
|
|
16
|
+
optionIds: optionIds.filter(
|
|
17
|
+
(optionId): optionId is string => typeof optionId === "string",
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
...(typeof text === "string" && { text }),
|
|
21
|
+
};
|
|
22
|
+
return answer.optionIds?.length || answer.text !== undefined
|
|
23
|
+
? [[id, answer]]
|
|
24
|
+
: [];
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import {
|
|
3
|
+
createApp,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
isProxy,
|
|
7
|
+
isReadonly,
|
|
8
|
+
nextTick,
|
|
9
|
+
version,
|
|
10
|
+
type App,
|
|
11
|
+
} from "vue";
|
|
12
|
+
import { afterEach, beforeAll, expect, expectTypeOf, it, vi } from "vitest";
|
|
13
|
+
import type { UIMessage } from "ai";
|
|
14
|
+
import type { ThreadMessage } from "@ulearning/ai-core";
|
|
15
|
+
import {
|
|
16
|
+
AssistantRuntimeImpl,
|
|
17
|
+
ExternalStoreRuntimeCore,
|
|
18
|
+
} from "@ulearning/ai-core/internal";
|
|
19
|
+
import { AssistantRuntimeProvider } from "@ulearning/ai-core/vue";
|
|
20
|
+
import { flushTapSync } from "@ulearning/ai-tap";
|
|
21
|
+
import { aiSDKExtras, type AISDKChatHelpers } from "./aiSDKExtras";
|
|
22
|
+
import { useAISDKChat, useAISDKError } from "./hooks";
|
|
23
|
+
import { useThreadTokenUsage } from "./usage";
|
|
24
|
+
|
|
25
|
+
const apps: App[] = [];
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
const expected = process.env.AUI_VUE_RUNTIME_ROOT?.replaceAll("\\", "/")
|
|
28
|
+
.split("/")
|
|
29
|
+
.at(-1);
|
|
30
|
+
if (expected) expect(version).toBe(expected);
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
apps.splice(0).forEach((app) => app.unmount());
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
const helpers = (id: string): AISDKChatHelpers => ({
|
|
37
|
+
id,
|
|
38
|
+
messages: [],
|
|
39
|
+
status: "ready",
|
|
40
|
+
error: undefined,
|
|
41
|
+
setMessages: vi.fn(),
|
|
42
|
+
clearError: vi.fn(),
|
|
43
|
+
sendMessage: vi.fn(async () => {}),
|
|
44
|
+
regenerate: vi.fn(async () => {}),
|
|
45
|
+
stop: vi.fn(async () => {}),
|
|
46
|
+
resumeStream: vi.fn(async () => {}),
|
|
47
|
+
addToolResult: vi.fn(async () => {}),
|
|
48
|
+
addToolOutput: vi.fn(async () => {}),
|
|
49
|
+
addToolApprovalResponse: vi.fn(async () => {}),
|
|
50
|
+
});
|
|
51
|
+
const message = (id: string, input: number, output: number): ThreadMessage => ({
|
|
52
|
+
id,
|
|
53
|
+
role: "assistant",
|
|
54
|
+
content: [{ type: "text", text: id }],
|
|
55
|
+
createdAt: new Date(0),
|
|
56
|
+
status: { type: "complete", reason: "stop" },
|
|
57
|
+
metadata: {
|
|
58
|
+
unstable_state: null,
|
|
59
|
+
unstable_annotations: [],
|
|
60
|
+
unstable_data: [],
|
|
61
|
+
steps: [],
|
|
62
|
+
custom: { usage: { inputTokens: input, outputTokens: output } },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const mount = (core: ExternalStoreRuntimeCore) => {
|
|
66
|
+
let values!: {
|
|
67
|
+
chat: ReturnType<typeof useAISDKChat>;
|
|
68
|
+
error: ReturnType<typeof useAISDKError>;
|
|
69
|
+
usage: ReturnType<typeof useThreadTokenUsage>;
|
|
70
|
+
};
|
|
71
|
+
let renders = 0;
|
|
72
|
+
const Consumer = defineComponent({
|
|
73
|
+
setup() {
|
|
74
|
+
values = {
|
|
75
|
+
chat: useAISDKChat(),
|
|
76
|
+
error: useAISDKError(),
|
|
77
|
+
usage: useThreadTokenUsage(),
|
|
78
|
+
};
|
|
79
|
+
expectTypeOf(values.chat.value?.messages).toEqualTypeOf<
|
|
80
|
+
UIMessage[] | undefined
|
|
81
|
+
>();
|
|
82
|
+
return () => {
|
|
83
|
+
renders++;
|
|
84
|
+
return h(
|
|
85
|
+
"p",
|
|
86
|
+
`${values.chat.value?.id}:${values.error.value?.message}:${values.usage.value?.totalTokens}`,
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const runtime = new AssistantRuntimeImpl(core);
|
|
92
|
+
const app = createApp(
|
|
93
|
+
defineComponent({
|
|
94
|
+
setup: () => () =>
|
|
95
|
+
h(
|
|
96
|
+
AssistantRuntimeProvider,
|
|
97
|
+
{ runtime },
|
|
98
|
+
{ default: () => h(Consumer) },
|
|
99
|
+
),
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
apps.push(app);
|
|
103
|
+
const element = document.createElement("div");
|
|
104
|
+
app.mount(element);
|
|
105
|
+
return {
|
|
106
|
+
element,
|
|
107
|
+
get values() {
|
|
108
|
+
return values;
|
|
109
|
+
},
|
|
110
|
+
get renders() {
|
|
111
|
+
return renders;
|
|
112
|
+
},
|
|
113
|
+
unmount: () => app.unmount(),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
it("returns explicit undefined Ref values for a runtime without AI SDK extras or usage", () => {
|
|
118
|
+
const view = mount(
|
|
119
|
+
new ExternalStoreRuntimeCore({ messages: [], onNew: async () => {} }),
|
|
120
|
+
);
|
|
121
|
+
expect(view.values.chat.value).toBeUndefined();
|
|
122
|
+
expect(view.values.error.value).toBeUndefined();
|
|
123
|
+
expect(view.values.usage.value).toBeUndefined();
|
|
124
|
+
expect(isReadonly(view.values.chat)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("updates advanced helpers, errors and usage through the real Core/Store bridge", async () => {
|
|
128
|
+
const first = helpers("first");
|
|
129
|
+
const next = helpers("next");
|
|
130
|
+
const onNew = async () => {};
|
|
131
|
+
const core = new ExternalStoreRuntimeCore({
|
|
132
|
+
messages: [message("one", 4, 2)],
|
|
133
|
+
onNew,
|
|
134
|
+
extras: aiSDKExtras.provide({ chat: first, error: undefined }),
|
|
135
|
+
});
|
|
136
|
+
const view = mount(core);
|
|
137
|
+
expect(view.values.chat.value).toBe(first);
|
|
138
|
+
expect(isProxy(view.values.chat.value)).toBe(false);
|
|
139
|
+
expect(view.values.chat.value?.resumeStream).toBe(first.resumeStream);
|
|
140
|
+
expect(view.values.usage.value).toEqual({
|
|
141
|
+
inputTokens: 4,
|
|
142
|
+
outputTokens: 2,
|
|
143
|
+
totalTokens: 6,
|
|
144
|
+
});
|
|
145
|
+
const failure = new Error("transport failed");
|
|
146
|
+
flushTapSync(() =>
|
|
147
|
+
core.setAdapter({
|
|
148
|
+
messages: [message("one", 4, 2), message("two", 8, 3)],
|
|
149
|
+
onNew,
|
|
150
|
+
extras: aiSDKExtras.provide({ chat: next, error: failure }),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
await nextTick();
|
|
154
|
+
expect(view.values.chat.value).toBe(next);
|
|
155
|
+
expect(view.values.error.value).toBe(failure);
|
|
156
|
+
expect(view.element.textContent).toBe("next:transport failed:11");
|
|
157
|
+
const renders = view.renders;
|
|
158
|
+
view.unmount();
|
|
159
|
+
flushTapSync(() =>
|
|
160
|
+
core.setAdapter({
|
|
161
|
+
messages: [],
|
|
162
|
+
onNew,
|
|
163
|
+
extras: aiSDKExtras.provide({ chat: first, error: undefined }),
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
await nextTick();
|
|
167
|
+
expect(view.renders).toBe(renders);
|
|
168
|
+
});
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ShallowRef } from "vue";
|
|
4
|
+
import type { UIMessage } from "ai";
|
|
5
|
+
import { aiSDKExtras, type AISDKChatHelpers } from "./aiSDKExtras";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read the last AI SDK chat error object from the runtime extras.
|
|
9
|
+
* The readonly Ref contains `undefined` when there is no error or the current thread is not backed by
|
|
10
|
+
* the AI SDK runtime.
|
|
11
|
+
*/
|
|
12
|
+
export const useAISDKError = () => aiSDKExtras.use((e) => e.error, undefined);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The underlying `useChat` helpers object, for advanced views — reach
|
|
16
|
+
* `resumeStream`, `clearError`, or the raw `sendMessage` without forking the
|
|
17
|
+
* runtime hook. Read `.value`; it is `undefined` when the thread is not backed by the AI
|
|
18
|
+
* SDK runtime.
|
|
19
|
+
*/
|
|
20
|
+
export const useAISDKChat = <
|
|
21
|
+
UI_MESSAGE extends UIMessage = UIMessage,
|
|
22
|
+
>(): Readonly<ShallowRef<AISDKChatHelpers<UI_MESSAGE> | undefined>> =>
|
|
23
|
+
aiSDKExtras.use(
|
|
24
|
+
(e) => e.chat as unknown as AISDKChatHelpers<UI_MESSAGE>,
|
|
25
|
+
undefined,
|
|
26
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/// <reference types="@ulearning/ai-core/vue" preserve="true" />
|
|
2
|
+
|
|
3
|
+
export { useAISDKRuntime } from "./runtime/useAISDKRuntime";
|
|
4
|
+
export { useChatRuntime } from "./runtime/useChatRuntime";
|
|
5
|
+
export type { UseChatRuntimeOptions } from "./runtime/useChatRuntime";
|
|
6
|
+
export { AssistantChatTransport } from "./transport/AssistantChatTransport";
|
|
7
|
+
export {
|
|
8
|
+
RESUMABLE_STREAM_ID_HEADER,
|
|
9
|
+
createResumableSessionStorage,
|
|
10
|
+
} from "./transport/resumable";
|
|
11
|
+
export type {
|
|
12
|
+
AssistantChatResumableOptions,
|
|
13
|
+
ResumableClientStorage,
|
|
14
|
+
} from "./transport/resumable";
|
|
15
|
+
export { frontendTools, type FrontendTools } from "@ulearning/ai-stream/ai-sdk";
|
|
16
|
+
export { injectQuoteContext } from "./model-context/injectQuoteContext";
|
|
17
|
+
export { unstable_injectInteractableContext } from "./model-context/injectInteractableContext";
|
|
18
|
+
export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
|
|
19
|
+
export { getThreadMessageTokenUsage, useThreadTokenUsage } from "./usage";
|
|
20
|
+
export { useAISDKChat, useAISDKError } from "./hooks";
|
|
21
|
+
export {
|
|
22
|
+
AISDKToolkit,
|
|
23
|
+
generativeTools,
|
|
24
|
+
type AISDKToolkitOptions,
|
|
25
|
+
type AISDKToolkitToolsOptions,
|
|
26
|
+
type GenerativeToolsOptions,
|
|
27
|
+
} from "./tools/generativeTools";
|
|
28
|
+
export { AISDKChat, type AISDKChatOptions } from "./runtime/AISDKChat";
|
|
29
|
+
export { AISDKThreads, type AISDKThreadsOptions } from "./runtime/AISDKThreads";
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { unstable_injectInteractableContext } from "./injectInteractableContext";
|
|
4
|
+
import { injectQuoteContext } from "./injectQuoteContext";
|
|
5
|
+
|
|
6
|
+
type Interactable = {
|
|
7
|
+
name: string;
|
|
8
|
+
id: string;
|
|
9
|
+
state: unknown;
|
|
10
|
+
partial?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const userMsg = (
|
|
14
|
+
interactables?: Interactable[],
|
|
15
|
+
extraCustom?: Record<string, unknown>,
|
|
16
|
+
): UIMessage =>
|
|
17
|
+
({
|
|
18
|
+
id: "m1",
|
|
19
|
+
role: "user",
|
|
20
|
+
parts: [{ type: "text", text: "hello" }],
|
|
21
|
+
...(interactables || extraCustom
|
|
22
|
+
? {
|
|
23
|
+
metadata: {
|
|
24
|
+
custom: {
|
|
25
|
+
...extraCustom,
|
|
26
|
+
...(interactables ? { interactables } : {}),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: {}),
|
|
31
|
+
}) as UIMessage;
|
|
32
|
+
|
|
33
|
+
const textOf = (part: unknown) => (part as { text?: string }).text;
|
|
34
|
+
const isStateText = (part: unknown) =>
|
|
35
|
+
(part as { type?: string }).type === "text" &&
|
|
36
|
+
(textOf(part) ?? "").startsWith("[Current state of");
|
|
37
|
+
|
|
38
|
+
describe("unstable_injectInteractableContext", () => {
|
|
39
|
+
it("returns the message unchanged when there is no interactable metadata", () => {
|
|
40
|
+
const input = [userMsg()];
|
|
41
|
+
const out = unstable_injectInteractableContext(input);
|
|
42
|
+
expect(out[0]).toBe(input[0]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("skips non-user messages", () => {
|
|
46
|
+
const input = [
|
|
47
|
+
{
|
|
48
|
+
id: "a1",
|
|
49
|
+
role: "assistant",
|
|
50
|
+
parts: [{ type: "text", text: "hi" }],
|
|
51
|
+
metadata: {
|
|
52
|
+
custom: {
|
|
53
|
+
interactables: [{ name: "note", id: "n1", state: { v: 1 } }],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
} as UIMessage,
|
|
57
|
+
];
|
|
58
|
+
const out = unstable_injectInteractableContext(input);
|
|
59
|
+
expect(out[0]).toBe(input[0]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("prepends a text part using the default format (id included for update_* addressing)", () => {
|
|
63
|
+
const out = unstable_injectInteractableContext([
|
|
64
|
+
userMsg([{ name: "note", id: "n1", state: { title: "Hi" } }]),
|
|
65
|
+
]);
|
|
66
|
+
expect(textOf(out[0]!.parts[0])).toBe(
|
|
67
|
+
'[Current state of "note" (id: "n1"): {"title":"Hi"}]\n\n',
|
|
68
|
+
);
|
|
69
|
+
expect(textOf(out[0]!.parts[1])).toBe("hello");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("formats a partial snapshot as changed fields", () => {
|
|
73
|
+
const out = unstable_injectInteractableContext([
|
|
74
|
+
userMsg([
|
|
75
|
+
{ name: "note", id: "n1", state: { title: "Hi" }, partial: true },
|
|
76
|
+
]),
|
|
77
|
+
]);
|
|
78
|
+
expect(textOf(out[0]!.parts[0])).toBe(
|
|
79
|
+
'[State of "note" (id: "n1") changed — updated fields: {"title":"Hi"}; fields not listed are unchanged]\n\n',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("joins multiple interactables with a newline", () => {
|
|
84
|
+
const out = unstable_injectInteractableContext([
|
|
85
|
+
userMsg([
|
|
86
|
+
{ name: "note", id: "n1", state: { v: 1 } },
|
|
87
|
+
{ name: "board", id: "b1", state: { v: 2 } },
|
|
88
|
+
]),
|
|
89
|
+
]);
|
|
90
|
+
expect(textOf(out[0]!.parts[0])).toBe(
|
|
91
|
+
'[Current state of "note" (id: "n1"): {"v":1}]\n' +
|
|
92
|
+
'[Current state of "board" (id: "b1"): {"v":2}]\n\n',
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("respects a custom format function", () => {
|
|
97
|
+
const out = unstable_injectInteractableContext(
|
|
98
|
+
[userMsg([{ name: "note", id: "n1", state: { v: 1 } }])],
|
|
99
|
+
(item) => `X:${item.name}`,
|
|
100
|
+
);
|
|
101
|
+
expect(textOf(out[0]!.parts[0])).toBe("X:note\n\n");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("leaves messages with an empty interactables array unchanged", () => {
|
|
105
|
+
const input = [userMsg([])];
|
|
106
|
+
expect(unstable_injectInteractableContext(input)[0]).toBe(input[0]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("does not throw when a user message has no parts", () => {
|
|
110
|
+
const input = [
|
|
111
|
+
{
|
|
112
|
+
id: "m1",
|
|
113
|
+
role: "user",
|
|
114
|
+
metadata: {
|
|
115
|
+
custom: {
|
|
116
|
+
interactables: [{ name: "note", id: "n1", state: { v: 1 } }],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
} as unknown as UIMessage,
|
|
120
|
+
];
|
|
121
|
+
const run = () => unstable_injectInteractableContext(input);
|
|
122
|
+
expect(run).not.toThrow();
|
|
123
|
+
expect(textOf(run()[0]!.parts[0])).toBe(
|
|
124
|
+
'[Current state of "note" (id: "n1"): {"v":1}]\n\n',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("is idempotent on its own (does not double-inject)", () => {
|
|
129
|
+
const once = unstable_injectInteractableContext([
|
|
130
|
+
userMsg([{ name: "note", id: "n1", state: { v: 1 } }]),
|
|
131
|
+
]);
|
|
132
|
+
const twice = unstable_injectInteractableContext(once);
|
|
133
|
+
expect(twice[0]).toBe(once[0]);
|
|
134
|
+
expect(twice[0]!.parts.filter(isStateText)).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("composition with injectQuoteContext", () => {
|
|
138
|
+
const both = () =>
|
|
139
|
+
userMsg([{ name: "note", id: "n1", state: { v: 1 } }], {
|
|
140
|
+
quote: { text: "quoted" },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("stacks both injections in a fixed order over source messages", () => {
|
|
144
|
+
// Apply interactable first, then quote — quote ends up outermost.
|
|
145
|
+
const out = injectQuoteContext(
|
|
146
|
+
unstable_injectInteractableContext([both()]),
|
|
147
|
+
);
|
|
148
|
+
const parts = out[0]!.parts;
|
|
149
|
+
expect(textOf(parts[0])).toBe("> quoted\n\n");
|
|
150
|
+
expect(isStateText(parts[1])).toBe(true);
|
|
151
|
+
expect(textOf(parts[2])).toBe("hello");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("is NOT idempotent across re-application: run injectors once over un-injected messages", () => {
|
|
155
|
+
// Each injector's `alreadyInjected` guard only inspects parts[0]. Once the
|
|
156
|
+
// other injector sits at parts[0], the guard misses and re-prepends. So the
|
|
157
|
+
// composition must run once over source messages, never re-run over output.
|
|
158
|
+
const once = injectQuoteContext(
|
|
159
|
+
unstable_injectInteractableContext([both()]),
|
|
160
|
+
);
|
|
161
|
+
const twice = injectQuoteContext(
|
|
162
|
+
unstable_injectInteractableContext(once),
|
|
163
|
+
);
|
|
164
|
+
expect(twice[0]!.parts.filter(isStateText)).toHaveLength(2);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import {
|
|
3
|
+
unstable_formatInteractableSnapshot,
|
|
4
|
+
unstable_getInteractableSnapshots,
|
|
5
|
+
type Unstable_InteractableSnapshotEntry,
|
|
6
|
+
} from "@ulearning/ai-core";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Injects interactable state snapshots into messages as model-visible text.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors {@link injectQuoteContext}: reads the frozen snapshot stamped on a user
|
|
12
|
+
* message's `metadata.custom.interactables` (by the interactables scope at send
|
|
13
|
+
* time) and prepends a text part. Run this in your route handler before
|
|
14
|
+
* `convertToModelMessages`, which otherwise ignores `metadata.custom`.
|
|
15
|
+
*
|
|
16
|
+
* Wording is consumer-owned — pass `format` to control how each snapshot reads.
|
|
17
|
+
* A snapshot may originate from a user edit or an agent `update_*` call, so the
|
|
18
|
+
* default phrasing is neutral. Keep the instance id visible in custom wording:
|
|
19
|
+
* the model needs it to address the `update_*` tool's `id` parameter. A custom
|
|
20
|
+
* `format` must also handle entries with `partial: true`, whose `state` carries
|
|
21
|
+
* only the fields that changed since the model's last known state.
|
|
22
|
+
*
|
|
23
|
+
* @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { convertToModelMessages, streamText } from "ai";
|
|
28
|
+
* import { unstable_injectInteractableContext } from "@ulearning/ai-sdk-adapter";
|
|
29
|
+
*
|
|
30
|
+
* export async function POST(req: Request) {
|
|
31
|
+
* const { messages } = await req.json();
|
|
32
|
+
* const result = streamText({
|
|
33
|
+
* abortSignal: req.signal,
|
|
34
|
+
* model: myModel,
|
|
35
|
+
* messages: await convertToModelMessages(unstable_injectInteractableContext(messages)),
|
|
36
|
+
* });
|
|
37
|
+
* return result.toUIMessageStreamResponse();
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function unstable_injectInteractableContext(
|
|
42
|
+
messages: UIMessage[],
|
|
43
|
+
format: (
|
|
44
|
+
item: Unstable_InteractableSnapshotEntry,
|
|
45
|
+
) => string = unstable_formatInteractableSnapshot,
|
|
46
|
+
): UIMessage[] {
|
|
47
|
+
return messages.map((msg) => {
|
|
48
|
+
if (msg.role !== "user") return msg;
|
|
49
|
+
|
|
50
|
+
const items = unstable_getInteractableSnapshots(msg);
|
|
51
|
+
if (!items?.length) return msg;
|
|
52
|
+
|
|
53
|
+
const text = `${items.map(format).join("\n")}\n\n`;
|
|
54
|
+
|
|
55
|
+
const alreadyInjected =
|
|
56
|
+
msg.parts?.[0]?.type === "text" && msg.parts[0].text === text;
|
|
57
|
+
if (alreadyInjected) return msg;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...msg,
|
|
61
|
+
parts: [{ type: "text" as const, text }, ...(msg.parts ?? [])],
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { injectQuoteContext } from "./injectQuoteContext";
|
|
4
|
+
|
|
5
|
+
const textOf = (part: unknown) => (part as { text?: string }).text;
|
|
6
|
+
|
|
7
|
+
describe("injectQuoteContext", () => {
|
|
8
|
+
it("prepends the quote as a markdown blockquote", () => {
|
|
9
|
+
const out = injectQuoteContext([
|
|
10
|
+
{
|
|
11
|
+
id: "m1",
|
|
12
|
+
role: "user",
|
|
13
|
+
parts: [{ type: "text", text: "hello" }],
|
|
14
|
+
metadata: { custom: { quote: { text: "quoted" } } },
|
|
15
|
+
} as unknown as UIMessage,
|
|
16
|
+
]);
|
|
17
|
+
expect(textOf(out[0]!.parts[0])).toBe("> quoted\n\n");
|
|
18
|
+
expect(textOf(out[0]!.parts[1])).toBe("hello");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not throw when a user message has no parts", () => {
|
|
22
|
+
const input = [
|
|
23
|
+
{
|
|
24
|
+
id: "m1",
|
|
25
|
+
role: "user",
|
|
26
|
+
metadata: { custom: { quote: { text: "quoted" } } },
|
|
27
|
+
} as unknown as UIMessage,
|
|
28
|
+
];
|
|
29
|
+
const run = () => injectQuoteContext(input);
|
|
30
|
+
expect(run).not.toThrow();
|
|
31
|
+
expect(textOf(run()[0]!.parts[0])).toBe("> quoted\n\n");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
const getQuoteText = (metadata: unknown): string | undefined => {
|
|
4
|
+
if (!metadata || typeof metadata !== "object") return undefined;
|
|
5
|
+
|
|
6
|
+
const custom = (metadata as Record<string, unknown>).custom;
|
|
7
|
+
if (!custom || typeof custom !== "object") return undefined;
|
|
8
|
+
|
|
9
|
+
const quote = (custom as Record<string, unknown>).quote;
|
|
10
|
+
if (!quote || typeof quote !== "object") return undefined;
|
|
11
|
+
|
|
12
|
+
const text = (quote as Record<string, unknown>).text;
|
|
13
|
+
return typeof text === "string" ? text : undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Injects quote context into messages as markdown blockquotes.
|
|
18
|
+
*
|
|
19
|
+
* Use this in your route handler before `convertToModelMessages` so the LLM
|
|
20
|
+
* sees the quoted text that the user is referring to.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { convertToModelMessages, streamText } from "ai";
|
|
25
|
+
* import { injectQuoteContext } from "@ulearning/ai-sdk-adapter";
|
|
26
|
+
*
|
|
27
|
+
* export async function POST(req: Request) {
|
|
28
|
+
* const { messages } = await req.json();
|
|
29
|
+
* const result = streamText({
|
|
30
|
+
* abortSignal: req.signal,
|
|
31
|
+
* model: myModel,
|
|
32
|
+
* messages: await convertToModelMessages(injectQuoteContext(messages)),
|
|
33
|
+
* });
|
|
34
|
+
* return result.toUIMessageStreamResponse();
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function injectQuoteContext(messages: UIMessage[]): UIMessage[] {
|
|
39
|
+
return messages.map((msg) => {
|
|
40
|
+
if (msg.role !== "user") return msg;
|
|
41
|
+
|
|
42
|
+
const text = getQuoteText(msg.metadata);
|
|
43
|
+
if (!text) return msg;
|
|
44
|
+
|
|
45
|
+
const blockquote = text
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.map((line) => `> ${line}`)
|
|
48
|
+
.join("\n");
|
|
49
|
+
|
|
50
|
+
const alreadyInjected =
|
|
51
|
+
msg.parts?.[0]?.type === "text" &&
|
|
52
|
+
msg.parts[0].text === `${blockquote}\n\n`;
|
|
53
|
+
if (alreadyInjected) return msg;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...msg,
|
|
57
|
+
parts: [
|
|
58
|
+
{ type: "text" as const, text: `${blockquote}\n\n` },
|
|
59
|
+
...(msg.parts ?? []),
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|