@kognitivedev/adapter-chat-kognitive 0.2.29
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-test.log +11 -0
- package/CHANGELOG.md +11 -0
- package/README.md +8 -0
- package/dist/__tests__/kognitive-backend.test.d.ts +1 -0
- package/dist/__tests__/kognitive-backend.test.js +283 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +325 -0
- package/package.json +38 -0
- package/src/__tests__/kognitive-backend.test.ts +331 -0
- package/src/index.ts +386 -0
- package/tsconfig.json +14 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildCanonicalConversationMessages,
|
|
3
|
+
isModerationErrorPayload,
|
|
4
|
+
ModerationError,
|
|
5
|
+
readKognitiveEventStream,
|
|
6
|
+
type FilePart,
|
|
7
|
+
type KognitiveContentPart,
|
|
8
|
+
type KognitiveMessage,
|
|
9
|
+
type KognitiveUIMessage,
|
|
10
|
+
} from "@kognitivedev/shared";
|
|
11
|
+
import type {
|
|
12
|
+
ChatBackendAdapter,
|
|
13
|
+
ChatBackendContext,
|
|
14
|
+
ChatThreadClient,
|
|
15
|
+
ThreadCreateInput,
|
|
16
|
+
ThreadDetail,
|
|
17
|
+
ThreadMetadata,
|
|
18
|
+
ThreadSummary,
|
|
19
|
+
} from "@kognitivedev/ui";
|
|
20
|
+
|
|
21
|
+
export interface KognitiveChatBackendOptions {
|
|
22
|
+
serverUrl?: string;
|
|
23
|
+
streamPath?: string | ((context: ChatBackendContext) => string);
|
|
24
|
+
threadBasePath?: string | ((context: ChatBackendContext) => string);
|
|
25
|
+
headers?: Record<string, string> | ((context: ChatBackendContext) => Record<string, string> | undefined);
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ABSOLUTE_URL_RE = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
30
|
+
|
|
31
|
+
function isAbsoluteUrl(value: string): boolean {
|
|
32
|
+
return ABSOLUTE_URL_RE.test(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveServerUrl(serverUrl?: string): string {
|
|
36
|
+
const resolved = serverUrl ?? "";
|
|
37
|
+
if (resolved === "/") return "";
|
|
38
|
+
return resolved.replace(/\/$/, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function joinServerPath(serverUrl: string, path: string): string {
|
|
42
|
+
const normalizedServerUrl = resolveServerUrl(serverUrl);
|
|
43
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
44
|
+
return `${normalizedServerUrl}${normalizedPath}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appendQueryParams(urlString: string, params: Record<string, string | undefined>): string {
|
|
48
|
+
if (isAbsoluteUrl(urlString)) {
|
|
49
|
+
const url = new URL(urlString);
|
|
50
|
+
for (const [key, value] of Object.entries(params)) {
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
url.searchParams.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return url.toString();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [pathAndQuery, hash = ""] = urlString.split("#", 2);
|
|
59
|
+
const [pathname, query = ""] = pathAndQuery.split("?", 2);
|
|
60
|
+
const searchParams = new URLSearchParams(query);
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(params)) {
|
|
63
|
+
if (value !== undefined) {
|
|
64
|
+
searchParams.set(key, value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nextQuery = searchParams.toString();
|
|
69
|
+
return `${pathname}${nextQuery ? `?${nextQuery}` : ""}${hash ? `#${hash}` : ""}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveTemplate(template: string | ((context: ChatBackendContext) => string) | undefined, context: ChatBackendContext, fallback: string): string {
|
|
73
|
+
const resolved = typeof template === "function" ? template(context) : template ?? fallback;
|
|
74
|
+
return resolved.replace(/:agentName/g, encodeURIComponent(context.agentName));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveEndpoint(serverUrl: string, path: string): string {
|
|
78
|
+
if (isAbsoluteUrl(path)) {
|
|
79
|
+
return path.replace(/\/$/, "");
|
|
80
|
+
}
|
|
81
|
+
return joinServerPath(serverUrl, path);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveAdapterHeaders(context: ChatBackendContext, source?: KognitiveChatBackendOptions["headers"]): Record<string, string> {
|
|
85
|
+
return {
|
|
86
|
+
...(typeof source === "function" ? source(context) ?? {} : source ?? {}),
|
|
87
|
+
...(context.headers ?? {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
92
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
93
|
+
? value as Record<string, unknown>
|
|
94
|
+
: undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractInlineFileData(url: string): string | undefined {
|
|
98
|
+
if (!url.startsWith("data:")) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const commaIndex = url.indexOf(",");
|
|
103
|
+
if (commaIndex === -1) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const metadata = url.slice(0, commaIndex);
|
|
108
|
+
const payload = url.slice(commaIndex + 1);
|
|
109
|
+
|
|
110
|
+
if (metadata.includes(";base64")) {
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
return decodeURIComponent(payload);
|
|
116
|
+
} catch {
|
|
117
|
+
return payload;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function serializeRuntimePart(part: KognitiveContentPart): KognitiveContentPart {
|
|
122
|
+
if (part.type !== "file") {
|
|
123
|
+
return part;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const filePart = part as FilePart;
|
|
127
|
+
if (filePart.data !== undefined) {
|
|
128
|
+
return filePart;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const inlineData = typeof filePart.url === "string" ? extractInlineFileData(filePart.url) : undefined;
|
|
132
|
+
if (inlineData === undefined) {
|
|
133
|
+
return filePart;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
type: "file",
|
|
138
|
+
data: inlineData,
|
|
139
|
+
filename: filePart.filename ?? filePart.name,
|
|
140
|
+
mediaType: filePart.mediaType ?? filePart.mimeType ?? filePart.mime_type,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function serializeRuntimeMetadata(metadata: KognitiveUIMessage["metadata"]) {
|
|
145
|
+
if (!metadata || typeof metadata !== "object") {
|
|
146
|
+
return metadata;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { feedback: _ignoredFeedback, ...rest } = metadata;
|
|
150
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function uiMessageToRuntimeMessage(message: KognitiveUIMessage): KognitiveMessage {
|
|
154
|
+
return {
|
|
155
|
+
role: message.role,
|
|
156
|
+
content: message.parts.map(serializeRuntimePart),
|
|
157
|
+
metadata: serializeRuntimeMetadata(message.metadata),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function buildKognitiveTransportError(response: Response): Promise<Error> {
|
|
162
|
+
const responseText = await response.clone().text();
|
|
163
|
+
if (responseText !== "") {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(responseText) as unknown;
|
|
166
|
+
if (isModerationErrorPayload(parsed)) {
|
|
167
|
+
return new ModerationError(parsed, response.status);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (parsed && typeof parsed === "object" && typeof (parsed as { error?: unknown }).error === "string") {
|
|
171
|
+
return new Error((parsed as { error: string }).error);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// fall back to raw text
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Error(`Kognitive transport failed: HTTP ${response.status}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createKognitiveChatBackend(options: KognitiveChatBackendOptions = {}): ChatBackendAdapter {
|
|
182
|
+
const serverUrl = resolveServerUrl(options.serverUrl);
|
|
183
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
capabilities: {
|
|
187
|
+
threads: true,
|
|
188
|
+
threadFork: true,
|
|
189
|
+
threadInterrupt: true,
|
|
190
|
+
threadDelete: true,
|
|
191
|
+
threadRename: true,
|
|
192
|
+
threadArchive: true,
|
|
193
|
+
messageFeedback: true,
|
|
194
|
+
},
|
|
195
|
+
createExecutionClient(context) {
|
|
196
|
+
const api = resolveEndpoint(
|
|
197
|
+
serverUrl,
|
|
198
|
+
resolveTemplate(options.streamPath, context, "/api/kognitive/agents/:agentName/stream"),
|
|
199
|
+
);
|
|
200
|
+
const baseHeaders = {
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
...resolveAdapterHeaders(context, options.headers),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
async stream(request) {
|
|
207
|
+
const contextBody = asRecord(context.body);
|
|
208
|
+
const requestBody = asRecord(request.body);
|
|
209
|
+
const mergedMetadata = {
|
|
210
|
+
...(asRecord(contextBody?.metadata) ?? {}),
|
|
211
|
+
...(asRecord(requestBody?.metadata) ?? {}),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const response = await fetchImpl(api, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: baseHeaders,
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
...(contextBody ?? {}),
|
|
219
|
+
...(requestBody ?? {}),
|
|
220
|
+
...(Object.keys(mergedMetadata).length > 0 ? { metadata: mergedMetadata } : {}),
|
|
221
|
+
...(request.sessionId ? { sessionId: request.sessionId } : {}),
|
|
222
|
+
...(context.runScope ? { runScope: context.runScope } : {}),
|
|
223
|
+
...(context.resourceId ? { resourceId: context.resourceId } : {}),
|
|
224
|
+
messages: buildCanonicalConversationMessages(request.messages.map(uiMessageToRuntimeMessage)),
|
|
225
|
+
}),
|
|
226
|
+
signal: request.signal,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
throw await buildKognitiveTransportError(response);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await readKognitiveEventStream(response, async (eventName, data) => {
|
|
234
|
+
if (eventName === "messages" || eventName === "custom" || eventName === "debug") {
|
|
235
|
+
await request.onEvent(eventName, data);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
createThreadClient(context): ChatThreadClient {
|
|
242
|
+
const apiBase = resolveEndpoint(
|
|
243
|
+
serverUrl,
|
|
244
|
+
resolveTemplate(options.threadBasePath, context, "/api/kognitive/threads/agents/:agentName"),
|
|
245
|
+
).replace(/\/$/, "");
|
|
246
|
+
const headers = {
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
...resolveAdapterHeaders(context, options.headers),
|
|
249
|
+
};
|
|
250
|
+
const userId = context.resourceId?.userId ? String(context.resourceId.userId) : undefined;
|
|
251
|
+
const buildUrl = (path = "") => appendQueryParams(`${apiBase}${path}`, { userId });
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
async list(query) {
|
|
255
|
+
const url = appendQueryParams(buildUrl(), {
|
|
256
|
+
userId,
|
|
257
|
+
...(query?.limit !== undefined ? { limit: String(query.limit) } : {}),
|
|
258
|
+
...(query?.offset !== undefined ? { offset: String(query.offset) } : {}),
|
|
259
|
+
});
|
|
260
|
+
const res = await fetchImpl(url, { headers });
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to list threads" }))).error);
|
|
263
|
+
}
|
|
264
|
+
const data = await res.json();
|
|
265
|
+
return {
|
|
266
|
+
threads: (data.threads ?? []) as ThreadSummary[],
|
|
267
|
+
total: typeof data.total === "number" ? data.total : (data.threads ?? []).length,
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
async get(sessionId) {
|
|
271
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}`), { headers });
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to get thread" }))).error);
|
|
274
|
+
}
|
|
275
|
+
return (await res.json()) as ThreadDetail;
|
|
276
|
+
},
|
|
277
|
+
async create(input) {
|
|
278
|
+
const res = await fetchImpl(buildUrl(), {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers,
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
...(userId ? { userId } : {}),
|
|
283
|
+
...(input ?? {}),
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to create thread" }))).error);
|
|
288
|
+
}
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
return data.thread as ThreadSummary;
|
|
291
|
+
},
|
|
292
|
+
async fork(sessionId) {
|
|
293
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}/fork`), {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers,
|
|
296
|
+
body: JSON.stringify(userId ? { userId } : {}),
|
|
297
|
+
});
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to fork thread" }))).error);
|
|
300
|
+
}
|
|
301
|
+
const data = await res.json();
|
|
302
|
+
return data.thread as ThreadSummary;
|
|
303
|
+
},
|
|
304
|
+
async interrupt(sessionId) {
|
|
305
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}/interrupt`), {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers,
|
|
308
|
+
});
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to interrupt thread" }))).error);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
async delete(sessionId) {
|
|
314
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}`), {
|
|
315
|
+
method: "DELETE",
|
|
316
|
+
headers,
|
|
317
|
+
});
|
|
318
|
+
if (!res.ok && res.status !== 204) {
|
|
319
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to delete thread" }))).error);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
async rename(sessionId, title) {
|
|
323
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}`), {
|
|
324
|
+
method: "PATCH",
|
|
325
|
+
headers,
|
|
326
|
+
body: JSON.stringify({ title }),
|
|
327
|
+
});
|
|
328
|
+
if (!res.ok) {
|
|
329
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to rename thread" }))).error);
|
|
330
|
+
}
|
|
331
|
+
const data = await res.json();
|
|
332
|
+
return data.thread as ThreadSummary;
|
|
333
|
+
},
|
|
334
|
+
async updateMetadata(sessionId: string, metadataPatch: ThreadMetadata) {
|
|
335
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}`), {
|
|
336
|
+
method: "PATCH",
|
|
337
|
+
headers,
|
|
338
|
+
body: JSON.stringify({ metadataPatch }),
|
|
339
|
+
});
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to update thread metadata" }))).error);
|
|
342
|
+
}
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
return data.thread as ThreadSummary;
|
|
345
|
+
},
|
|
346
|
+
async archive(sessionId) {
|
|
347
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}`), {
|
|
348
|
+
method: "PATCH",
|
|
349
|
+
headers,
|
|
350
|
+
body: JSON.stringify({ archived: true }),
|
|
351
|
+
});
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to archive thread" }))).error);
|
|
354
|
+
}
|
|
355
|
+
const data = await res.json();
|
|
356
|
+
return data.thread as ThreadSummary;
|
|
357
|
+
},
|
|
358
|
+
async unarchive(sessionId) {
|
|
359
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}`), {
|
|
360
|
+
method: "PATCH",
|
|
361
|
+
headers,
|
|
362
|
+
body: JSON.stringify({ archived: false }),
|
|
363
|
+
});
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to unarchive thread" }))).error);
|
|
366
|
+
}
|
|
367
|
+
const data = await res.json();
|
|
368
|
+
return data.thread as ThreadSummary;
|
|
369
|
+
},
|
|
370
|
+
async setMessageFeedback(sessionId, messageId, value) {
|
|
371
|
+
const res = await fetchImpl(buildUrl(`/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/feedback`), {
|
|
372
|
+
method: "PUT",
|
|
373
|
+
headers,
|
|
374
|
+
body: JSON.stringify({ value }),
|
|
375
|
+
});
|
|
376
|
+
if (!res.ok) {
|
|
377
|
+
throw new Error((await res.json().catch(() => ({ error: "Failed to set message feedback" }))).error);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const createKognitiveUIChatBackend = createKognitiveChatBackend;
|
|
386
|
+
export type KognitiveUIChatBackendOptions = KognitiveChatBackendOptions;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|