@aleph-alpha/chat-kit 1.11.1 → 1.12.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/adapters/responses-api.d.ts +54 -5
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +43 -9
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useMessageHandler.d.ts +6 -2
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts +10 -1
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/{index-BLq6_Lt2.js → index-Ct5hD214.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/{useMessageHandler-ZRC-gIOJ.js → useMessageHandler-CMdy-lm0.js} +36 -9
- package/package.json +1 -1
- package/src/adapters/responses-api.spec.ts +70 -0
- package/src/adapters/responses-api.ts +123 -12
- package/src/handlers/useMessageHandler.spec.ts +214 -0
- package/src/handlers/useMessageHandler.ts +28 -3
- package/src/helpers/conversationTree.spec.ts +134 -0
- package/src/helpers/conversationTree.ts +32 -1
- package/src/index.ts +1 -0
- package/src/types.ts +15 -0
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
addNode,
|
|
9
9
|
createEmptyTree,
|
|
10
|
+
cutBranchAfter,
|
|
10
11
|
getLinearPath,
|
|
11
12
|
navigate,
|
|
12
13
|
updateNode,
|
|
@@ -193,6 +194,35 @@ describe('getLinearPath', () => {
|
|
|
193
194
|
expect(u1Msg.status).toBe('completed');
|
|
194
195
|
expect(u1Msg.createdAt).toEqual(new Date('2025-01-01'));
|
|
195
196
|
});
|
|
197
|
+
|
|
198
|
+
it('forwards node metadata onto the BaseMessage projection', () => {
|
|
199
|
+
const tree = createEmptyTree();
|
|
200
|
+
addNode(
|
|
201
|
+
tree,
|
|
202
|
+
makeTreeMessage({
|
|
203
|
+
id: 'u1',
|
|
204
|
+
content: [text('Hello')],
|
|
205
|
+
metadata: {
|
|
206
|
+
attachments: [
|
|
207
|
+
{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const path = getLinearPath(tree);
|
|
214
|
+
expect(path[0].metadata).toEqual({
|
|
215
|
+
attachments: [{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' }],
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('omits metadata when the node has none', () => {
|
|
220
|
+
const tree = createEmptyTree();
|
|
221
|
+
addNode(tree, makeTreeMessage({ id: 'u1', content: [text('Hello')] }));
|
|
222
|
+
|
|
223
|
+
const path = getLinearPath(tree);
|
|
224
|
+
expect('metadata' in path[0]).toBe(false);
|
|
225
|
+
});
|
|
196
226
|
});
|
|
197
227
|
|
|
198
228
|
describe('addNode', () => {
|
|
@@ -287,6 +317,38 @@ describe('updateNode', () => {
|
|
|
287
317
|
updateNode(tree, 'nonexistent', { content: [text('x')] });
|
|
288
318
|
expect(Object.keys(tree.nodes).length).toBe(nodeCount);
|
|
289
319
|
});
|
|
320
|
+
|
|
321
|
+
it('sets metadata when supplied', () => {
|
|
322
|
+
const tree = makeBranchingTree();
|
|
323
|
+
updateNode(tree, 'u1', {
|
|
324
|
+
metadata: {
|
|
325
|
+
attachments: [{ name: 'report.pdf', fileId: 'f1' }],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
expect(tree.nodes.u1.metadata).toEqual({
|
|
329
|
+
attachments: [{ name: 'report.pdf', fileId: 'f1' }],
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('clears metadata when explicitly set to undefined', () => {
|
|
334
|
+
const tree = makeBranchingTree();
|
|
335
|
+
tree.nodes.u1.metadata = {
|
|
336
|
+
attachments: [{ name: 'old.pdf' }],
|
|
337
|
+
};
|
|
338
|
+
updateNode(tree, 'u1', { metadata: undefined });
|
|
339
|
+
expect(tree.nodes.u1.metadata).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('leaves metadata untouched when the key is absent from updates', () => {
|
|
343
|
+
const tree = makeBranchingTree();
|
|
344
|
+
tree.nodes.u1.metadata = {
|
|
345
|
+
attachments: [{ name: 'keep.pdf' }],
|
|
346
|
+
};
|
|
347
|
+
updateNode(tree, 'u1', { content: [text('Updated')] });
|
|
348
|
+
expect(tree.nodes.u1.metadata).toEqual({
|
|
349
|
+
attachments: [{ name: 'keep.pdf' }],
|
|
350
|
+
});
|
|
351
|
+
});
|
|
290
352
|
});
|
|
291
353
|
|
|
292
354
|
describe('updateNodeId', () => {
|
|
@@ -385,6 +447,78 @@ describe('navigate', () => {
|
|
|
385
447
|
});
|
|
386
448
|
});
|
|
387
449
|
|
|
450
|
+
describe('cutBranchAfter', () => {
|
|
451
|
+
it('removes every descendant of the target node', () => {
|
|
452
|
+
const tree = makeBranchingTree();
|
|
453
|
+
|
|
454
|
+
cutBranchAfter(tree, 'u2');
|
|
455
|
+
|
|
456
|
+
expect(tree.nodes.a2).toBeUndefined();
|
|
457
|
+
expect(tree.nodes.a3).toBeUndefined();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('keeps the target node and clears its children', () => {
|
|
461
|
+
const tree = makeBranchingTree();
|
|
462
|
+
|
|
463
|
+
cutBranchAfter(tree, 'u2');
|
|
464
|
+
|
|
465
|
+
expect(tree.nodes.u2).toBeDefined();
|
|
466
|
+
expect(tree.nodes.u2.childrenIds).toEqual([]);
|
|
467
|
+
expect(tree.nodes.u2.activeChildIndex).toBe(0);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('makes the target node the leaf of the active path', () => {
|
|
471
|
+
const tree = makeBranchingTree();
|
|
472
|
+
|
|
473
|
+
cutBranchAfter(tree, 'u2');
|
|
474
|
+
|
|
475
|
+
const path = getLinearPath(tree);
|
|
476
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2']);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('removes the entire subtree, including grandchildren', () => {
|
|
480
|
+
const tree = makeBranchingTree();
|
|
481
|
+
|
|
482
|
+
cutBranchAfter(tree, 'u1');
|
|
483
|
+
|
|
484
|
+
expect(Object.keys(tree.nodes)).toEqual(['u1']);
|
|
485
|
+
expect(tree.nodes.u1.childrenIds).toEqual([]);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('only cuts descendants — sibling branches are untouched', () => {
|
|
489
|
+
const tree = makeBranchingTree();
|
|
490
|
+
|
|
491
|
+
cutBranchAfter(tree, 'a1');
|
|
492
|
+
|
|
493
|
+
expect(tree.nodes.u2).toBeUndefined();
|
|
494
|
+
expect(tree.nodes.a2).toBeUndefined();
|
|
495
|
+
expect(tree.nodes.a3).toBeUndefined();
|
|
496
|
+
|
|
497
|
+
expect(tree.nodes.a1b).toBeDefined();
|
|
498
|
+
expect(tree.nodes.u3).toBeDefined();
|
|
499
|
+
expect(tree.nodes.u1.childrenIds).toEqual(['a1', 'a1b']);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('is a no-op when the node has no children', () => {
|
|
503
|
+
const tree = makeBranchingTree();
|
|
504
|
+
const nodeCount = Object.keys(tree.nodes).length;
|
|
505
|
+
|
|
506
|
+
cutBranchAfter(tree, 'a2');
|
|
507
|
+
|
|
508
|
+
expect(Object.keys(tree.nodes).length).toBe(nodeCount);
|
|
509
|
+
expect(tree.nodes.a2.childrenIds).toEqual([]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('is a no-op for a nonexistent id', () => {
|
|
513
|
+
const tree = makeBranchingTree();
|
|
514
|
+
const before = Object.keys(tree.nodes).sort();
|
|
515
|
+
|
|
516
|
+
cutBranchAfter(tree, 'nonexistent');
|
|
517
|
+
|
|
518
|
+
expect(Object.keys(tree.nodes).sort()).toEqual(before);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
388
522
|
describe('integration: full branching scenario', () => {
|
|
389
523
|
it('simulates send → regenerate → navigate workflow', () => {
|
|
390
524
|
const tree = createEmptyTree();
|
|
@@ -25,6 +25,7 @@ export function getLinearPath(tree: ConversationTree): BaseMessage[] {
|
|
|
25
25
|
content: node.content,
|
|
26
26
|
createdAt: node.createdAt,
|
|
27
27
|
status: node.status,
|
|
28
|
+
...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
if (node.parentId !== null) {
|
|
@@ -61,7 +62,9 @@ export function addNode(tree: ConversationTree, node: TreeMessage): void {
|
|
|
61
62
|
export function updateNode(
|
|
62
63
|
tree: ConversationTree,
|
|
63
64
|
id: string,
|
|
64
|
-
updates: Partial<
|
|
65
|
+
updates: Partial<
|
|
66
|
+
Pick<TreeMessage, 'content' | 'status' | 'role' | 'metadata'>
|
|
67
|
+
>,
|
|
65
68
|
): void {
|
|
66
69
|
const node = tree.nodes[id];
|
|
67
70
|
if (!node) return;
|
|
@@ -69,6 +72,9 @@ export function updateNode(
|
|
|
69
72
|
if (updates.content !== undefined) node.content = updates.content;
|
|
70
73
|
if (updates.status !== undefined) node.status = updates.status;
|
|
71
74
|
if (updates.role !== undefined) node.role = updates.role;
|
|
75
|
+
// `metadata` is intentionally allowed through with an `undefined` value so
|
|
76
|
+
// callers can clear it; the call site checks for `'metadata' in updates`.
|
|
77
|
+
if ('metadata' in updates) node.metadata = updates.metadata;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
export function updateNodeId(
|
|
@@ -99,6 +105,31 @@ export function updateNodeId(
|
|
|
99
105
|
if (tree.rootId === oldId) tree.rootId = newId;
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Remove every descendant of the node with the given id, keeping the node
|
|
110
|
+
* itself intact. The node's `childrenIds` is cleared and `activeChildIndex`
|
|
111
|
+
* is reset to 0, which makes the node the new leaf of its branch.
|
|
112
|
+
*
|
|
113
|
+
* Useful when editing a message and discarding the conversation that was
|
|
114
|
+
* generated after it.
|
|
115
|
+
*/
|
|
116
|
+
export function cutBranchAfter(tree: ConversationTree, id: string): void {
|
|
117
|
+
const node = tree.nodes[id];
|
|
118
|
+
if (!node) return;
|
|
119
|
+
|
|
120
|
+
const stack: string[] = [...node.childrenIds];
|
|
121
|
+
while (stack.length > 0) {
|
|
122
|
+
const currentId = stack.pop() as string;
|
|
123
|
+
const current = tree.nodes[currentId];
|
|
124
|
+
if (!current) continue;
|
|
125
|
+
stack.push(...current.childrenIds);
|
|
126
|
+
delete tree.nodes[currentId];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
node.childrenIds = [];
|
|
130
|
+
node.activeChildIndex = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
/**
|
|
103
134
|
* Navigate to a different branch by updating the parent's activeChildIndex
|
|
104
135
|
* to point at the target sibling.
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -68,6 +68,19 @@ export type MessageContent =
|
|
|
68
68
|
| MessageToolContent
|
|
69
69
|
| MessageReasoningContent;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Open-ended metadata bag carried alongside a message. chat-kit is
|
|
73
|
+
* intentionally agnostic about its contents — adapter authors
|
|
74
|
+
* (`responsesToTree`, …) and message handlers preserve the value verbatim
|
|
75
|
+
* through the tree → linear path projection, but never inspect it.
|
|
76
|
+
*
|
|
77
|
+
* Consumers attach their own well-known keys (attachments, citations,
|
|
78
|
+
* ratings, …) and define their own typed projections at the read site
|
|
79
|
+
* rather than extending this base type. That keeps the handler API stable
|
|
80
|
+
* even as the shape of what gets stored on a message evolves.
|
|
81
|
+
*/
|
|
82
|
+
export type MessageMetadata = Record<string, unknown>;
|
|
83
|
+
|
|
71
84
|
export interface BaseMessage {
|
|
72
85
|
id: string;
|
|
73
86
|
role: MessageRole;
|
|
@@ -76,6 +89,7 @@ export interface BaseMessage {
|
|
|
76
89
|
status: MessageStatus;
|
|
77
90
|
siblingIds?: string[];
|
|
78
91
|
siblingIndex?: number;
|
|
92
|
+
metadata?: MessageMetadata;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
export interface TreeMessage {
|
|
@@ -87,6 +101,7 @@ export interface TreeMessage {
|
|
|
87
101
|
content: MessageContent[];
|
|
88
102
|
status: MessageStatus;
|
|
89
103
|
createdAt: Date;
|
|
104
|
+
metadata?: MessageMetadata;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
export interface ConversationTree {
|