@aleph-alpha/chat-kit 1.3.1 → 1.5.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-CyGLItQZ.js → CkInput.vue_vue_type_script_setup_true_lang-C5C09u0z.js} +9 -8
- 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/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/fileUpload/index.d.ts +7 -0
- package/dist/composables/fileUpload/index.d.ts.map +1 -0
- package/dist/composables/fileUpload/types.d.ts +295 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -0
- package/dist/composables/fileUpload/useFileUpload.d.ts +4 -0
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -0
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts +3 -0
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -0
- package/dist/composables/index.d.ts +2 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/index.js +5 -2
- package/dist/composables/useConversations.d.ts +12 -0
- package/dist/composables/useConversations.d.ts.map +1 -1
- 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-BbTgIoYa.js +1557 -0
- 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/{useMessageHandler-BmBdu2EW.js → useMessageHandler-ZRC-gIOJ.js} +5 -5
- package/package.json +10 -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/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/fileUpload/index.spec.ts +213 -0
- package/src/composables/fileUpload/index.ts +83 -0
- package/src/composables/fileUpload/types.ts +409 -0
- package/src/composables/fileUpload/useFileUpload.ts +1242 -0
- package/src/composables/fileUpload/useFileUploadWrapper.ts +382 -0
- package/src/composables/index.ts +2 -1
- 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/dist/useConversationSessions-ulFhtpQr.js +0 -243
- package/src/services/ChatService.ts +0 -60
- package/src/services/MockChatService.ts +0 -47
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { ref, computed, shallowRef, watch, toValue, type MaybeRef } from 'vue';
|
|
2
|
+
import type {
|
|
3
|
+
HttpClient,
|
|
4
|
+
RequestConfig,
|
|
5
|
+
SseStream,
|
|
6
|
+
SseEvent,
|
|
7
|
+
FileEntry,
|
|
8
|
+
ScopedFileView,
|
|
9
|
+
DocumentUploadConfig,
|
|
10
|
+
DocumentUploadProvider,
|
|
11
|
+
DocumentUploadReturn,
|
|
12
|
+
UploadEvent,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { useFileUpload } from './useFileUpload';
|
|
15
|
+
|
|
16
|
+
function buildHttpClient(
|
|
17
|
+
baseUrl: string,
|
|
18
|
+
defaultHeaders: Record<string, string>,
|
|
19
|
+
): HttpClient {
|
|
20
|
+
return {
|
|
21
|
+
async get<T>(url: string, config?: RequestConfig): Promise<T> {
|
|
22
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
23
|
+
const res = await fetch(fullUrl, {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
signal: config?.signal,
|
|
26
|
+
headers: defaultHeaders,
|
|
27
|
+
credentials: 'include',
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) throw new Error(`GET ${fullUrl} failed: ${res.status}`);
|
|
30
|
+
return res.json();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async post<T>(
|
|
34
|
+
url: string,
|
|
35
|
+
body: unknown,
|
|
36
|
+
config?: RequestConfig,
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
39
|
+
const headers: Record<string, string> = { ...defaultHeaders };
|
|
40
|
+
let resolvedBody: BodyInit | undefined;
|
|
41
|
+
|
|
42
|
+
if (body instanceof FormData) {
|
|
43
|
+
resolvedBody = body;
|
|
44
|
+
} else {
|
|
45
|
+
headers['Content-Type'] = 'application/json';
|
|
46
|
+
resolvedBody = JSON.stringify(body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const res = await fetch(fullUrl, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
signal: config?.signal,
|
|
52
|
+
headers,
|
|
53
|
+
body: resolvedBody,
|
|
54
|
+
credentials: 'include',
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) throw new Error(`POST ${fullUrl} failed: ${res.status}`);
|
|
57
|
+
return res.json();
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async delete<T>(url: string, config?: RequestConfig): Promise<T> {
|
|
61
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
62
|
+
const res = await fetch(fullUrl, {
|
|
63
|
+
method: 'DELETE',
|
|
64
|
+
signal: config?.signal,
|
|
65
|
+
headers: defaultHeaders,
|
|
66
|
+
credentials: 'include',
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(`DELETE ${fullUrl} failed: ${res.status}`);
|
|
69
|
+
return res.json();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
stream(url: string, config?: RequestConfig): SseStream {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const signal = config?.signal
|
|
75
|
+
? AbortSignal.any([controller.signal, config.signal])
|
|
76
|
+
: controller.signal;
|
|
77
|
+
|
|
78
|
+
const listeners: Array<(event: SseEvent) => void> = [];
|
|
79
|
+
let closed = false;
|
|
80
|
+
|
|
81
|
+
const fullUrl = `${baseUrl}/${url}`;
|
|
82
|
+
const headers: Record<string, string> = {
|
|
83
|
+
Accept: 'text/event-stream',
|
|
84
|
+
...defaultHeaders,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function parseSseLines(
|
|
88
|
+
lines: string[],
|
|
89
|
+
notify: (event: SseEvent) => void,
|
|
90
|
+
): void {
|
|
91
|
+
let currentEvent = '';
|
|
92
|
+
let currentData = '';
|
|
93
|
+
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.startsWith('event:')) {
|
|
96
|
+
currentEvent = line.slice(6).trim();
|
|
97
|
+
} else if (line.startsWith('data:')) {
|
|
98
|
+
currentData = line.slice(5).trim();
|
|
99
|
+
} else if (line === '') {
|
|
100
|
+
if (currentData) {
|
|
101
|
+
notify({
|
|
102
|
+
event: currentEvent || 'message',
|
|
103
|
+
data: currentData,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
currentEvent = '';
|
|
107
|
+
currentData = '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(fullUrl, {
|
|
115
|
+
signal,
|
|
116
|
+
headers,
|
|
117
|
+
credentials: 'include',
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok || !response.body) return;
|
|
120
|
+
|
|
121
|
+
const reader = response.body.getReader();
|
|
122
|
+
const decoder = new TextDecoder();
|
|
123
|
+
let buffer = '';
|
|
124
|
+
|
|
125
|
+
while (!closed) {
|
|
126
|
+
const { done, value } = await reader.read();
|
|
127
|
+
if (done) break;
|
|
128
|
+
|
|
129
|
+
buffer += decoder.decode(value, { stream: true });
|
|
130
|
+
const lines = buffer.split('\n');
|
|
131
|
+
buffer = lines.pop() ?? '';
|
|
132
|
+
|
|
133
|
+
parseSseLines(lines, (event) => {
|
|
134
|
+
for (const cb of listeners) cb(event);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// stream ended or aborted
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
subscribe(callback: (event: SseEvent) => void) {
|
|
144
|
+
listeners.push(callback);
|
|
145
|
+
},
|
|
146
|
+
close() {
|
|
147
|
+
closed = true;
|
|
148
|
+
controller.abort();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createDocumentUploadProvider(
|
|
156
|
+
config: DocumentUploadConfig,
|
|
157
|
+
): DocumentUploadProvider {
|
|
158
|
+
const {
|
|
159
|
+
baseUrl,
|
|
160
|
+
userId,
|
|
161
|
+
workflowId,
|
|
162
|
+
searchStoreConfig,
|
|
163
|
+
workflowConfig,
|
|
164
|
+
persist = true,
|
|
165
|
+
maxConcurrency = 3,
|
|
166
|
+
} = config;
|
|
167
|
+
|
|
168
|
+
const defaultHeaders: Record<string, string> = {
|
|
169
|
+
'x-user-id': userId,
|
|
170
|
+
...(config.headers ?? {}),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const httpClient = buildHttpClient(baseUrl, defaultHeaders);
|
|
174
|
+
|
|
175
|
+
// ---- Search store resolution (one per user) ----
|
|
176
|
+
|
|
177
|
+
let resolvedStoreId: string | null = null;
|
|
178
|
+
let resolvePromise: Promise<string> | null = null;
|
|
179
|
+
|
|
180
|
+
async function createSearchStoreWithWorkflow(): Promise<string> {
|
|
181
|
+
const name = `${userId}-store-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
182
|
+
const storeRes = await httpClient.post<{ id: string }>('search-stores', {
|
|
183
|
+
name,
|
|
184
|
+
...searchStoreConfig,
|
|
185
|
+
metadata: { created_by: userId },
|
|
186
|
+
});
|
|
187
|
+
const newStoreId = storeRes.id;
|
|
188
|
+
|
|
189
|
+
await httpClient.post(
|
|
190
|
+
`search-stores/${newStoreId}/workflows`,
|
|
191
|
+
workflowConfig,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return newStoreId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function resolveSearchStore(): Promise<string> {
|
|
198
|
+
if (resolvedStoreId) return resolvedStoreId;
|
|
199
|
+
if (resolvePromise) return resolvePromise;
|
|
200
|
+
|
|
201
|
+
resolvePromise = (async () => {
|
|
202
|
+
try {
|
|
203
|
+
const stores = await httpClient.get<{ id: string }[]>('search-stores');
|
|
204
|
+
|
|
205
|
+
if (stores.length > 0 && stores[0]) {
|
|
206
|
+
resolvedStoreId = stores[0].id;
|
|
207
|
+
return resolvedStoreId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
resolvedStoreId = await createSearchStoreWithWorkflow();
|
|
211
|
+
return resolvedStoreId;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
resolvePromise = null;
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
return resolvePromise;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- Upload SDK singleton ----
|
|
222
|
+
|
|
223
|
+
let instance: ReturnType<typeof useFileUpload> | null = null;
|
|
224
|
+
|
|
225
|
+
function purgeInterruptedFiles() {
|
|
226
|
+
if (!instance) return;
|
|
227
|
+
const allFiles = [...instance.files.value.values()];
|
|
228
|
+
for (const entry of allFiles) {
|
|
229
|
+
if (entry.status === 'failed' && !entry.metadata.messageId) {
|
|
230
|
+
instance.removeFile(entry.id, { removeFromRemote: false });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getUploadInstance(): ReturnType<typeof useFileUpload> {
|
|
236
|
+
if (!instance) {
|
|
237
|
+
const onEvent = (event: UploadEvent) => {
|
|
238
|
+
config.onEvent?.(event);
|
|
239
|
+
if (event.type === 'state:restored') {
|
|
240
|
+
purgeInterruptedFiles();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
instance = useFileUpload({
|
|
245
|
+
client: httpClient,
|
|
246
|
+
workflowId,
|
|
247
|
+
persist,
|
|
248
|
+
maxConcurrency,
|
|
249
|
+
onEvent,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return instance;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- Public composable ----
|
|
256
|
+
|
|
257
|
+
function useDocumentUpload(
|
|
258
|
+
conversationIdRef: MaybeRef<string>,
|
|
259
|
+
): DocumentUploadReturn {
|
|
260
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
261
|
+
const upload = getUploadInstance();
|
|
262
|
+
const storeId = ref<string>(resolvedStoreId ?? '');
|
|
263
|
+
|
|
264
|
+
const currentScope = shallowRef<ScopedFileView | null>(
|
|
265
|
+
upload.scopeBy('conversationId', toValue(conversationIdRef)),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
watch(
|
|
269
|
+
() => toValue(conversationIdRef),
|
|
270
|
+
(newId: string, oldId: string) => {
|
|
271
|
+
if (oldId && oldId !== newId) {
|
|
272
|
+
const oldScope = upload.scopeBy('conversationId', oldId);
|
|
273
|
+
for (const entry of oldScope.files.value) {
|
|
274
|
+
upload.updateFileMetadata(entry.id, { conversationId: newId });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
currentScope.value = upload.scopeBy('conversationId', newId);
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
function scopedFiles(): FileEntry[] {
|
|
282
|
+
return currentScope.value?.files.value ?? [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function uploadFiles(files: File[]) {
|
|
286
|
+
if (!files.length) return;
|
|
287
|
+
|
|
288
|
+
const userStoreId = await resolveSearchStore();
|
|
289
|
+
upload.setStoreId(userStoreId);
|
|
290
|
+
storeId.value = userStoreId;
|
|
291
|
+
|
|
292
|
+
const conversationId = toValue(conversationIdRef);
|
|
293
|
+
const result = await upload.upload({
|
|
294
|
+
id: `${conversationId}::${Date.now()}`,
|
|
295
|
+
files,
|
|
296
|
+
forwardedProps: { conversationId },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
for (const r of result.results) {
|
|
300
|
+
if (r.success && r.remoteFileId) {
|
|
301
|
+
upload.updateFileMetadata(r.fileId, {
|
|
302
|
+
data_platform_id: r.remoteFileId,
|
|
303
|
+
searchStoreId: storeId.value,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const successFileIds = result.results
|
|
309
|
+
.filter((r) => r.success && r.remoteFileId)
|
|
310
|
+
.map((r) => r.remoteFileId!);
|
|
311
|
+
|
|
312
|
+
if (successFileIds.length > 0) {
|
|
313
|
+
await upload.runWorkflow(successFileIds);
|
|
314
|
+
purgeCompletedFiles();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const unassignedFiles = computed(() =>
|
|
321
|
+
scopedFiles().filter((entry) => !entry.metadata.messageId),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
function assignFilesToMessage(messageId: string) {
|
|
325
|
+
for (const entry of scopedFiles()) {
|
|
326
|
+
if (!entry.metadata.messageId && entry.status === 'success') {
|
|
327
|
+
upload.updateFileMetadata(entry.id, { messageId });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getFilesForMessage(messageId: string): FileEntry[] {
|
|
333
|
+
return scopedFiles().filter(
|
|
334
|
+
(entry) =>
|
|
335
|
+
entry.metadata.messageId === messageId ||
|
|
336
|
+
(entry.metadata.responseId &&
|
|
337
|
+
messageId === `user-${entry.metadata.responseId}-0`),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function stampResponseId(responseId: string, localMessageId: string) {
|
|
342
|
+
for (const entry of scopedFiles()) {
|
|
343
|
+
if (entry.metadata.messageId === localMessageId) {
|
|
344
|
+
upload.updateFileMetadata(entry.id, { responseId });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function purgeCompletedFiles() {
|
|
350
|
+
const toPurge = scopedFiles().filter(
|
|
351
|
+
(entry) =>
|
|
352
|
+
entry.metadata.messageId &&
|
|
353
|
+
(entry.status === 'success' || entry.status === 'failed'),
|
|
354
|
+
);
|
|
355
|
+
for (const entry of toPurge) {
|
|
356
|
+
upload.removeFile(entry.id, { removeFromRemote: false });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleFilesSelected(event: Event) {
|
|
361
|
+
const input = event.target as HTMLInputElement;
|
|
362
|
+
const fileList = Array.from(input.files ?? []);
|
|
363
|
+
uploadFiles(fileList);
|
|
364
|
+
input.value = '';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
upload,
|
|
369
|
+
storeId,
|
|
370
|
+
fileInput,
|
|
371
|
+
uploadFiles,
|
|
372
|
+
unassignedFiles,
|
|
373
|
+
assignFilesToMessage,
|
|
374
|
+
getFilesForMessage,
|
|
375
|
+
stampResponseId,
|
|
376
|
+
purgeCompletedFiles,
|
|
377
|
+
handleFilesSelected,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { useDocumentUpload };
|
|
382
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResponseList, StoredResponse } from '../adapters/responses-api';
|
|
1
2
|
import type {
|
|
2
3
|
HttpClient,
|
|
3
4
|
RawConversation,
|
|
@@ -554,4 +555,121 @@ describe('useConversations', () => {
|
|
|
554
555
|
expect(conv.getById('missing')).toBeUndefined();
|
|
555
556
|
});
|
|
556
557
|
});
|
|
558
|
+
|
|
559
|
+
describe('fetchResponses', () => {
|
|
560
|
+
function makeStoredResponse(
|
|
561
|
+
overrides: Partial<StoredResponse> & { id: string },
|
|
562
|
+
): StoredResponse {
|
|
563
|
+
return {
|
|
564
|
+
object: 'response',
|
|
565
|
+
created_at: 1_777_456_700,
|
|
566
|
+
completed_at: 1_777_456_701,
|
|
567
|
+
status: 'completed',
|
|
568
|
+
model: 'pharia-1',
|
|
569
|
+
output: [],
|
|
570
|
+
request_input: [],
|
|
571
|
+
...overrides,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function makeResponseList(data: StoredResponse[]): ResponseList {
|
|
576
|
+
return { object: 'list', data };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
it('GETs /:id/responses with order=asc by default and returns the unwrapped list', async () => {
|
|
580
|
+
const http = makeFakeHttp();
|
|
581
|
+
http.get.mockResolvedValue(
|
|
582
|
+
makeResponseList([
|
|
583
|
+
makeStoredResponse({ id: 'resp_1' }),
|
|
584
|
+
makeStoredResponse({ id: 'resp_2' }),
|
|
585
|
+
]),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const conv = useConversations({
|
|
589
|
+
httpClient: asHttp(http),
|
|
590
|
+
path: '/conversations',
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const result = await conv.fetchResponses('conv_22');
|
|
594
|
+
|
|
595
|
+
expect(http.get).toHaveBeenCalledWith(
|
|
596
|
+
'/conversations/conv_22/responses',
|
|
597
|
+
{
|
|
598
|
+
params: { order: 'asc' },
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
expect(result.map((r) => r.id)).toEqual(['resp_1', 'resp_2']);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('passes through the caller-supplied order', async () => {
|
|
605
|
+
const http = makeFakeHttp();
|
|
606
|
+
http.get.mockResolvedValue(makeResponseList([]));
|
|
607
|
+
|
|
608
|
+
const conv = useConversations({
|
|
609
|
+
httpClient: asHttp(http),
|
|
610
|
+
path: '/conversations',
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await conv.fetchResponses('conv_22', { order: 'desc' });
|
|
614
|
+
|
|
615
|
+
expect(http.get).toHaveBeenCalledWith(
|
|
616
|
+
'/conversations/conv_22/responses',
|
|
617
|
+
{
|
|
618
|
+
params: { order: 'desc' },
|
|
619
|
+
},
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('does not modify the conversations list', async () => {
|
|
624
|
+
const http = makeFakeHttp();
|
|
625
|
+
http.get
|
|
626
|
+
.mockResolvedValueOnce(makeRawList([makeRawConversation({ id: 'a' })]))
|
|
627
|
+
.mockResolvedValueOnce(
|
|
628
|
+
makeResponseList([makeStoredResponse({ id: 'resp_1' })]),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const conv = useConversations({
|
|
632
|
+
httpClient: asHttp(http),
|
|
633
|
+
path: '/conversations',
|
|
634
|
+
});
|
|
635
|
+
await conv.fetch();
|
|
636
|
+
await conv.fetchResponses('a');
|
|
637
|
+
|
|
638
|
+
expect(conv.conversations.value.map((c) => c.id)).toEqual(['a']);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('toggles isLoading around the call', async () => {
|
|
642
|
+
const http = makeFakeHttp();
|
|
643
|
+
let resolve!: (value: ResponseList) => void;
|
|
644
|
+
http.get.mockReturnValue(new Promise((r) => (resolve = r)));
|
|
645
|
+
|
|
646
|
+
const conv = useConversations({
|
|
647
|
+
httpClient: asHttp(http),
|
|
648
|
+
path: '/conversations',
|
|
649
|
+
});
|
|
650
|
+
const promise = conv.fetchResponses('a');
|
|
651
|
+
|
|
652
|
+
await nextTick();
|
|
653
|
+
expect(conv.isLoading.value).toBe(true);
|
|
654
|
+
|
|
655
|
+
resolve(makeResponseList([]));
|
|
656
|
+
await promise;
|
|
657
|
+
|
|
658
|
+
expect(conv.isLoading.value).toBe(false);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('records and re-throws errors', async () => {
|
|
662
|
+
const http = makeFakeHttp();
|
|
663
|
+
http.get.mockRejectedValue(new Error('boom'));
|
|
664
|
+
|
|
665
|
+
const conv = useConversations({
|
|
666
|
+
httpClient: asHttp(http),
|
|
667
|
+
path: '/conversations',
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await expect(conv.fetchResponses('a')).rejects.toThrow('boom');
|
|
671
|
+
expect(conv.error.value?.message).toBe('boom');
|
|
672
|
+
expect(conv.isLoading.value).toBe(false);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
557
675
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResponseList, StoredResponse } from '../adapters/responses-api';
|
|
1
2
|
import { generateId, secondsToDate, toError } from '../helpers';
|
|
2
3
|
import type {
|
|
3
4
|
ConversationSummary,
|
|
@@ -85,6 +86,18 @@ export interface UseConversationsReturn {
|
|
|
85
86
|
remove(id: string, options?: MutationOptions): Promise<void>;
|
|
86
87
|
/** Local lookup. Does not fetch — only checks the in-memory list. */
|
|
87
88
|
getById(id: string): ConversationSummary | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* List the OpenAI Responses-API records attached to a conversation. Pure
|
|
91
|
+
* fetch: it does not touch the conversations list. The endpoint is not
|
|
92
|
+
* paginated, so the returned array is the full timeline ordered by
|
|
93
|
+
* `created_at` (ascending by default). Pair with `responsesToTree` from
|
|
94
|
+
* `@aleph-alpha/chat-kit/adapters/responses-api` to hydrate a chat
|
|
95
|
+
* handler: `setTree(responsesToTree(await fetchResponses(id)))`.
|
|
96
|
+
*/
|
|
97
|
+
fetchResponses(
|
|
98
|
+
id: string,
|
|
99
|
+
options?: { order?: 'asc' | 'desc' },
|
|
100
|
+
): Promise<StoredResponse[]>;
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
const DEFAULT_PAGE_SIZE = 20;
|
|
@@ -277,6 +290,24 @@ export function useConversations(
|
|
|
277
290
|
return conversations.value.find((c) => c.id === id);
|
|
278
291
|
}
|
|
279
292
|
|
|
293
|
+
async function fetchResponses(
|
|
294
|
+
id: string,
|
|
295
|
+
opts?: { order?: 'asc' | 'desc' },
|
|
296
|
+
): Promise<StoredResponse[]> {
|
|
297
|
+
beginRequest();
|
|
298
|
+
try {
|
|
299
|
+
const raw = await http.get<ResponseList>(`${path}/${id}/responses`, {
|
|
300
|
+
params: { order: opts?.order ?? 'asc' },
|
|
301
|
+
});
|
|
302
|
+
return raw.data;
|
|
303
|
+
} catch (e) {
|
|
304
|
+
error.value = toError(e);
|
|
305
|
+
throw error.value;
|
|
306
|
+
} finally {
|
|
307
|
+
endRequest();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
return {
|
|
281
312
|
conversations: readonly(conversations) as Readonly<
|
|
282
313
|
Ref<readonly ConversationSummary[]>
|
|
@@ -291,6 +322,7 @@ export function useConversations(
|
|
|
291
322
|
rename,
|
|
292
323
|
remove,
|
|
293
324
|
getById,
|
|
325
|
+
fetchResponses,
|
|
294
326
|
};
|
|
295
327
|
}
|
|
296
328
|
|
|
@@ -261,7 +261,14 @@ describe('CkConversation uses injected labels', () => {
|
|
|
261
261
|
{
|
|
262
262
|
id: '1',
|
|
263
263
|
role: 'assistant',
|
|
264
|
-
content:
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
id: 'txt-1',
|
|
268
|
+
text: 'Hi',
|
|
269
|
+
status: 'completed',
|
|
270
|
+
},
|
|
271
|
+
],
|
|
265
272
|
createdAt: new Date(),
|
|
266
273
|
status: 'completed',
|
|
267
274
|
},
|
|
@@ -290,7 +297,14 @@ describe('CkConversation uses injected labels', () => {
|
|
|
290
297
|
{
|
|
291
298
|
id: '1',
|
|
292
299
|
role: 'assistant',
|
|
293
|
-
content:
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: 'text',
|
|
303
|
+
id: 'txt-1',
|
|
304
|
+
text: 'Hi',
|
|
305
|
+
status: 'completed',
|
|
306
|
+
},
|
|
307
|
+
],
|
|
294
308
|
createdAt: new Date(),
|
|
295
309
|
status: 'completed',
|
|
296
310
|
},
|