@aleph-alpha/chat-kit 1.3.0 → 1.4.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/dist/{CkInput.vue_vue_type_script_setup_true_lang-BoUxOQbH.js → CkInput.vue_vue_type_script_setup_true_lang-C5C09u0z.js} +13 -12
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/responses-api.d.ts +180 -0
- package/dist/adapters/responses-api.d.ts.map +1 -0
- package/dist/adapters/responses-api.js +153 -0
- package/dist/chat-kit.css +2 -11
- package/dist/components/base/CkFeedbackButton/CkFeedbackButton.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +11 -3
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +5 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/composables/useConversations.d.ts +12 -0
- package/dist/composables/useConversations.d.ts.map +1 -1
- package/dist/favicon.ico +0 -0
- package/dist/generateId-BHf0W4Rq.js +6 -0
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useMessageHandler.d.ts +2 -2
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -8
- package/dist/services/index.d.ts +0 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -37
- package/dist/services/types.d.ts +2 -2
- package/dist/services/types.d.ts.map +1 -1
- package/dist/types.d.ts +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/{useConversationSessions-ulFhtpQr.js → useConversationSessions-GLZV_Ctr.js} +18 -2
- package/dist/{useMessageHandler-BmBdu2EW.js → useMessageHandler-ZRC-gIOJ.js} +5 -5
- package/package.json +11 -2
- package/src/adapters/index.ts +17 -0
- package/src/adapters/responses-api.spec.ts +365 -0
- package/src/adapters/responses-api.ts +444 -0
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +8 -24
- package/src/components/composed/CkConversation/CkConversation.spec.ts +171 -25
- package/src/components/composed/CkConversation/CkConversation.stories.ts +266 -46
- package/src/components/composed/CkConversation/CkConversation.vue +19 -7
- package/src/composables/useConversations.spec.ts +118 -0
- package/src/composables/useConversations.ts +32 -0
- package/src/handlers/useChatKitLabels.spec.ts +16 -2
- package/src/handlers/useMessageHandler.spec.ts +112 -44
- package/src/handlers/useMessageHandler.ts +16 -2
- package/src/helpers/conversationTree.spec.ts +46 -19
- package/src/index.ts +11 -2
- package/src/services/index.ts +0 -2
- package/src/services/types.ts +2 -2
- package/src/types.ts +55 -2
- package/dist/ChatService-LogwJNxh.js +0 -9
- package/dist/services/ChatService.d.ts +0 -42
- package/dist/services/ChatService.d.ts.map +0 -1
- package/dist/services/MockChatService.d.ts +0 -10
- package/dist/services/MockChatService.d.ts.map +0 -1
- package/src/services/ChatService.ts +0 -60
- package/src/services/MockChatService.ts +0 -47
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { useMessageHandler } from '../handlers/useMessageHandler';
|
|
2
|
+
import type { MessageContent, MessageTextContent } from '../types';
|
|
3
|
+
import {
|
|
4
|
+
responsesToTree,
|
|
5
|
+
type ResponseOutputMessage,
|
|
6
|
+
type ResponseOutputReasoning,
|
|
7
|
+
type ResponseOutputTool,
|
|
8
|
+
type StoredResponse,
|
|
9
|
+
} from './responses-api';
|
|
10
|
+
|
|
11
|
+
function getText(content: MessageContent[]): string {
|
|
12
|
+
return content
|
|
13
|
+
.filter((c): c is MessageTextContent => c.type === 'text')
|
|
14
|
+
.map((c) => c.text)
|
|
15
|
+
.join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeResponse(
|
|
19
|
+
overrides: Partial<StoredResponse> & { id: string },
|
|
20
|
+
): StoredResponse {
|
|
21
|
+
return {
|
|
22
|
+
object: 'response',
|
|
23
|
+
created_at: 1_777_456_700,
|
|
24
|
+
completed_at: 1_777_456_701,
|
|
25
|
+
status: 'completed',
|
|
26
|
+
model: 'pharia-1',
|
|
27
|
+
output: [],
|
|
28
|
+
request_input: [],
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function userInput(text: string): Record<string, unknown> {
|
|
34
|
+
return { role: 'user', content: text };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assistantOutput(id: string, text: string): ResponseOutputMessage {
|
|
38
|
+
return {
|
|
39
|
+
type: 'message',
|
|
40
|
+
id,
|
|
41
|
+
status: 'completed',
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
content: [{ type: 'output_text', text }],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function reasoningOutput(id: string, text: string): ResponseOutputReasoning {
|
|
48
|
+
return {
|
|
49
|
+
type: 'reasoning',
|
|
50
|
+
id,
|
|
51
|
+
status: 'completed',
|
|
52
|
+
role: null,
|
|
53
|
+
content: [{ type: 'reasoning_text', text }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toolOutput(
|
|
58
|
+
overrides: Partial<ResponseOutputTool> & { id: string },
|
|
59
|
+
): ResponseOutputTool {
|
|
60
|
+
return {
|
|
61
|
+
type: 'mcp_list_tools',
|
|
62
|
+
server_label: 'firecrawl',
|
|
63
|
+
tools: [],
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('responsesToTree', () => {
|
|
69
|
+
it('returns an empty tree for an empty response list', () => {
|
|
70
|
+
expect(responsesToTree([])).toEqual({ nodes: {}, rootId: null });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('builds a linear tree from a single response with one user input and one assistant output', () => {
|
|
74
|
+
const tree = responsesToTree([
|
|
75
|
+
makeResponse({
|
|
76
|
+
id: 'r1',
|
|
77
|
+
request_input: [userInput('Hello')],
|
|
78
|
+
output: [assistantOutput('msg-a1', 'Hi there')],
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const ids = Object.keys(tree.nodes);
|
|
83
|
+
expect(ids).toHaveLength(2);
|
|
84
|
+
|
|
85
|
+
const userNode = tree.nodes['user-r1-0']!;
|
|
86
|
+
expect(userNode.role).toBe('user');
|
|
87
|
+
expect(userNode.parentId).toBeNull();
|
|
88
|
+
expect(getText(userNode.content)).toBe('Hello');
|
|
89
|
+
|
|
90
|
+
const assistantNode = tree.nodes['msg-a1']!;
|
|
91
|
+
expect(assistantNode.role).toBe('assistant');
|
|
92
|
+
expect(assistantNode.parentId).toBe('user-r1-0');
|
|
93
|
+
expect(getText(assistantNode.content)).toBe('Hi there');
|
|
94
|
+
|
|
95
|
+
expect(tree.rootId).toBe('user-r1-0');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('chains a follow-up response under the previous assistant message via previous_response_id', () => {
|
|
99
|
+
const tree = responsesToTree([
|
|
100
|
+
makeResponse({
|
|
101
|
+
id: 'r1',
|
|
102
|
+
request_input: [userInput('First')],
|
|
103
|
+
output: [assistantOutput('msg-a1', 'A1')],
|
|
104
|
+
}),
|
|
105
|
+
makeResponse({
|
|
106
|
+
id: 'r2',
|
|
107
|
+
previous_response_id: 'r1',
|
|
108
|
+
request_input: [userInput('Second')],
|
|
109
|
+
output: [assistantOutput('msg-a2', 'A2')],
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
expect(tree.nodes['user-r2-0']!.parentId).toBe('msg-a1');
|
|
114
|
+
expect(tree.nodes['msg-a2']!.parentId).toBe('user-r2-0');
|
|
115
|
+
expect(tree.rootId).toBe('user-r1-0');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('treats sibling responses (same previous_response_id) as branches and surfaces the latest as active', () => {
|
|
119
|
+
const tree = responsesToTree([
|
|
120
|
+
makeResponse({
|
|
121
|
+
id: 'r1',
|
|
122
|
+
request_input: [userInput('Hello')],
|
|
123
|
+
output: [assistantOutput('msg-a1', 'Hi')],
|
|
124
|
+
}),
|
|
125
|
+
makeResponse({
|
|
126
|
+
id: 'r2',
|
|
127
|
+
previous_response_id: 'r1',
|
|
128
|
+
request_input: [userInput('Branch A')],
|
|
129
|
+
output: [assistantOutput('msg-a2', 'A')],
|
|
130
|
+
}),
|
|
131
|
+
makeResponse({
|
|
132
|
+
id: 'r3',
|
|
133
|
+
previous_response_id: 'r1',
|
|
134
|
+
request_input: [userInput('Branch B')],
|
|
135
|
+
output: [assistantOutput('msg-a3', 'B')],
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const a1 = tree.nodes['msg-a1']!;
|
|
140
|
+
expect(a1.childrenIds).toEqual(['user-r2-0', 'user-r3-0']);
|
|
141
|
+
expect(a1.activeChildIndex).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('skips responses with no visible content but keeps descendants attached to the parent tail', () => {
|
|
145
|
+
const tree = responsesToTree([
|
|
146
|
+
makeResponse({
|
|
147
|
+
id: 'r1',
|
|
148
|
+
request_input: [userInput('Hello')],
|
|
149
|
+
output: [assistantOutput('msg-a1', 'Hi')],
|
|
150
|
+
}),
|
|
151
|
+
makeResponse({
|
|
152
|
+
id: 'r2',
|
|
153
|
+
previous_response_id: 'r1',
|
|
154
|
+
request_input: [],
|
|
155
|
+
output: [],
|
|
156
|
+
}),
|
|
157
|
+
makeResponse({
|
|
158
|
+
id: 'r3',
|
|
159
|
+
previous_response_id: 'r2',
|
|
160
|
+
request_input: [userInput('Follow-up')],
|
|
161
|
+
output: [assistantOutput('msg-a3', 'Reply')],
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
expect(tree.nodes['user-r3-0']!.parentId).toBe('msg-a1');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('extracts text from string content as well as output_text / text parts', () => {
|
|
169
|
+
const tree = responsesToTree([
|
|
170
|
+
makeResponse({
|
|
171
|
+
id: 'r1',
|
|
172
|
+
request_input: [userInput('plain string')],
|
|
173
|
+
output: [
|
|
174
|
+
{
|
|
175
|
+
type: 'message',
|
|
176
|
+
id: 'msg-mixed',
|
|
177
|
+
status: 'completed',
|
|
178
|
+
role: 'assistant',
|
|
179
|
+
content: [
|
|
180
|
+
{ type: 'output_text', text: 'first ' },
|
|
181
|
+
{ type: 'reasoning_summary', text: 'should-be-ignored' },
|
|
182
|
+
{ type: 'text', text: 'second' },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
expect(getText(tree.nodes['user-r1-0']!.content)).toBe('plain string');
|
|
190
|
+
expect(getText(tree.nodes['msg-mixed']!.content)).toBe('first second');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('merges reasoning, tool calls, and message text into a single assistant node, preserving wire order', () => {
|
|
194
|
+
const tree = responsesToTree([
|
|
195
|
+
makeResponse({
|
|
196
|
+
id: 'r1',
|
|
197
|
+
request_input: [userInput('Hello')],
|
|
198
|
+
output: [
|
|
199
|
+
toolOutput({
|
|
200
|
+
id: 'tool-list',
|
|
201
|
+
type: 'mcp_list_tools',
|
|
202
|
+
server_label: 'firecrawl',
|
|
203
|
+
tools: [{ name: 'firecrawl_scrape' }],
|
|
204
|
+
}),
|
|
205
|
+
reasoningOutput('rs-1', 'thinking out loud'),
|
|
206
|
+
toolOutput({
|
|
207
|
+
id: 'tool-call-1',
|
|
208
|
+
type: 'mcp_call',
|
|
209
|
+
name: 'firecrawl_scrape',
|
|
210
|
+
}),
|
|
211
|
+
assistantOutput('msg-a1', 'Hi'),
|
|
212
|
+
],
|
|
213
|
+
}),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
// Reasoning and tool items don't get their own tree nodes; they are
|
|
217
|
+
// rolled up onto the assistant message.
|
|
218
|
+
expect(tree.nodes['tool-list']).toBeUndefined();
|
|
219
|
+
expect(tree.nodes['rs-1']).toBeUndefined();
|
|
220
|
+
expect(tree.nodes['tool-call-1']).toBeUndefined();
|
|
221
|
+
|
|
222
|
+
const assistant = tree.nodes['msg-a1']!;
|
|
223
|
+
expect(assistant).toBeDefined();
|
|
224
|
+
// mcp_list_tools is informational and skipped; reasoning + mcp_call +
|
|
225
|
+
// text remain in their original order.
|
|
226
|
+
expect(assistant.content).toHaveLength(3);
|
|
227
|
+
expect(assistant.content[0]).toMatchObject({
|
|
228
|
+
type: 'reasoning',
|
|
229
|
+
id: 'rs-1',
|
|
230
|
+
text: 'thinking out loud',
|
|
231
|
+
status: 'completed',
|
|
232
|
+
});
|
|
233
|
+
expect(assistant.content[1]).toMatchObject({
|
|
234
|
+
type: 'tool',
|
|
235
|
+
id: 'tool-call-1',
|
|
236
|
+
name: 'firecrawl_scrape',
|
|
237
|
+
});
|
|
238
|
+
expect(assistant.content[2]).toMatchObject({
|
|
239
|
+
type: 'text',
|
|
240
|
+
id: 'msg-a1',
|
|
241
|
+
text: 'Hi',
|
|
242
|
+
status: 'completed',
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('also picks up function_call / tool_call outputs as tool parts', () => {
|
|
247
|
+
const tree = responsesToTree([
|
|
248
|
+
makeResponse({
|
|
249
|
+
id: 'r1',
|
|
250
|
+
request_input: [userInput('Hello')],
|
|
251
|
+
output: [
|
|
252
|
+
toolOutput({ id: 'fn-1', type: 'function_call', name: 'lookup' }),
|
|
253
|
+
toolOutput({ id: 'tc-1', type: 'tool_call', name: 'shell' }),
|
|
254
|
+
assistantOutput('msg-a1', 'Done'),
|
|
255
|
+
],
|
|
256
|
+
}),
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
const assistant = tree.nodes['msg-a1']!;
|
|
260
|
+
expect(assistant.content).toHaveLength(3);
|
|
261
|
+
expect(assistant.content[0]).toMatchObject({
|
|
262
|
+
type: 'tool',
|
|
263
|
+
id: 'fn-1',
|
|
264
|
+
name: 'lookup',
|
|
265
|
+
});
|
|
266
|
+
expect(assistant.content[1]).toMatchObject({
|
|
267
|
+
type: 'tool',
|
|
268
|
+
id: 'tc-1',
|
|
269
|
+
name: 'shell',
|
|
270
|
+
});
|
|
271
|
+
expect(assistant.content[2].type).toBe('text');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('creates an assistant node from reasoning / tool outputs even when no message output is present', () => {
|
|
275
|
+
const tree = responsesToTree([
|
|
276
|
+
makeResponse({
|
|
277
|
+
id: 'r1',
|
|
278
|
+
request_input: [userInput('Hello')],
|
|
279
|
+
output: [
|
|
280
|
+
reasoningOutput('rs-1', 'planning'),
|
|
281
|
+
toolOutput({ id: 'tool-1', type: 'mcp_call', name: 'search' }),
|
|
282
|
+
],
|
|
283
|
+
}),
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
const assistant = tree.nodes['assistant-r1']!;
|
|
287
|
+
expect(assistant).toBeDefined();
|
|
288
|
+
expect(assistant.role).toBe('assistant');
|
|
289
|
+
expect(assistant.content).toHaveLength(2);
|
|
290
|
+
expect(assistant.content[0]).toMatchObject({
|
|
291
|
+
type: 'reasoning',
|
|
292
|
+
text: 'planning',
|
|
293
|
+
});
|
|
294
|
+
expect(assistant.content[1].type).toBe('tool');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('omits reasoning text when no reasoning_text parts are present', () => {
|
|
298
|
+
const tree = responsesToTree([
|
|
299
|
+
makeResponse({
|
|
300
|
+
id: 'r1',
|
|
301
|
+
request_input: [userInput('Hello')],
|
|
302
|
+
output: [
|
|
303
|
+
{
|
|
304
|
+
type: 'reasoning',
|
|
305
|
+
id: 'rs-empty',
|
|
306
|
+
status: 'completed',
|
|
307
|
+
role: null,
|
|
308
|
+
content: [],
|
|
309
|
+
},
|
|
310
|
+
assistantOutput('msg-a1', 'Hi'),
|
|
311
|
+
],
|
|
312
|
+
}),
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const reasoning = tree.nodes['msg-a1']!.content[0];
|
|
316
|
+
expect(reasoning.type).toBe('reasoning');
|
|
317
|
+
if (reasoning.type === 'reasoning') {
|
|
318
|
+
expect(reasoning.text).toBeUndefined();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('chains the next response under a reasoning-only assistant turn', () => {
|
|
323
|
+
const tree = responsesToTree([
|
|
324
|
+
makeResponse({
|
|
325
|
+
id: 'r1',
|
|
326
|
+
request_input: [userInput('Hello')],
|
|
327
|
+
output: [reasoningOutput('rs-1', 'planning')],
|
|
328
|
+
}),
|
|
329
|
+
makeResponse({
|
|
330
|
+
id: 'r2',
|
|
331
|
+
previous_response_id: 'r1',
|
|
332
|
+
request_input: [userInput('Follow-up')],
|
|
333
|
+
output: [assistantOutput('msg-a2', 'Reply')],
|
|
334
|
+
}),
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
expect(tree.nodes['rs-1']).toBeUndefined();
|
|
338
|
+
expect(tree.nodes['assistant-r1']).toBeDefined();
|
|
339
|
+
expect(tree.nodes['user-r2-0']!.parentId).toBe('assistant-r1');
|
|
340
|
+
expect(tree.nodes['msg-a2']!.parentId).toBe('user-r2-0');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('responsesToTree + useMessageHandler.setTree', () => {
|
|
345
|
+
it('hydrates a handler from a Responses-API list via the documented adapter pattern', () => {
|
|
346
|
+
const handler = useMessageHandler();
|
|
347
|
+
handler.setTree(
|
|
348
|
+
responsesToTree([
|
|
349
|
+
makeResponse({
|
|
350
|
+
id: 'r1',
|
|
351
|
+
request_input: [userInput('Hello')],
|
|
352
|
+
output: [assistantOutput('msg-a1', 'Hi')],
|
|
353
|
+
}),
|
|
354
|
+
]),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
expect(handler.messages.value).toHaveLength(2);
|
|
358
|
+
expect(handler.messages.value.map((m) => m.role)).toEqual([
|
|
359
|
+
'user',
|
|
360
|
+
'assistant',
|
|
361
|
+
]);
|
|
362
|
+
expect(getText(handler.messages.value[0].content)).toBe('Hello');
|
|
363
|
+
expect(getText(handler.messages.value[1].content)).toBe('Hi');
|
|
364
|
+
});
|
|
365
|
+
});
|