@dxos/assistant 0.7.4 → 0.7.5-labs.5f04cf6
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/lib/browser/chunk-37SDA65J.mjs +565 -0
- package/dist/lib/browser/chunk-37SDA65J.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +936 -47
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +14 -8
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/chunk-UYNIAO5N.cjs +577 -0
- package/dist/lib/node/chunk-UYNIAO5N.cjs.map +7 -0
- package/dist/lib/node/index.cjs +936 -54
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +20 -13
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/lib/node-esm/chunk-AR24K7TY.mjs +567 -0
- package/dist/lib/node-esm/chunk-AR24K7TY.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +936 -47
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +14 -8
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/ai-service/client.d.ts +16 -32
- package/dist/types/src/ai-service/client.d.ts.map +1 -1
- package/dist/types/src/ai-service/defs.d.ts +134 -0
- package/dist/types/src/ai-service/defs.d.ts.map +1 -0
- package/dist/types/src/ai-service/index.d.ts +3 -1
- package/dist/types/src/ai-service/index.d.ts.map +1 -1
- package/dist/types/src/ai-service/parser.d.ts +40 -0
- package/dist/types/src/ai-service/parser.d.ts.map +1 -0
- package/dist/types/src/ai-service/stream.d.ts +35 -0
- package/dist/types/src/ai-service/stream.d.ts.map +1 -0
- package/dist/types/src/ai-service/stream.test.d.ts +2 -0
- package/dist/types/src/ai-service/stream.test.d.ts.map +1 -0
- package/dist/types/src/ai-service/util/index.d.ts +2 -0
- package/dist/types/src/ai-service/util/index.d.ts.map +1 -0
- package/dist/types/src/ai-service/{sse.d.ts → util/sse.d.ts} +5 -0
- package/dist/types/src/ai-service/util/sse.d.ts.map +1 -0
- package/dist/types/src/conversation/conversation.d.ts +56 -5
- package/dist/types/src/conversation/conversation.d.ts.map +1 -1
- package/dist/types/src/conversation/index.d.ts +0 -1
- package/dist/types/src/conversation/index.d.ts.map +1 -1
- package/dist/types/src/conversation/types.d.ts +44 -13
- package/dist/types/src/conversation/types.d.ts.map +1 -1
- package/dist/types/src/cypher/combinators.d.ts.map +1 -1
- package/dist/types/src/cypher/parjs.d.ts +1 -1
- package/dist/types/src/cypher/parjs.d.ts.map +1 -1
- package/dist/types/src/cypher/schema.d.ts.map +1 -1
- package/dist/types/src/testing/artifacts.test.d.ts +1 -0
- package/dist/types/src/testing/artifacts.test.d.ts.map +1 -0
- package/dist/types/src/testing/data.d.ts +2 -0
- package/dist/types/src/testing/data.d.ts.map +1 -0
- package/dist/types/src/testing/defs.d.ts +5 -0
- package/dist/types/src/testing/defs.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/logger.d.ts +1 -1
- package/dist/types/src/testing/logger.d.ts.map +1 -1
- package/dist/types/src/testing/query-promts.d.ts +1 -1
- package/dist/types/src/testing/query-promts.d.ts.map +1 -1
- package/dist/types/src/testing/test-data.d.ts +1 -1
- package/dist/types/src/testing/test-data.d.ts.map +1 -1
- package/dist/types/src/testing/test-schema.d.ts +4 -4
- package/package.json +20 -18
- package/src/ai-service/client.test.ts +28 -28
- package/src/ai-service/client.ts +31 -177
- package/src/ai-service/defs.ts +98 -0
- package/src/ai-service/index.ts +3 -1
- package/src/ai-service/parser.ts +183 -0
- package/src/ai-service/stream.test.ts +106 -0
- package/src/ai-service/stream.test.txt +33 -0
- package/src/ai-service/stream.ts +182 -0
- package/src/ai-service/util/index.ts +5 -0
- package/src/ai-service/{sse.ts → util/sse.ts} +5 -0
- package/src/bin/chat.ts +8 -10
- package/src/conversation/conversation.test.ts +13 -17
- package/src/conversation/conversation.ts +113 -18
- package/src/conversation/index.ts +0 -1
- package/src/conversation/types.ts +55 -16
- package/src/cypher/combinators.test.ts +4 -4
- package/src/cypher/combinators.ts +0 -1
- package/src/cypher/execution-plan.test.ts +5 -3
- package/src/cypher/parjs-shims.d.ts +4 -0
- package/src/cypher/parjs.ts +2 -2
- package/src/cypher/query-executor.test.ts +1 -1
- package/src/cypher/schema.ts +0 -1
- package/src/index.ts +1 -1
- package/src/testing/artifacts.test.ts +182 -0
- package/src/testing/data.ts +13 -0
- package/src/testing/defs.ts +8 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/logger.ts +3 -2
- package/src/testing/query-promts.ts +3 -3
- package/src/testing/query.test.ts +11 -10
- package/src/testing/test-data.ts +1 -1
- package/vendor/parjs-browser.mjs +4 -0
- package/vendor/parjs-default.mjs +8 -0
- package/dist/lib/browser/chunk-AKP45LQH.mjs +0 -1394
- package/dist/lib/browser/chunk-AKP45LQH.mjs.map +0 -7
- package/dist/lib/node/chunk-G6CGWEMO.cjs +0 -1418
- package/dist/lib/node/chunk-G6CGWEMO.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-WUPUPEFB.mjs +0 -1396
- package/dist/lib/node-esm/chunk-WUPUPEFB.mjs.map +0 -7
- package/dist/types/src/ai-service/schema.d.ts +0 -296
- package/dist/types/src/ai-service/schema.d.ts.map +0 -1
- package/dist/types/src/ai-service/sse.d.ts.map +0 -1
- package/dist/types/src/sanity.test.d.ts +0 -2
- package/dist/types/src/sanity.test.d.ts.map +0 -1
- package/src/ai-service/schema.ts +0 -260
- package/src/sanity.test.ts +0 -9
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export type Block = TextBlock | TagBlock | CodeBlock;
|
|
6
|
+
|
|
7
|
+
export type TextBlock = {
|
|
8
|
+
type: 'text';
|
|
9
|
+
content: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TagBlock = {
|
|
13
|
+
type: 'tag';
|
|
14
|
+
name: string;
|
|
15
|
+
content: string;
|
|
16
|
+
attributes: Record<string, string>;
|
|
17
|
+
selfClosing: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CodeBlock = {
|
|
21
|
+
type: 'code';
|
|
22
|
+
language: string;
|
|
23
|
+
content: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Permissive streaming parser for XML content and plain text.
|
|
28
|
+
*/
|
|
29
|
+
export class StreamingParser {
|
|
30
|
+
private buffer = '';
|
|
31
|
+
|
|
32
|
+
constructor(private readonly onBlock: (block: Block) => void) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write a chunk of text to the parser.
|
|
36
|
+
*/
|
|
37
|
+
write(chunk: string) {
|
|
38
|
+
this.buffer += chunk;
|
|
39
|
+
this.processBuffer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Signal that no more input will be written.
|
|
44
|
+
*/
|
|
45
|
+
end() {
|
|
46
|
+
if (this.buffer.length > 0) {
|
|
47
|
+
this.emitTextBlock(this.buffer);
|
|
48
|
+
this.buffer = '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private processBuffer() {
|
|
53
|
+
while (this.buffer.length > 0) {
|
|
54
|
+
// Try to parse code block first.
|
|
55
|
+
if (this.tryParseCodeBlock()) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If no tag start found, keep buffering.
|
|
60
|
+
const tagStartIndex = this.buffer.indexOf('<');
|
|
61
|
+
if (tagStartIndex === -1) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Emit any text before the tag.
|
|
66
|
+
if (tagStartIndex > 0) {
|
|
67
|
+
this.emitTextBlock(this.buffer.slice(0, tagStartIndex));
|
|
68
|
+
this.buffer = this.buffer.slice(tagStartIndex);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If incomplete tag, keep buffering.
|
|
73
|
+
const tagEndIndex = this.buffer.indexOf('>', tagStartIndex);
|
|
74
|
+
if (tagEndIndex === -1) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if this is a self-closing tag.
|
|
79
|
+
const isSelfClosing = this.buffer[tagEndIndex - 1] === '/';
|
|
80
|
+
const actualTagEndIndex = isSelfClosing ? tagEndIndex - 1 : tagEndIndex;
|
|
81
|
+
|
|
82
|
+
// Parse the tag name and attributes.
|
|
83
|
+
const tagContent = this.buffer.slice(tagStartIndex + 1, actualTagEndIndex).trim();
|
|
84
|
+
const [tagName, ...attrParts] = tagContent.split(/\s+/);
|
|
85
|
+
const attributes = this.parseAttributes(attrParts.join(' '));
|
|
86
|
+
|
|
87
|
+
if (isSelfClosing) {
|
|
88
|
+
this.emitTagBlock(tagName, '', attributes, true);
|
|
89
|
+
this.buffer = this.buffer.slice(tagEndIndex + 1);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If no closing tag yet, keep buffering.
|
|
94
|
+
const closingTagIndex = this.buffer.indexOf('</' + tagName + '>', tagEndIndex);
|
|
95
|
+
if (closingTagIndex === -1) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Complete tag.
|
|
100
|
+
const content = this.buffer.slice(tagEndIndex + 1, closingTagIndex);
|
|
101
|
+
this.emitTagBlock(tagName, content, attributes, false);
|
|
102
|
+
this.buffer = this.buffer.slice(closingTagIndex + tagName.length + 3); // +3 for '</>'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private tryParseCodeBlock(): boolean {
|
|
107
|
+
const codeBlockStart = this.buffer.indexOf('```');
|
|
108
|
+
if (codeBlockStart === -1) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If there's text before the code block, emit it first.
|
|
113
|
+
if (codeBlockStart > 0) {
|
|
114
|
+
this.emitTextBlock(this.buffer.slice(0, codeBlockStart));
|
|
115
|
+
this.buffer = this.buffer.slice(codeBlockStart);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Look for the closing fence.
|
|
120
|
+
const nextNewline = this.buffer.indexOf('\n', codeBlockStart);
|
|
121
|
+
if (nextNewline === -1) {
|
|
122
|
+
return false; // Incomplete, keep buffering.
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const codeBlockEnd = this.buffer.indexOf('\n```', nextNewline);
|
|
126
|
+
if (codeBlockEnd === -1) {
|
|
127
|
+
return false; // No closing fence yet, keep buffering.
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract language (if specified) and content.
|
|
131
|
+
const languageLine = this.buffer.slice(3, nextNewline).trim();
|
|
132
|
+
const content = this.buffer.slice(nextNewline + 1, codeBlockEnd);
|
|
133
|
+
|
|
134
|
+
this.emitCodeBlock(languageLine, content);
|
|
135
|
+
this.buffer = this.buffer.slice(codeBlockEnd + 4); // +4 for '\n```'
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private parseAttributes(attrString: string): Record<string, string> {
|
|
140
|
+
const attributes: Record<string, string> = {};
|
|
141
|
+
if (!attrString) {
|
|
142
|
+
return attributes;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Match attribute patterns like: name="value" or name='value' or name=value.
|
|
146
|
+
const attrPattern = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
|
|
147
|
+
let match;
|
|
148
|
+
|
|
149
|
+
while ((match = attrPattern.exec(attrString)) !== null) {
|
|
150
|
+
const [, name, doubleQuoted, singleQuoted, unquoted] = match;
|
|
151
|
+
attributes[name] = doubleQuoted ?? singleQuoted ?? unquoted;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return attributes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private emitTextBlock(text: string) {
|
|
158
|
+
if (text.trim().length > 0) {
|
|
159
|
+
this.onBlock({
|
|
160
|
+
type: 'text',
|
|
161
|
+
content: text,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private emitTagBlock(name: string, content: string, attributes: Record<string, string>, selfClosing: boolean) {
|
|
167
|
+
this.onBlock({
|
|
168
|
+
type: 'tag',
|
|
169
|
+
name,
|
|
170
|
+
content: content.trim(),
|
|
171
|
+
attributes,
|
|
172
|
+
selfClosing,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private emitCodeBlock(language: string, content: string) {
|
|
177
|
+
this.onBlock({
|
|
178
|
+
type: 'code',
|
|
179
|
+
language,
|
|
180
|
+
content: content.trim(),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type Block, StreamingParser } from './parser';
|
|
8
|
+
import { GenerationStream } from './stream';
|
|
9
|
+
/** @ts-ignore */
|
|
10
|
+
import TEXT from './stream.test.txt?raw';
|
|
11
|
+
|
|
12
|
+
type Part = { event: string; data: any };
|
|
13
|
+
|
|
14
|
+
describe('GenerationStream', () => {
|
|
15
|
+
it('should stream messages', async ({ expect }) => {
|
|
16
|
+
const input = [['<cot>', '1. Ready.', '2. Aim.', '3. Fire.', '</cot>'].join('\n'), 'Hello world!'];
|
|
17
|
+
const output = [];
|
|
18
|
+
|
|
19
|
+
let block: string[] = [];
|
|
20
|
+
const stream = GenerationStream.fromSSEResponse({}, new Response(createTestStream(input)));
|
|
21
|
+
for await (const event of stream) {
|
|
22
|
+
switch (event.type) {
|
|
23
|
+
case 'content_block_start': {
|
|
24
|
+
block = [(event.content as any).text];
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case 'content_block_delta': {
|
|
29
|
+
block.push((event.delta as any).text);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case 'content_block_stop': {
|
|
34
|
+
output.push(block.join('').trim());
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
expect(output.length).to.equal(input.length);
|
|
41
|
+
expect(output).to.deep.equal(input);
|
|
42
|
+
await stream.complete();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should parse mixed content.', ({ expect }) => {
|
|
46
|
+
const blocks: Block[] = [];
|
|
47
|
+
const parser = new StreamingParser((block) => {
|
|
48
|
+
blocks.push(block);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const words = TEXT.split(' ');
|
|
52
|
+
for (const word of words) {
|
|
53
|
+
parser.write(word + ' ');
|
|
54
|
+
}
|
|
55
|
+
parser.end();
|
|
56
|
+
|
|
57
|
+
expect(blocks).to.have.length(7);
|
|
58
|
+
expect(blocks.map((block) => block.type)).to.deep.eq(['text', 'tag', 'text', 'tag', 'text', 'code', 'code']);
|
|
59
|
+
expect(blocks.map((block) => block.type === 'tag' && block.name).filter(Boolean)).to.deep.eq(['cot', 'artifact']);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const createTestStream = (blocks: string[]): ReadableStream => {
|
|
64
|
+
const encoder = new TextEncoder();
|
|
65
|
+
return new ReadableStream({
|
|
66
|
+
start: (controller) => {
|
|
67
|
+
const push = (message: Part) => {
|
|
68
|
+
controller.enqueue(encoder.encode(`event: ${message.event}\ndata: ${JSON.stringify(message.data)}\n\n`));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
push({
|
|
72
|
+
event: 'message_start',
|
|
73
|
+
data: { type: 'message_start', message: { id: '123', role: 'assistant', content: [] } },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
for (const block of blocks) {
|
|
77
|
+
push({
|
|
78
|
+
event: 'content_block_start',
|
|
79
|
+
data: { type: 'content_block_start', content: { type: 'text', text: '' } },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let index = 0;
|
|
83
|
+
for (const word of block.split(' ')) {
|
|
84
|
+
push({
|
|
85
|
+
event: 'content_block_delta',
|
|
86
|
+
data: { type: 'content_block_delta', index, delta: { type: 'text_delta', text: word + ' ' } },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
index += word.length + 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
push({
|
|
93
|
+
event: 'content_block_stop',
|
|
94
|
+
data: { type: 'content_block_stop' },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
push({
|
|
99
|
+
event: 'message_stop',
|
|
100
|
+
data: { type: 'message_stop' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
controller.close();
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test": 100
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
<cot>
|
|
6
|
+
1. Planning step.
|
|
7
|
+
2. Research step.
|
|
8
|
+
3. Output step.
|
|
9
|
+
</cot>
|
|
10
|
+
|
|
11
|
+
This is some text.
|
|
12
|
+
|
|
13
|
+
This is a tool call.
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
"test": 100
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
<artifact id="100" />
|
|
20
|
+
|
|
21
|
+
This is some code.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"test": 100
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const Test = () => {
|
|
31
|
+
return <div>Hello</div>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Message } from '@dxos/artifact';
|
|
6
|
+
import { Trigger } from '@dxos/async';
|
|
7
|
+
import { type ObjectId } from '@dxos/echo-schema';
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { type SpaceId } from '@dxos/keys';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
|
|
12
|
+
import { type ResultStreamEvent } from './defs';
|
|
13
|
+
import { iterSSEMessages } from './util';
|
|
14
|
+
|
|
15
|
+
export type GenerationParams = {
|
|
16
|
+
spaceId?: SpaceId;
|
|
17
|
+
threadId?: ObjectId;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class GenerationStream implements AsyncIterable<ResultStreamEvent> {
|
|
21
|
+
/**
|
|
22
|
+
* Creates a stream from an SSE response.
|
|
23
|
+
*/
|
|
24
|
+
static fromSSEResponse(params: GenerationParams, response: Response, controller = new AbortController()) {
|
|
25
|
+
const iterator = async function* () {
|
|
26
|
+
for await (const sse of iterSSEMessages(response, controller)) {
|
|
27
|
+
if (sse.event === 'completion') {
|
|
28
|
+
try {
|
|
29
|
+
yield JSON.parse(sse.data);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
log.error('could not parse message into JSON:', { data: sse.data, raw: sse.raw });
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
sse.event === 'message_start' ||
|
|
38
|
+
sse.event === 'message_delta' ||
|
|
39
|
+
sse.event === 'message_stop' ||
|
|
40
|
+
sse.event === 'content_block_start' ||
|
|
41
|
+
sse.event === 'content_block_delta' ||
|
|
42
|
+
sse.event === 'content_block_stop'
|
|
43
|
+
) {
|
|
44
|
+
try {
|
|
45
|
+
yield JSON.parse(sse.data);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.error('could not parse message into JSON:', { data: sse.data, raw: sse.raw });
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (sse.event === 'ping') {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (sse.event === 'error') {
|
|
57
|
+
throw new Error(`Message generation error: ${sse.data}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return new GenerationStream(controller, iterator, params);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private readonly _done = new Trigger();
|
|
66
|
+
private _previousMessages: Message[] = [];
|
|
67
|
+
private _accumulatedMessage?: Message = undefined;
|
|
68
|
+
private _iterator?: AsyncIterator<ResultStreamEvent> = undefined;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly _controller: AbortController,
|
|
72
|
+
private readonly _getIterator: () => AsyncIterableIterator<ResultStreamEvent>,
|
|
73
|
+
private readonly _params: GenerationParams,
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
[Symbol.asyncIterator](): AsyncIterator<ResultStreamEvent> {
|
|
77
|
+
return this._createIterator();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get accumulatedMessages(): Message[] {
|
|
81
|
+
// TODO(dmaretskyi): Support multiple accumulated messages.
|
|
82
|
+
return [...this._previousMessages, ...(this._accumulatedMessage == null ? [] : [this._accumulatedMessage])];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Aborts the stream.
|
|
87
|
+
*/
|
|
88
|
+
abort() {
|
|
89
|
+
this._controller.abort();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns the complete message list generated by the AI service.
|
|
94
|
+
*/
|
|
95
|
+
async complete(): Promise<Message[]> {
|
|
96
|
+
await this._done.wait();
|
|
97
|
+
return this.accumulatedMessages;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private _createIterator(): AsyncIterator<ResultStreamEvent> {
|
|
101
|
+
const self = this;
|
|
102
|
+
return (this._iterator ??= (() => {
|
|
103
|
+
const generator = async function* (this: GenerationStream) {
|
|
104
|
+
try {
|
|
105
|
+
for await (const event of self._getIterator()) {
|
|
106
|
+
self._processEvent(event);
|
|
107
|
+
yield event;
|
|
108
|
+
}
|
|
109
|
+
this._done.wake();
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
this._done.throw(err);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return generator.call(this);
|
|
115
|
+
})());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private _processEvent(event: ResultStreamEvent) {
|
|
119
|
+
// TODO(dmaretskyi): Support multiple message, e.g., for service-defined tools.
|
|
120
|
+
switch (event.type) {
|
|
121
|
+
case 'message_start': {
|
|
122
|
+
if (this._accumulatedMessage) {
|
|
123
|
+
this._previousMessages.push(this._accumulatedMessage);
|
|
124
|
+
this._accumulatedMessage = undefined;
|
|
125
|
+
}
|
|
126
|
+
this._accumulatedMessage = {
|
|
127
|
+
...event.message,
|
|
128
|
+
threadId: this._params.threadId,
|
|
129
|
+
spaceId: this._params.spaceId,
|
|
130
|
+
};
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'message_delta': {
|
|
135
|
+
if (this._accumulatedMessage === undefined) {
|
|
136
|
+
throw new Error('Received message delta without a message start');
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'message_stop': {
|
|
142
|
+
if (this._accumulatedMessage === undefined) {
|
|
143
|
+
throw new Error('Received message stop without a message start');
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'content_block_start': {
|
|
149
|
+
invariant(this._accumulatedMessage);
|
|
150
|
+
this._accumulatedMessage.content.push(event.content);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'content_block_delta': {
|
|
155
|
+
invariant(this._accumulatedMessage);
|
|
156
|
+
const snapshotContent = this._accumulatedMessage.content.at(event.index);
|
|
157
|
+
if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') {
|
|
158
|
+
snapshotContent.text += event.delta.text;
|
|
159
|
+
} else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') {
|
|
160
|
+
snapshotContent.inputJson ??= '';
|
|
161
|
+
snapshotContent.inputJson += event.delta.partial_json;
|
|
162
|
+
|
|
163
|
+
// TODO(dmaretskyi): Partial parsing.
|
|
164
|
+
// if (jsonBuf) {
|
|
165
|
+
// snapshotContent.input = partialParse(jsonBuf);
|
|
166
|
+
// }
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'content_block_stop': {
|
|
172
|
+
invariant(this._accumulatedMessage);
|
|
173
|
+
const lastBlock = this._accumulatedMessage.content.at(-1);
|
|
174
|
+
if (lastBlock?.type === 'tool_use') {
|
|
175
|
+
lastBlock.input =
|
|
176
|
+
lastBlock.inputJson == null || lastBlock.inputJson === '' ? {} : JSON.parse(lastBlock.inputJson ?? '');
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/bin/chat.ts
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { writeFileSync } from 'fs';
|
|
5
6
|
import inquirer from 'inquirer';
|
|
6
7
|
|
|
8
|
+
import { createUserMessage } from '@dxos/artifact';
|
|
9
|
+
import { ObjectId } from '@dxos/echo-schema';
|
|
7
10
|
import { SpaceId } from '@dxos/keys';
|
|
8
11
|
|
|
9
|
-
import { AIServiceClientImpl,
|
|
10
|
-
import { runLLM
|
|
12
|
+
import { AIServiceClientImpl, ToolTypes } from '../ai-service';
|
|
13
|
+
import { runLLM } from '../conversation';
|
|
11
14
|
import {
|
|
15
|
+
AI_SERVICE_ENDPOINT,
|
|
12
16
|
createLogger,
|
|
13
17
|
createCypherTool,
|
|
14
18
|
createSystemPrompt,
|
|
@@ -18,15 +22,9 @@ import {
|
|
|
18
22
|
Project,
|
|
19
23
|
Task,
|
|
20
24
|
} from '../testing';
|
|
21
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
22
|
-
|
|
23
|
-
// TODO(burdon): Move out of src?
|
|
24
|
-
|
|
25
|
-
const ENDPOINT = 'http://localhost:8787';
|
|
26
|
-
// const ENDPOINT = 'https://ai-service.dxos.workers.dev';
|
|
27
25
|
|
|
28
26
|
const client = new AIServiceClientImpl({
|
|
29
|
-
endpoint:
|
|
27
|
+
endpoint: AI_SERVICE_ENDPOINT.LOCAL,
|
|
30
28
|
});
|
|
31
29
|
|
|
32
30
|
const dataSource = createTestData();
|
|
@@ -46,7 +44,7 @@ while (true) {
|
|
|
46
44
|
message: 'Enter a message:',
|
|
47
45
|
},
|
|
48
46
|
]);
|
|
49
|
-
await client.
|
|
47
|
+
await client.appendMessages([createUserMessage(spaceId, threadId, prompt.message)]);
|
|
50
48
|
|
|
51
49
|
await runLLM({
|
|
52
50
|
model: '@anthropic/claude-3-5-sonnet-20241022',
|
|
@@ -5,37 +5,35 @@
|
|
|
5
5
|
import { Schema as S } from '@effect/schema';
|
|
6
6
|
import { describe, test } from 'vitest';
|
|
7
7
|
|
|
8
|
+
import { createUserMessage, defineTool, ToolResult } from '@dxos/artifact';
|
|
9
|
+
import { ObjectId } from '@dxos/echo-schema';
|
|
8
10
|
import { SpaceId } from '@dxos/keys';
|
|
9
11
|
import { log } from '@dxos/log';
|
|
10
12
|
|
|
11
13
|
import { runLLM, type ConversationEvent } from './conversation';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
const ENDPOINT = 'http://localhost:8787';
|
|
14
|
+
import { AIServiceClientImpl } from '../ai-service';
|
|
15
|
+
import { AI_SERVICE_ENDPOINT } from '../testing';
|
|
16
16
|
|
|
17
17
|
describe.skip('Conversation tests', () => {
|
|
18
18
|
const client = new AIServiceClientImpl({
|
|
19
|
-
endpoint:
|
|
19
|
+
endpoint: AI_SERVICE_ENDPOINT.LOCAL,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test('hello', async ({ expect }) => {
|
|
23
23
|
const spaceId = SpaceId.random();
|
|
24
24
|
const threadId = ObjectId.random();
|
|
25
25
|
|
|
26
|
-
await client.insertMessages([createUserMessage(spaceId, threadId, 'Hello, how are you?')]);
|
|
27
26
|
const result = await runLLM({
|
|
28
27
|
model: '@anthropic/claude-3-5-sonnet-20241022',
|
|
29
|
-
spaceId,
|
|
30
|
-
threadId,
|
|
28
|
+
history: [createUserMessage(spaceId, threadId, 'Hello, how are you?')],
|
|
31
29
|
tools: [],
|
|
32
30
|
client,
|
|
33
31
|
logger: messageLogger,
|
|
34
32
|
});
|
|
35
|
-
log
|
|
33
|
+
log('result', { result });
|
|
36
34
|
});
|
|
37
35
|
|
|
38
|
-
test('tool call', async ({ expect }) => {
|
|
36
|
+
test.only('tool call', async ({ expect }) => {
|
|
39
37
|
const custodian = defineTool({
|
|
40
38
|
name: 'custodian',
|
|
41
39
|
description: 'Custodian can tell you the password if you say the magic word',
|
|
@@ -44,9 +42,9 @@ describe.skip('Conversation tests', () => {
|
|
|
44
42
|
}),
|
|
45
43
|
execute: async ({ magicWord }) => {
|
|
46
44
|
if (magicWord === 'pretty please') {
|
|
47
|
-
return
|
|
45
|
+
return ToolResult.Success('The password is: "The sky is gray"');
|
|
48
46
|
} else {
|
|
49
|
-
return
|
|
47
|
+
return ToolResult.Error('Wrong magic word');
|
|
50
48
|
}
|
|
51
49
|
},
|
|
52
50
|
});
|
|
@@ -54,21 +52,19 @@ describe.skip('Conversation tests', () => {
|
|
|
54
52
|
const spaceId = SpaceId.random();
|
|
55
53
|
const threadId = ObjectId.random();
|
|
56
54
|
|
|
57
|
-
await client.insertMessages([createUserMessage(spaceId, threadId, 'What is the password? Ask the custodian.')]);
|
|
58
55
|
const result = await runLLM({
|
|
59
56
|
model: '@anthropic/claude-3-5-sonnet-20241022',
|
|
60
|
-
spaceId,
|
|
61
|
-
threadId,
|
|
57
|
+
history: [createUserMessage(spaceId, threadId, 'What is the password? Ask the custodian.')],
|
|
62
58
|
tools: [custodian],
|
|
63
59
|
client,
|
|
64
60
|
logger: messageLogger,
|
|
65
61
|
});
|
|
66
|
-
log
|
|
62
|
+
log('result', { result });
|
|
67
63
|
});
|
|
68
64
|
|
|
69
65
|
const messageLogger = (event: ConversationEvent) => {
|
|
70
66
|
if (event.type === 'message') {
|
|
71
|
-
log
|
|
67
|
+
log('message', { message: event.message });
|
|
72
68
|
}
|
|
73
69
|
};
|
|
74
70
|
});
|