@copilotkit/vue 2.0.0-rc.7 → 2.0.0-rc.8
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/index.cjs +1 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +292 -16947
- package/dist/index.mjs.map +1 -1
- package/dist/use-render-activity-message-C5LCG9a9.js +16451 -0
- package/dist/use-render-activity-message-C5LCG9a9.js.map +1 -0
- package/dist/use-render-activity-message-CPzguixR.cjs +74 -0
- package/dist/use-render-activity-message-CPzguixR.cjs.map +1 -0
- package/dist/v2/components/LicenseWarningBanner.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAttachmentRenderer.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +6 -270
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +0 -1
- package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
- package/dist/v2/components/chat/index.d.ts +0 -1
- package/dist/v2/components/chat/index.d.ts.map +1 -1
- package/dist/v2/components/chat/tool-call-display-context.d.ts +2 -19
- package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -1
- package/dist/v2/components/chat/tool-call-group-parts.d.ts +7 -20
- package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -1
- package/dist/v2/components/chat/turn-work.d.ts +0 -2
- package/dist/v2/components/chat/turn-work.d.ts.map +1 -1
- package/dist/v2/components/chat/types.d.ts +1 -2
- package/dist/v2/components/chat/types.d.ts.map +1 -1
- package/dist/v2/components/chat/use-chat-labels.d.ts +8 -0
- package/dist/v2/components/chat/use-chat-labels.d.ts.map +1 -0
- package/dist/v2/components/icons/index.d.ts +968 -1
- package/dist/v2/components/icons/index.d.ts.map +1 -1
- package/dist/v2/hooks/use-frontend-tool.d.ts +5 -0
- package/dist/v2/hooks/use-frontend-tool.d.ts.map +1 -1
- package/dist/v2/hooks/use-render-activity-message.d.ts +13 -9
- package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
- package/dist/v2/hooks/use-render-custom-messages.d.ts +5 -7
- package/dist/v2/hooks/use-render-custom-messages.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +196 -199
- package/dist/v2/index.mjs.map +1 -1
- package/dist/v2/lib/vue-core.d.ts +0 -1
- package/dist/v2/lib/vue-core.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/types.d.ts +3 -46
- package/dist/v2/providers/types.d.ts.map +1 -1
- package/dist/v2/types/human-in-the-loop.d.ts +8 -29
- package/dist/v2/types/human-in-the-loop.d.ts.map +1 -1
- package/dist/v2/types/interrupt.d.ts +1 -22
- package/dist/v2/types/interrupt.d.ts.map +1 -1
- package/dist/v2/types/vue-activity-message-renderer.d.ts +16 -15
- package/dist/v2/types/vue-activity-message-renderer.d.ts.map +1 -1
- package/dist/v2/types/vue-custom-message-renderer.d.ts +7 -16
- package/dist/v2/types/vue-custom-message-renderer.d.ts.map +1 -1
- package/dist/v2/types/vue-tool-call-renderer.d.ts +16 -35
- package/dist/v2/types/vue-tool-call-renderer.d.ts.map +1 -1
- package/package.json +7 -8
- package/src/__tests__/build-entries.test.ts +27 -0
- package/src/v2/components/LicenseWarningBanner.vue +31 -15
- package/src/v2/components/__tests__/LicenseWarningBanner.test.ts +20 -0
- package/src/v2/components/chat/CopilotChat.vue +19 -40
- package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +14 -10
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.vue +14 -6
- package/src/v2/components/chat/CopilotChatInput.vue +40 -30
- package/src/v2/components/chat/CopilotChatMessageView.vue +72 -140
- package/src/v2/components/chat/CopilotChatReasoningMessage.vue +31 -13
- package/src/v2/components/chat/CopilotChatToolCallGroup.vue +60 -37
- package/src/v2/components/chat/CopilotChatToolCallItem.vue +3 -3
- package/src/v2/components/chat/CopilotChatToolCallsView.vue +15 -13
- package/src/v2/components/chat/CopilotChatView.vue +5 -5
- package/src/v2/components/chat/CopilotChatWorkSummary.vue +44 -18
- package/src/v2/components/chat/__tests__/CopilotChat.labels.test.ts +19 -4
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +33 -1
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.ts +247 -403
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.toolCallDisplay.test.ts +1 -99
- package/src/v2/components/chat/copilotChatToolCallItemRender.ts +3 -19
- package/src/v2/components/chat/index.ts +0 -1
- package/src/v2/components/chat/tool-call-display-context.ts +2 -23
- package/src/v2/components/chat/tool-call-group-parts.ts +44 -85
- package/src/v2/components/chat/turn-work.ts +0 -9
- package/src/v2/components/chat/types.ts +4 -4
- package/src/v2/components/chat/use-chat-labels.ts +16 -0
- package/src/v2/components/icons/__tests__/icons.test.ts +72 -68
- package/src/v2/components/icons/index.ts +109 -27
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +62 -1
- package/src/v2/hooks/__tests__/use-render-activity-message.test.ts +105 -0
- package/src/v2/hooks/__tests__/use-render-tool.test.ts +3 -6
- package/src/v2/hooks/__tests__/zod-regression.test.ts +4 -3
- package/src/v2/hooks/use-frontend-tool.ts +10 -10
- package/src/v2/hooks/use-render-activity-message.ts +27 -48
- package/src/v2/hooks/use-render-custom-messages.ts +14 -35
- package/src/v2/lib/vue-core.ts +3 -5
- package/src/v2/providers/CopilotKitProvider.vue +29 -80
- package/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.ts +3 -3
- package/src/v2/providers/__tests__/CopilotKitProvider.learning.test.ts +3 -1
- package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +17 -0
- package/src/v2/providers/types.ts +3 -49
- package/src/v2/types/__tests__/defineToolCallRenderer.test.ts +28 -33
- package/src/v2/types/defineToolCallRenderer.ts +5 -4
- package/src/v2/types/human-in-the-loop.ts +8 -32
- package/src/v2/types/interrupt.ts +7 -31
- package/src/v2/types/vue-activity-message-renderer.ts +24 -15
- package/src/v2/types/vue-custom-message-renderer.ts +12 -16
- package/src/v2/types/vue-tool-call-renderer.ts +23 -38
- package/vite.config.ts +1 -2
- package/dist/v2/components/chat/BasicMarkdown.vue.d.ts +0 -9
- package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +0 -1
- package/dist/v2/components/chat/tool-call-layout.d.ts +0 -115
- package/dist/v2/components/chat/tool-call-layout.d.ts.map +0 -1
- package/dist/v2/lib/transcription-client.d.ts +0 -19
- package/dist/v2/lib/transcription-client.d.ts.map +0 -1
- package/src/v2/components/chat/BasicMarkdown.vue +0 -160
- package/src/v2/components/chat/__tests__/BasicMarkdown.test.ts +0 -69
- package/src/v2/components/chat/tool-call-layout.ts +0 -304
- package/src/v2/lib/__tests__/transcription-client.test.ts +0 -67
- package/src/v2/lib/transcription-client.ts +0 -151
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import type { Message, ToolCall, ToolMessage } from "@ag-ui/core";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* How the chat shows tool calls.
|
|
5
|
-
*
|
|
6
|
-
* - `"grouped"` (default): calls made in a row share one card, collapsed to a
|
|
7
|
-
* summary ("Used 3 tools") that opens to a row per call.
|
|
8
|
-
* - `"line"`: the same grouping as a quiet line of text, like the reasoning
|
|
9
|
-
* message's "Thought for 4 seconds".
|
|
10
|
-
* - `"stacked"`: the latest call on top of a stack of the earlier ones; click
|
|
11
|
-
* to fan the stack out into its cards.
|
|
12
|
-
* - `"turn"`: the agent works in the open (narration, then a row per call),
|
|
13
|
-
* and when the reply is done everything before it folds into "Worked for 9s".
|
|
14
|
-
* - `"separate"`: every call gets its own card, as before grouping existed.
|
|
15
|
-
*
|
|
16
|
-
* Only calls drawn by a renderer that opts into grouping (`group: true`) are
|
|
17
|
-
* grouped; CopilotKit's built-in card from `useDefaultRenderTool()` does. Any
|
|
18
|
-
* other call always shows on its own and splits the group around it.
|
|
19
|
-
*/
|
|
20
|
-
export type CopilotChatToolCallDisplay =
|
|
21
|
-
| "grouped"
|
|
22
|
-
| "line"
|
|
23
|
-
| "stacked"
|
|
24
|
-
| "turn"
|
|
25
|
-
| "separate";
|
|
26
|
-
|
|
27
|
-
/** One tool call and, once it has finished, the message carrying its result. */
|
|
28
|
-
export interface ToolCallEntry {
|
|
29
|
-
toolCall: ToolCall;
|
|
30
|
-
toolMessage?: ToolMessage;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ToolCallSegment =
|
|
34
|
-
/** Consecutive calls drawn by renderers that opted into grouping. */
|
|
35
|
-
| {
|
|
36
|
-
kind: "group";
|
|
37
|
-
id: string;
|
|
38
|
-
calls: ToolCallEntry[];
|
|
39
|
-
/** Nothing renders after this group yet, so a running agent may add to it. */
|
|
40
|
-
trailing: boolean;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* A call drawn by a renderer that didn't opt into grouping, or with no
|
|
44
|
-
* renderer at all. The latter renders nothing (and warns in development)
|
|
45
|
-
* without breaking a group.
|
|
46
|
-
*/
|
|
47
|
-
| { kind: "call"; id: string; call: ToolCallEntry; rendered: boolean };
|
|
48
|
-
|
|
49
|
-
/** What one assistant message's tool-call area renders. */
|
|
50
|
-
export interface ToolCallLayout {
|
|
51
|
-
segments: ToolCallSegment[];
|
|
52
|
-
/** Changes whenever anything this layout renders changes; for memoization. */
|
|
53
|
-
signature: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Which tool-call renderers are registered. */
|
|
57
|
-
export interface ToolCallRendererNames {
|
|
58
|
-
/** Tool names with a renderer of their own. */
|
|
59
|
-
named: ReadonlySet<string>;
|
|
60
|
-
/** Of those, the ones whose renderer opted into grouping. */
|
|
61
|
-
groupedNames: ReadonlySet<string>;
|
|
62
|
-
/** A wildcard (`"*"`) renderer is registered. */
|
|
63
|
-
wildcard: boolean;
|
|
64
|
-
/** The wildcard renderer opted into grouping (the built-in card does). */
|
|
65
|
-
wildcardGroups: boolean;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function contentLength(content: ToolMessage["content"]): number {
|
|
69
|
-
return typeof content === "string"
|
|
70
|
-
? content.length
|
|
71
|
-
: JSON.stringify(content ?? "").length;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const entrySignature = ({ toolCall, toolMessage }: ToolCallEntry) =>
|
|
75
|
-
[
|
|
76
|
-
toolCall.id,
|
|
77
|
-
toolCall.function.name,
|
|
78
|
-
toolCall.function.arguments.length,
|
|
79
|
-
toolMessage
|
|
80
|
-
? `${contentLength(toolMessage.content)}${toolMessage.error ? "!" : ""}`
|
|
81
|
-
: "-",
|
|
82
|
-
].join(":");
|
|
83
|
-
|
|
84
|
-
function signatureOf(segments: ToolCallSegment[]): string {
|
|
85
|
-
return segments
|
|
86
|
-
.map((segment) =>
|
|
87
|
-
segment.kind === "call"
|
|
88
|
-
? `c(${entrySignature(segment.call)})`
|
|
89
|
-
: `g${segment.trailing ? "*" : ""}(${segment.calls.map(entrySignature).join(",")})`,
|
|
90
|
-
)
|
|
91
|
-
.join("|");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Index of tool results by the call they answer. */
|
|
95
|
-
export function getToolResults(messages: Message[]): Map<string, ToolMessage> {
|
|
96
|
-
const results = new Map<string, ToolMessage>();
|
|
97
|
-
for (const message of messages) {
|
|
98
|
-
if (message.role === "tool") results.set(message.toolCallId, message);
|
|
99
|
-
}
|
|
100
|
-
return results;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Lays out the tool calls of every assistant message in `rendered`.
|
|
105
|
-
*
|
|
106
|
-
* Calls drawn by a renderer that opted into grouping join the group that's
|
|
107
|
-
* open; anything else that shows between them closes it: assistant text, a
|
|
108
|
-
* user, reasoning or activity message, or a call drawn by any other renderer. A group belongs to
|
|
109
|
-
* the message where it starts, so later messages whose calls joined it render
|
|
110
|
-
* no tool calls themselves.
|
|
111
|
-
*
|
|
112
|
-
* @param rendered - The messages as the chat renders them, in order.
|
|
113
|
-
* @param all - Every message, for finding tool results (a transform may hide them).
|
|
114
|
-
* @param renderers - The registered renderers.
|
|
115
|
-
*/
|
|
116
|
-
export function getToolCallLayouts(
|
|
117
|
-
rendered: Message[],
|
|
118
|
-
all: Message[],
|
|
119
|
-
renderers: ToolCallRendererNames,
|
|
120
|
-
): Map<string, ToolCallLayout> {
|
|
121
|
-
const results = getToolResults(all);
|
|
122
|
-
const layouts = new Map<string, ToolCallSegment[]>();
|
|
123
|
-
let open: Extract<ToolCallSegment, { kind: "group" }> | undefined;
|
|
124
|
-
|
|
125
|
-
for (const message of rendered) {
|
|
126
|
-
if (message.role === "tool") continue;
|
|
127
|
-
if (message.role !== "assistant") {
|
|
128
|
-
open = undefined;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (typeof message.content === "string" && message.content.trim()) {
|
|
132
|
-
open = undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const segments: ToolCallSegment[] = [];
|
|
136
|
-
layouts.set(message.id, segments);
|
|
137
|
-
// Streaming can repeat a call: keep its first position, last content.
|
|
138
|
-
const toolCalls = new Map<string, ToolCall>();
|
|
139
|
-
for (const toolCall of message.toolCalls ?? []) {
|
|
140
|
-
toolCalls.set(toolCall.id, toolCall);
|
|
141
|
-
}
|
|
142
|
-
for (const toolCall of toolCalls.values()) {
|
|
143
|
-
const entry = { toolCall, toolMessage: results.get(toolCall.id) };
|
|
144
|
-
|
|
145
|
-
const name = toolCall.function.name;
|
|
146
|
-
const named = renderers.named.has(name);
|
|
147
|
-
const rendered = named || renderers.wildcard;
|
|
148
|
-
const groups = named
|
|
149
|
-
? renderers.groupedNames.has(name)
|
|
150
|
-
: renderers.wildcardGroups;
|
|
151
|
-
|
|
152
|
-
if (!rendered || !groups) {
|
|
153
|
-
if (rendered) open = undefined;
|
|
154
|
-
segments.push({ kind: "call", id: toolCall.id, call: entry, rendered });
|
|
155
|
-
} else if (open) {
|
|
156
|
-
open.calls.push(entry);
|
|
157
|
-
} else {
|
|
158
|
-
open = {
|
|
159
|
-
kind: "group",
|
|
160
|
-
id: toolCall.id,
|
|
161
|
-
calls: [entry],
|
|
162
|
-
trailing: false,
|
|
163
|
-
};
|
|
164
|
-
segments.push(open);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (open) open.trailing = true;
|
|
169
|
-
|
|
170
|
-
const layout = new Map<string, ToolCallLayout>();
|
|
171
|
-
for (const [id, segments] of layouts) {
|
|
172
|
-
layout.set(id, { segments, signature: signatureOf(segments) });
|
|
173
|
-
}
|
|
174
|
-
return layout;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const TOOL_CALL_SLOT_PREFIX = "tool-call-";
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Which renderers draw tool calls, from the registered renderers and the
|
|
181
|
-
* view's tool-call slots. A slot wins over a registered renderer and never
|
|
182
|
-
* groups, since the chat can't tell what it draws: `#tool-call-<name>` takes
|
|
183
|
-
* that tool, and `#tool-call` takes every tool.
|
|
184
|
-
*
|
|
185
|
-
* @param renderers - The registered renderers (`copilotkit.renderToolCalls`).
|
|
186
|
-
* @param slotNames - The names of the slots the view was given.
|
|
187
|
-
*/
|
|
188
|
-
export function getToolCallRendererNames(
|
|
189
|
-
renderers: readonly { name: string; group?: boolean }[],
|
|
190
|
-
slotNames: readonly string[] = [],
|
|
191
|
-
): ToolCallRendererNames {
|
|
192
|
-
const wildcardSlot = slotNames.includes("tool-call");
|
|
193
|
-
const slotTools = slotNames
|
|
194
|
-
.filter((name) => name.startsWith(TOOL_CALL_SLOT_PREFIX))
|
|
195
|
-
.map((name) => name.slice(TOOL_CALL_SLOT_PREFIX.length));
|
|
196
|
-
const named = renderers.filter((renderer) => renderer.name !== "*");
|
|
197
|
-
const wildcards = renderers.filter((renderer) => renderer.name === "*");
|
|
198
|
-
return {
|
|
199
|
-
named: new Set([...named.map((renderer) => renderer.name), ...slotTools]),
|
|
200
|
-
groupedNames: new Set(
|
|
201
|
-
wildcardSlot
|
|
202
|
-
? []
|
|
203
|
-
: named
|
|
204
|
-
.filter(
|
|
205
|
-
(renderer) =>
|
|
206
|
-
renderer.group && !slotTools.includes(renderer.name),
|
|
207
|
-
)
|
|
208
|
-
.map((renderer) => renderer.name),
|
|
209
|
-
),
|
|
210
|
-
wildcard: wildcards.length > 0 || wildcardSlot,
|
|
211
|
-
wildcardGroups:
|
|
212
|
-
!wildcardSlot && wildcards.some((renderer) => renderer.group),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** A reply's working phase, for the `"turn"` display. */
|
|
217
|
-
export interface TurnWork {
|
|
218
|
-
/** Stable across the turn's life: the id of the user message it answers. */
|
|
219
|
-
key: string;
|
|
220
|
-
/** The first assistant-side message, where the "Worked for" line goes. */
|
|
221
|
-
firstMessageId: string;
|
|
222
|
-
/** Everything up to and including the last message with a tool call. */
|
|
223
|
-
workMessageIds: ReadonlySet<string>;
|
|
224
|
-
/**
|
|
225
|
-
* The reply's last assistant message, when it has text and is part of the
|
|
226
|
-
* work (it also calls a tool). Its text is the answer, so it stays out.
|
|
227
|
-
*/
|
|
228
|
-
replyTextMessageId?: string;
|
|
229
|
-
calls: ToolCallEntry[];
|
|
230
|
-
/** No user message follows, so the agent may still be working on it. */
|
|
231
|
-
isLatest: boolean;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Finds the working phase of every reply in `rendered`: its messages up to the
|
|
236
|
-
* last one that calls a tool. Text in there was narration; what follows is the
|
|
237
|
-
* reply itself. Replies that called no tools are omitted, except the latest,
|
|
238
|
-
* which may be about to.
|
|
239
|
-
*/
|
|
240
|
-
export function getTurnWork(
|
|
241
|
-
rendered: Message[],
|
|
242
|
-
layouts: ReadonlyMap<string, ToolCallLayout>,
|
|
243
|
-
): Map<string, TurnWork> {
|
|
244
|
-
const turns = new Map<string, TurnWork>();
|
|
245
|
-
let start = 0;
|
|
246
|
-
while (start < rendered.length) {
|
|
247
|
-
let end = start;
|
|
248
|
-
while (end < rendered.length && rendered[end]!.role !== "user") end++;
|
|
249
|
-
const key = start > 0 ? rendered[start - 1]!.id : "start";
|
|
250
|
-
const visible = rendered
|
|
251
|
-
.slice(start, end)
|
|
252
|
-
.filter((message) => message.role !== "tool");
|
|
253
|
-
const isLatest = end === rendered.length;
|
|
254
|
-
|
|
255
|
-
// A message's calls may sit in a group an earlier message owns, so
|
|
256
|
-
// collect every shown call first, then find the last message making one.
|
|
257
|
-
const calls: ToolCallEntry[] = [];
|
|
258
|
-
for (const message of visible) {
|
|
259
|
-
for (const segment of layouts.get(message.id)?.segments ?? []) {
|
|
260
|
-
if (segment.kind === "group") calls.push(...segment.calls);
|
|
261
|
-
else if (segment.rendered) calls.push(segment.call);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const shown = new Set(calls.map((entry) => entry.toolCall.id));
|
|
265
|
-
let lastWork = -1;
|
|
266
|
-
visible.forEach((message, index) => {
|
|
267
|
-
if (
|
|
268
|
-
message.role === "assistant" &&
|
|
269
|
-
message.toolCalls?.some((call) => shown.has(call.id))
|
|
270
|
-
) {
|
|
271
|
-
lastWork = index;
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const first = visible[0];
|
|
276
|
-
if (first && (calls.length > 0 || isLatest)) {
|
|
277
|
-
let lastAssistant = -1;
|
|
278
|
-
visible.forEach((message, index) => {
|
|
279
|
-
if (message.role === "assistant") lastAssistant = index;
|
|
280
|
-
});
|
|
281
|
-
const replyText = visible[lastAssistant]?.content;
|
|
282
|
-
const endsWithWork =
|
|
283
|
-
lastAssistant >= 0 &&
|
|
284
|
-
lastAssistant <= lastWork &&
|
|
285
|
-
typeof replyText === "string" &&
|
|
286
|
-
replyText.trim().length > 0;
|
|
287
|
-
|
|
288
|
-
turns.set(key, {
|
|
289
|
-
key,
|
|
290
|
-
firstMessageId: first.id,
|
|
291
|
-
workMessageIds: new Set(
|
|
292
|
-
visible.slice(0, lastWork + 1).map((message) => message.id),
|
|
293
|
-
),
|
|
294
|
-
replyTextMessageId: endsWithWork
|
|
295
|
-
? visible[lastAssistant]!.id
|
|
296
|
-
: undefined,
|
|
297
|
-
calls,
|
|
298
|
-
isLatest,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
start = end + 1;
|
|
302
|
-
}
|
|
303
|
-
return turns;
|
|
304
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { TranscriptionErrorCode } from "@copilotkit/shared";
|
|
3
|
-
import type { TranscriptionError } from "../transcription-client";
|
|
4
|
-
import { transcribeAudio } from "../transcription-client";
|
|
5
|
-
import type { CopilotKitCoreVue } from "../vue-core";
|
|
6
|
-
|
|
7
|
-
describe("transcription-client", () => {
|
|
8
|
-
const originalFetch = globalThis.fetch;
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
globalThis.fetch = originalFetch;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("transcribes audio in REST mode", async () => {
|
|
15
|
-
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
16
|
-
ok: true,
|
|
17
|
-
json: async () => ({
|
|
18
|
-
text: "Transcribed text",
|
|
19
|
-
size: 12,
|
|
20
|
-
type: "audio/webm",
|
|
21
|
-
}),
|
|
22
|
-
} satisfies Partial<Response>) as typeof fetch;
|
|
23
|
-
|
|
24
|
-
const result = await transcribeAudio(
|
|
25
|
-
{
|
|
26
|
-
runtimeUrl: "/api/copilotkit",
|
|
27
|
-
runtimeTransport: "rest",
|
|
28
|
-
headers: {},
|
|
29
|
-
resolveHeaders: async () => ({}),
|
|
30
|
-
} as CopilotKitCoreVue,
|
|
31
|
-
new Blob(["audio"], { type: "audio/webm" }),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
35
|
-
expect(result.text).toBe("Transcribed text");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("maps structured error responses to TranscriptionError", async () => {
|
|
39
|
-
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
40
|
-
ok: false,
|
|
41
|
-
status: 429,
|
|
42
|
-
statusText: "Too Many Requests",
|
|
43
|
-
json: async () => ({
|
|
44
|
-
error: TranscriptionErrorCode.RATE_LIMITED,
|
|
45
|
-
message: "Rate limited",
|
|
46
|
-
retryable: true,
|
|
47
|
-
}),
|
|
48
|
-
} satisfies Partial<Response>) as typeof fetch;
|
|
49
|
-
|
|
50
|
-
await expect(
|
|
51
|
-
transcribeAudio(
|
|
52
|
-
{
|
|
53
|
-
runtimeUrl: "/api/copilotkit",
|
|
54
|
-
runtimeTransport: "rest",
|
|
55
|
-
headers: {},
|
|
56
|
-
resolveHeaders: async () => ({}),
|
|
57
|
-
} as CopilotKitCoreVue,
|
|
58
|
-
new Blob(["audio"], { type: "audio/webm" }),
|
|
59
|
-
),
|
|
60
|
-
).rejects.toMatchObject({
|
|
61
|
-
info: {
|
|
62
|
-
code: TranscriptionErrorCode.RATE_LIMITED,
|
|
63
|
-
retryable: true,
|
|
64
|
-
},
|
|
65
|
-
} satisfies Partial<TranscriptionError>);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import type { CopilotKitCoreVue } from "./vue-core";
|
|
2
|
-
import { TranscriptionErrorCode } from "@copilotkit/shared";
|
|
3
|
-
import type { TranscriptionErrorResponse } from "@copilotkit/shared";
|
|
4
|
-
|
|
5
|
-
export interface TranscriptionResult {
|
|
6
|
-
text: string;
|
|
7
|
-
size: number;
|
|
8
|
-
type: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TranscriptionErrorInfo {
|
|
12
|
-
code: TranscriptionErrorCode;
|
|
13
|
-
message: string;
|
|
14
|
-
retryable: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export { TranscriptionErrorCode };
|
|
18
|
-
|
|
19
|
-
async function blobToBase64(blob: Blob): Promise<string> {
|
|
20
|
-
return await new Promise((resolve, reject) => {
|
|
21
|
-
const reader = new FileReader();
|
|
22
|
-
reader.onloadend = () => {
|
|
23
|
-
const result = reader.result as string;
|
|
24
|
-
resolve(result.split(",")[1] ?? "");
|
|
25
|
-
};
|
|
26
|
-
reader.addEventListener("error", () =>
|
|
27
|
-
reject(new Error("Failed to read audio data")),
|
|
28
|
-
);
|
|
29
|
-
reader.readAsDataURL(blob);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isTranscriptionErrorResponse(
|
|
34
|
-
data: unknown,
|
|
35
|
-
): data is TranscriptionErrorResponse {
|
|
36
|
-
return (
|
|
37
|
-
typeof data === "object" &&
|
|
38
|
-
data !== null &&
|
|
39
|
-
"error" in data &&
|
|
40
|
-
"message" in data &&
|
|
41
|
-
typeof (data as TranscriptionErrorResponse).error === "string" &&
|
|
42
|
-
typeof (data as TranscriptionErrorResponse).message === "string"
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseTranscriptionError(
|
|
47
|
-
response: TranscriptionErrorResponse,
|
|
48
|
-
): TranscriptionErrorInfo {
|
|
49
|
-
return {
|
|
50
|
-
code: response.error,
|
|
51
|
-
message: response.message,
|
|
52
|
-
retryable: response.retryable ?? false,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class TranscriptionError extends Error {
|
|
57
|
-
public readonly info: TranscriptionErrorInfo;
|
|
58
|
-
|
|
59
|
-
constructor(info: TranscriptionErrorInfo) {
|
|
60
|
-
super(info.message);
|
|
61
|
-
this.name = "TranscriptionError";
|
|
62
|
-
this.info = info;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function transcribeAudio(
|
|
67
|
-
core: CopilotKitCoreVue,
|
|
68
|
-
audioBlob: Blob,
|
|
69
|
-
filename: string = "recording.webm",
|
|
70
|
-
): Promise<TranscriptionResult> {
|
|
71
|
-
const runtimeUrl = core.runtimeUrl;
|
|
72
|
-
if (!runtimeUrl) {
|
|
73
|
-
throw new TranscriptionError({
|
|
74
|
-
code: TranscriptionErrorCode.INVALID_REQUEST,
|
|
75
|
-
message: "Runtime URL is not configured",
|
|
76
|
-
retryable: false,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const headers: Record<string, string> = { ...(await core.resolveHeaders()) };
|
|
81
|
-
let response: Response;
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
if (core.runtimeTransport === "single") {
|
|
85
|
-
const base64Audio = await blobToBase64(audioBlob);
|
|
86
|
-
|
|
87
|
-
headers["Content-Type"] = "application/json";
|
|
88
|
-
|
|
89
|
-
response = await fetch(runtimeUrl, {
|
|
90
|
-
method: "POST",
|
|
91
|
-
headers,
|
|
92
|
-
body: JSON.stringify({
|
|
93
|
-
method: "transcribe",
|
|
94
|
-
body: {
|
|
95
|
-
audio: base64Audio,
|
|
96
|
-
mimeType: audioBlob.type || "audio/webm",
|
|
97
|
-
filename,
|
|
98
|
-
},
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
} else {
|
|
102
|
-
delete headers["Content-Type"];
|
|
103
|
-
|
|
104
|
-
const formData = new FormData();
|
|
105
|
-
formData.append("audio", audioBlob, filename);
|
|
106
|
-
|
|
107
|
-
response = await fetch(`${runtimeUrl}/transcribe`, {
|
|
108
|
-
method: "POST",
|
|
109
|
-
headers,
|
|
110
|
-
body: formData,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
throw new TranscriptionError({
|
|
115
|
-
code: TranscriptionErrorCode.NETWORK_ERROR,
|
|
116
|
-
message:
|
|
117
|
-
error instanceof Error ? error.message : "Network request failed",
|
|
118
|
-
retryable: true,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!response.ok) {
|
|
123
|
-
let errorData: unknown;
|
|
124
|
-
try {
|
|
125
|
-
errorData = await response.json();
|
|
126
|
-
} catch {
|
|
127
|
-
throw new TranscriptionError({
|
|
128
|
-
code: TranscriptionErrorCode.PROVIDER_ERROR,
|
|
129
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
130
|
-
retryable: response.status >= 500,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (isTranscriptionErrorResponse(errorData)) {
|
|
135
|
-
throw new TranscriptionError(parseTranscriptionError(errorData));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
throw new TranscriptionError({
|
|
139
|
-
code: TranscriptionErrorCode.PROVIDER_ERROR,
|
|
140
|
-
message:
|
|
141
|
-
typeof errorData === "object" &&
|
|
142
|
-
errorData !== null &&
|
|
143
|
-
"message" in errorData
|
|
144
|
-
? String((errorData as { message: unknown }).message)
|
|
145
|
-
: "Transcription failed",
|
|
146
|
-
retryable: response.status >= 500,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (await response.json()) as TranscriptionResult;
|
|
151
|
-
}
|