@dxos/assistant 0.7.4-df37807 → 0.7.5-feature-compute.4d9d99a
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-AKP45LQH.mjs → chunk-LVLSJQB3.mjs} +506 -357
- package/dist/lib/browser/chunk-LVLSJQB3.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +17 -17
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +10 -3
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/{chunk-G6CGWEMO.cjs → chunk-SHOSAIOD.cjs} +507 -362
- package/dist/lib/node/chunk-SHOSAIOD.cjs.map +7 -0
- package/dist/lib/node/index.cjs +42 -42
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +18 -12
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/lib/node-esm/{chunk-WUPUPEFB.mjs → chunk-ZLBTRSUY.mjs} +506 -357
- package/dist/lib/node-esm/chunk-ZLBTRSUY.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +17 -17
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +10 -3
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/ai-service/client.d.ts +15 -32
- package/dist/types/src/ai-service/client.d.ts.map +1 -1
- package/dist/types/src/ai-service/index.d.ts +2 -1
- package/dist/types/src/ai-service/index.d.ts.map +1 -1
- package/dist/types/src/ai-service/stream.d.ts +34 -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/types/index.d.ts +3 -0
- package/dist/types/src/ai-service/types/index.d.ts.map +1 -0
- package/dist/types/src/ai-service/types/message.d.ts +191 -0
- package/dist/types/src/ai-service/types/message.d.ts.map +1 -0
- package/dist/types/src/ai-service/types/schema.d.ts +175 -0
- package/dist/types/src/ai-service/types/schema.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 +54 -4
- package/dist/types/src/conversation/conversation.d.ts.map +1 -1
- package/dist/types/src/conversation/types.d.ts +35 -4
- 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/test-data.d.ts +1 -1
- package/dist/types/src/testing/test-data.d.ts.map +1 -1
- package/package.json +20 -18
- package/src/ai-service/client.test.ts +25 -26
- package/src/ai-service/client.ts +29 -177
- package/src/ai-service/index.ts +2 -1
- package/src/ai-service/stream.test.ts +186 -0
- package/src/ai-service/stream.ts +181 -0
- package/src/ai-service/types/index.ts +6 -0
- package/src/ai-service/types/message.ts +147 -0
- package/src/ai-service/types/schema.ts +128 -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 +6 -9
- package/src/conversation/conversation.test.ts +10 -14
- package/src/conversation/conversation.ts +109 -14
- package/src/conversation/types.ts +47 -6
- package/src/cypher/combinators.test.ts +3 -3
- 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/query.test.ts +9 -9
- 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.map +0 -7
- package/dist/lib/node/chunk-G6CGWEMO.cjs.map +0 -7
- 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
package/src/ai-service/client.ts
CHANGED
|
@@ -2,40 +2,35 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
/* eslint-disable no-console */
|
|
6
|
-
|
|
7
5
|
import { Schema as S } from '@effect/schema';
|
|
8
6
|
|
|
9
|
-
import { Trigger } from '@dxos/async';
|
|
10
7
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import type
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
Message,
|
|
15
|
-
type GenerateRequest,
|
|
16
|
-
type ObjectId,
|
|
17
|
-
type ResultStreamEvent,
|
|
18
|
-
type Space,
|
|
19
|
-
type Thread,
|
|
20
|
-
} from './schema';
|
|
21
|
-
import { iterSSEMessages } from './sse';
|
|
22
|
-
import { log } from '@dxos/log';
|
|
8
|
+
import { type SpaceId } from '@dxos/keys';
|
|
23
9
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
10
|
+
import { GenerationStream } from './stream';
|
|
11
|
+
import { Message, type GenerateRequest, type Space, type Thread } from './types';
|
|
27
12
|
|
|
28
13
|
export interface AIServiceClient {
|
|
29
14
|
getSpace(spaceId: SpaceId): Promise<Space>;
|
|
30
15
|
getThread(spaceId: SpaceId, threadId: string): Promise<Thread>;
|
|
31
|
-
|
|
32
|
-
|
|
16
|
+
getMessages(spaceId: SpaceId, threadId: string): Promise<Message[]>;
|
|
17
|
+
appendMessages(messages: Message[]): Promise<void>;
|
|
33
18
|
updateMessage(spaceId: SpaceId, threadId: string, messageId: string, message: Message): Promise<void>;
|
|
34
19
|
generate(request: GenerateRequest): Promise<GenerationStream>;
|
|
35
20
|
}
|
|
36
21
|
|
|
22
|
+
// TODO(burdon): Create mock.
|
|
23
|
+
|
|
24
|
+
export type AIServiceClientParams = {
|
|
25
|
+
endpoint: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Edge GPT client.
|
|
30
|
+
*/
|
|
37
31
|
export class AIServiceClientImpl implements AIServiceClient {
|
|
38
32
|
private readonly _endpoint: string;
|
|
33
|
+
|
|
39
34
|
constructor({ endpoint }: AIServiceClientParams) {
|
|
40
35
|
this._endpoint = endpoint;
|
|
41
36
|
}
|
|
@@ -48,13 +43,14 @@ export class AIServiceClientImpl implements AIServiceClient {
|
|
|
48
43
|
throw new Error('Not implemented');
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
async
|
|
46
|
+
async getMessages(spaceId: SpaceId, threadId: string): Promise<Message[]> {
|
|
52
47
|
const res = await fetch(`${this._endpoint}/space/${spaceId}/thread/${threadId}/message`);
|
|
53
48
|
return S.decodePromise(S.Array(Message).pipe(S.mutable))((await res.json()).data.results);
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
async
|
|
57
|
-
const
|
|
51
|
+
async appendMessages(messages: Message[]): Promise<void> {
|
|
52
|
+
const url = `${this._endpoint}/message`;
|
|
53
|
+
const res = await fetch(url, {
|
|
58
54
|
method: 'POST',
|
|
59
55
|
headers: { 'Content-Type': 'application/json' },
|
|
60
56
|
body: JSON.stringify({
|
|
@@ -63,7 +59,7 @@ export class AIServiceClientImpl implements AIServiceClient {
|
|
|
63
59
|
});
|
|
64
60
|
|
|
65
61
|
if (!res.ok) {
|
|
66
|
-
throw new Error(await res.text());
|
|
62
|
+
throw new Error(`${await res.text()} [${url}]`);
|
|
67
63
|
}
|
|
68
64
|
}
|
|
69
65
|
|
|
@@ -72,172 +68,28 @@ export class AIServiceClientImpl implements AIServiceClient {
|
|
|
72
68
|
throw new Error('Not implemented');
|
|
73
69
|
}
|
|
74
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Open message stream.
|
|
73
|
+
*/
|
|
75
74
|
async generate(request: GenerateRequest): Promise<GenerationStream> {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const signal = controller.signal;
|
|
77
|
+
|
|
76
78
|
const response = await fetch(`${this._endpoint}/generate`, {
|
|
77
79
|
method: 'POST',
|
|
78
80
|
headers: { 'Content-Type': 'application/json' },
|
|
79
81
|
body: JSON.stringify(request),
|
|
82
|
+
signal,
|
|
80
83
|
});
|
|
81
|
-
invariant(response.body instanceof ReadableStream);
|
|
82
84
|
|
|
85
|
+
invariant(response.body instanceof ReadableStream);
|
|
83
86
|
return GenerationStream.fromSSEResponse(
|
|
84
87
|
{
|
|
85
88
|
spaceId: request.spaceId,
|
|
86
89
|
threadId: request.threadId,
|
|
87
90
|
},
|
|
88
91
|
response,
|
|
92
|
+
controller,
|
|
89
93
|
);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
|
-
|
|
93
|
-
type GenerationParams = {
|
|
94
|
-
spaceId: SpaceId;
|
|
95
|
-
threadId: ObjectId;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export class GenerationStream implements AsyncIterable<ResultStreamEvent> {
|
|
99
|
-
static fromSSEResponse(params: GenerationParams, response: Response) {
|
|
100
|
-
const controller = new AbortController();
|
|
101
|
-
const iterator = async function* () {
|
|
102
|
-
for await (const sse of iterSSEMessages(response, controller)) {
|
|
103
|
-
if (sse.event === 'completion') {
|
|
104
|
-
try {
|
|
105
|
-
yield JSON.parse(sse.data);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
console.error('Could not parse message into JSON:', sse.data);
|
|
108
|
-
console.error('From chunk:', sse.raw);
|
|
109
|
-
throw e;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
sse.event === 'message_start' ||
|
|
115
|
-
sse.event === 'message_delta' ||
|
|
116
|
-
sse.event === 'message_stop' ||
|
|
117
|
-
sse.event === 'content_block_start' ||
|
|
118
|
-
sse.event === 'content_block_delta' ||
|
|
119
|
-
sse.event === 'content_block_stop'
|
|
120
|
-
) {
|
|
121
|
-
try {
|
|
122
|
-
yield JSON.parse(sse.data);
|
|
123
|
-
} catch (e) {
|
|
124
|
-
console.error('Could not parse message into JSON:', sse.data);
|
|
125
|
-
console.error('From chunk:', sse.raw);
|
|
126
|
-
throw e;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (sse.event === 'ping') {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (sse.event === 'error') {
|
|
135
|
-
throw new Error(`Message generation error: ${sse.data}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
return new GenerationStream(params, iterator);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private _previousMessages: Message[] = [];
|
|
143
|
-
private _accumulatedMessage?: Message = undefined;
|
|
144
|
-
private _done = new Trigger();
|
|
145
|
-
private _iterator?: AsyncIterator<ResultStreamEvent> = undefined;
|
|
146
|
-
|
|
147
|
-
constructor(
|
|
148
|
-
private readonly _params: GenerationParams,
|
|
149
|
-
private readonly _getIterator: () => AsyncIterableIterator<ResultStreamEvent>,
|
|
150
|
-
) {}
|
|
151
|
-
|
|
152
|
-
[Symbol.asyncIterator](): AsyncIterator<ResultStreamEvent> {
|
|
153
|
-
return this._createIterator();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
cancel() {
|
|
157
|
-
throw new Error('Not implemented');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get accumulatedMessages(): Message[] {
|
|
161
|
-
// TODO(dmaretskyi): Support multiple accumulated messages.
|
|
162
|
-
return [...this._previousMessages, ...(this._accumulatedMessage == null ? [] : [this._accumulatedMessage])];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Returns the complete message list generated by the AI service.
|
|
167
|
-
*/
|
|
168
|
-
async complete(): Promise<Message[]> {
|
|
169
|
-
await this._done.wait();
|
|
170
|
-
return this.accumulatedMessages;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private _createIterator(): AsyncIterator<ResultStreamEvent> {
|
|
174
|
-
const self = this;
|
|
175
|
-
return (this._iterator ??= (async function* () {
|
|
176
|
-
for await (const event of self._getIterator()) {
|
|
177
|
-
self._processEvent(event);
|
|
178
|
-
yield event;
|
|
179
|
-
}
|
|
180
|
-
})());
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private _processEvent(event: ResultStreamEvent) {
|
|
184
|
-
// TODO(dmaretskyi): Support multiple message, e.g. for service-defined tools.
|
|
185
|
-
switch (event.type) {
|
|
186
|
-
case 'message_start': {
|
|
187
|
-
if (this._accumulatedMessage) {
|
|
188
|
-
this._previousMessages.push(this._accumulatedMessage);
|
|
189
|
-
this._accumulatedMessage = undefined;
|
|
190
|
-
}
|
|
191
|
-
this._accumulatedMessage = {
|
|
192
|
-
...event.message,
|
|
193
|
-
threadId: this._params.threadId,
|
|
194
|
-
spaceId: this._params.spaceId,
|
|
195
|
-
};
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case 'message_delta': {
|
|
199
|
-
if (this._accumulatedMessage === undefined) {
|
|
200
|
-
throw new Error('Received message delta without a message start');
|
|
201
|
-
}
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
case 'message_stop': {
|
|
205
|
-
if (this._accumulatedMessage === undefined) {
|
|
206
|
-
throw new Error('Received message stop without a message start');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
case 'content_block_start': {
|
|
212
|
-
invariant(this._accumulatedMessage);
|
|
213
|
-
this._accumulatedMessage.content.push(event.content);
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
case 'content_block_delta': {
|
|
217
|
-
invariant(this._accumulatedMessage);
|
|
218
|
-
const snapshotContent = this._accumulatedMessage.content.at(event.index);
|
|
219
|
-
if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') {
|
|
220
|
-
snapshotContent.text += event.delta.text;
|
|
221
|
-
} else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') {
|
|
222
|
-
snapshotContent.inputJson ??= '';
|
|
223
|
-
snapshotContent.inputJson += event.delta.partial_json;
|
|
224
|
-
|
|
225
|
-
// TODO(dmaretskyi): Partial parsing.
|
|
226
|
-
// if (jsonBuf) {
|
|
227
|
-
// snapshotContent.input = partialParse(jsonBuf);
|
|
228
|
-
// }
|
|
229
|
-
}
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
case 'content_block_stop': {
|
|
233
|
-
invariant(this._accumulatedMessage);
|
|
234
|
-
const lastBlock = this._accumulatedMessage.content.at(-1);
|
|
235
|
-
if (lastBlock?.type === 'tool_use') {
|
|
236
|
-
lastBlock.input = JSON.parse(lastBlock.inputJson ?? '');
|
|
237
|
-
}
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
this._done.wake();
|
|
242
|
-
}
|
|
243
|
-
}
|
package/src/ai-service/index.ts
CHANGED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { SaxesParser } from 'saxes';
|
|
6
|
+
import { describe, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { ObjectId } from '@dxos/echo-schema';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
|
|
11
|
+
import { GenerationStream } from './stream';
|
|
12
|
+
import { type Message } from './types';
|
|
13
|
+
|
|
14
|
+
type Part = { event: string; data: any };
|
|
15
|
+
|
|
16
|
+
export const createTestStream = (blocks: string[]): ReadableStream => {
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
return new ReadableStream({
|
|
19
|
+
start: (controller) => {
|
|
20
|
+
const push = (message: Part) => {
|
|
21
|
+
controller.enqueue(encoder.encode(`event: ${message.event}\ndata: ${JSON.stringify(message.data)}\n\n`));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
push({
|
|
25
|
+
event: 'message_start',
|
|
26
|
+
data: { type: 'message_start', message: { id: '123', role: 'assistant', content: [] } },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
for (const block of blocks) {
|
|
30
|
+
push({
|
|
31
|
+
event: 'content_block_start',
|
|
32
|
+
data: { type: 'content_block_start', content: { type: 'text', text: '' } },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let index = 0;
|
|
36
|
+
for (const word of block.split(' ')) {
|
|
37
|
+
push({
|
|
38
|
+
event: 'content_block_delta',
|
|
39
|
+
data: { type: 'content_block_delta', index, delta: { type: 'text_delta', text: word + ' ' } },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
index += word.length + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
push({
|
|
46
|
+
event: 'content_block_stop',
|
|
47
|
+
data: { type: 'content_block_stop' },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
push({
|
|
52
|
+
event: 'message_stop',
|
|
53
|
+
data: { type: 'message_stop' },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
controller.close();
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('GenerationStream', () => {
|
|
62
|
+
it('should stream messages', async ({ expect }) => {
|
|
63
|
+
const input = [['<cot>', '1. Ready.', '2. Aim.', '3. Fire.', '</cot>'].join('\n'), 'Hello world!'];
|
|
64
|
+
const output = [];
|
|
65
|
+
|
|
66
|
+
let block: string[] = [];
|
|
67
|
+
const stream = GenerationStream.fromSSEResponse({}, new Response(createTestStream(input)));
|
|
68
|
+
for await (const event of stream) {
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case 'content_block_start': {
|
|
71
|
+
block = [(event.content as any).text];
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'content_block_delta': {
|
|
76
|
+
block.push((event.delta as any).text);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'content_block_stop': {
|
|
81
|
+
output.push(block.join('').trim());
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expect(output.length).to.equal(input.length);
|
|
88
|
+
expect(output).to.deep.equal(input);
|
|
89
|
+
await stream.complete();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* https://astexplorer.net
|
|
94
|
+
*/
|
|
95
|
+
it('should parse the stream', async ({ expect }) => {
|
|
96
|
+
const input = [
|
|
97
|
+
// Text
|
|
98
|
+
`
|
|
99
|
+
Hello world!
|
|
100
|
+
`,
|
|
101
|
+
|
|
102
|
+
// COT
|
|
103
|
+
`
|
|
104
|
+
<cot>
|
|
105
|
+
1. Prepare the ship.
|
|
106
|
+
2. Arm the torpedoes.
|
|
107
|
+
3. Fire lasers.
|
|
108
|
+
</cot>
|
|
109
|
+
`,
|
|
110
|
+
|
|
111
|
+
// Artifact
|
|
112
|
+
// <?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
+
`
|
|
114
|
+
<artifact id="123" />
|
|
115
|
+
<choice id="123">
|
|
116
|
+
<artifact id="123" />
|
|
117
|
+
</choice>
|
|
118
|
+
`,
|
|
119
|
+
|
|
120
|
+
// JSON
|
|
121
|
+
`
|
|
122
|
+
<json>
|
|
123
|
+
<![CDATA[
|
|
124
|
+
{
|
|
125
|
+
"type": "text",
|
|
126
|
+
"text": "<test>OK</test>"
|
|
127
|
+
}
|
|
128
|
+
]]>
|
|
129
|
+
</json>
|
|
130
|
+
`,
|
|
131
|
+
|
|
132
|
+
// Code/JSX
|
|
133
|
+
`,
|
|
134
|
+
<code>
|
|
135
|
+
<![CDATA[
|
|
136
|
+
const Test = () => {
|
|
137
|
+
return <div>Hello world!</div>;
|
|
138
|
+
}
|
|
139
|
+
]]>
|
|
140
|
+
</code>
|
|
141
|
+
`,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// TODO(burdon): Delimiter for JSON blocks.
|
|
145
|
+
|
|
146
|
+
const output: Message = { id: ObjectId.random(), role: 'assistant', content: [] };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* https://www.npmjs.com/package/saxex
|
|
150
|
+
*/
|
|
151
|
+
const parser = new SaxesParser({
|
|
152
|
+
fragment: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
parser.on('error', (error) => {
|
|
156
|
+
log.error('error', { error });
|
|
157
|
+
});
|
|
158
|
+
parser.on('opentag', (node) => {
|
|
159
|
+
log.info('opentag', { node });
|
|
160
|
+
});
|
|
161
|
+
parser.on('closetag', (node) => {
|
|
162
|
+
log.info('closetag', { node });
|
|
163
|
+
});
|
|
164
|
+
parser.on('cdata', (text) => {
|
|
165
|
+
log.info('cdata', { text });
|
|
166
|
+
});
|
|
167
|
+
parser.on('text', (text) => {
|
|
168
|
+
log.info('text', { text });
|
|
169
|
+
});
|
|
170
|
+
parser.on('end', () => {
|
|
171
|
+
log.info('end');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const stream = GenerationStream.fromSSEResponse({}, new Response(createTestStream(input)));
|
|
175
|
+
for await (const event of stream) {
|
|
176
|
+
if (event.type === 'content_block_delta') {
|
|
177
|
+
// log.info('delta', { delta: (event.delta as any).text });
|
|
178
|
+
parser.write((event.delta as any).text);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await stream.complete();
|
|
183
|
+
|
|
184
|
+
expect(output.content).to.deep.equal([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Trigger } from '@dxos/async';
|
|
6
|
+
import { type ObjectId } from '@dxos/echo-schema';
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
import { type SpaceId } from '@dxos/keys';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
|
|
11
|
+
import { type Message, type ResultStreamEvent } from './types';
|
|
12
|
+
import { iterSSEMessages } from './util';
|
|
13
|
+
|
|
14
|
+
export type GenerationParams = {
|
|
15
|
+
spaceId?: SpaceId;
|
|
16
|
+
threadId?: ObjectId;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class GenerationStream implements AsyncIterable<ResultStreamEvent> {
|
|
20
|
+
/**
|
|
21
|
+
* Creates a stream from an SSE response.
|
|
22
|
+
*/
|
|
23
|
+
static fromSSEResponse(params: GenerationParams, response: Response, controller = new AbortController()) {
|
|
24
|
+
const iterator = async function* () {
|
|
25
|
+
for await (const sse of iterSSEMessages(response, controller)) {
|
|
26
|
+
if (sse.event === 'completion') {
|
|
27
|
+
try {
|
|
28
|
+
yield JSON.parse(sse.data);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
log.error('could not parse message into JSON:', { data: sse.data, raw: sse.raw });
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
sse.event === 'message_start' ||
|
|
37
|
+
sse.event === 'message_delta' ||
|
|
38
|
+
sse.event === 'message_stop' ||
|
|
39
|
+
sse.event === 'content_block_start' ||
|
|
40
|
+
sse.event === 'content_block_delta' ||
|
|
41
|
+
sse.event === 'content_block_stop'
|
|
42
|
+
) {
|
|
43
|
+
try {
|
|
44
|
+
yield JSON.parse(sse.data);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
log.error('could not parse message into JSON:', { data: sse.data, raw: sse.raw });
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (sse.event === 'ping') {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sse.event === 'error') {
|
|
56
|
+
throw new Error(`Message generation error: ${sse.data}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return new GenerationStream(controller, iterator, params);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private readonly _done = new Trigger();
|
|
65
|
+
private _previousMessages: Message[] = [];
|
|
66
|
+
private _accumulatedMessage?: Message = undefined;
|
|
67
|
+
private _iterator?: AsyncIterator<ResultStreamEvent> = undefined;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly _controller: AbortController,
|
|
71
|
+
private readonly _getIterator: () => AsyncIterableIterator<ResultStreamEvent>,
|
|
72
|
+
private readonly _params: GenerationParams,
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
[Symbol.asyncIterator](): AsyncIterator<ResultStreamEvent> {
|
|
76
|
+
return this._createIterator();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get accumulatedMessages(): Message[] {
|
|
80
|
+
// TODO(dmaretskyi): Support multiple accumulated messages.
|
|
81
|
+
return [...this._previousMessages, ...(this._accumulatedMessage == null ? [] : [this._accumulatedMessage])];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Aborts the stream.
|
|
86
|
+
*/
|
|
87
|
+
abort() {
|
|
88
|
+
this._controller.abort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the complete message list generated by the AI service.
|
|
93
|
+
*/
|
|
94
|
+
async complete(): Promise<Message[]> {
|
|
95
|
+
await this._done.wait();
|
|
96
|
+
return this.accumulatedMessages;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _createIterator(): AsyncIterator<ResultStreamEvent> {
|
|
100
|
+
const self = this;
|
|
101
|
+
return (this._iterator ??= (() => {
|
|
102
|
+
const generator = async function* (this: GenerationStream) {
|
|
103
|
+
try {
|
|
104
|
+
for await (const event of self._getIterator()) {
|
|
105
|
+
self._processEvent(event);
|
|
106
|
+
yield event;
|
|
107
|
+
}
|
|
108
|
+
this._done.wake();
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
this._done.throw(err);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return generator.call(this);
|
|
114
|
+
})());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _processEvent(event: ResultStreamEvent) {
|
|
118
|
+
// TODO(dmaretskyi): Support multiple message, e.g., for service-defined tools.
|
|
119
|
+
switch (event.type) {
|
|
120
|
+
case 'message_start': {
|
|
121
|
+
if (this._accumulatedMessage) {
|
|
122
|
+
this._previousMessages.push(this._accumulatedMessage);
|
|
123
|
+
this._accumulatedMessage = undefined;
|
|
124
|
+
}
|
|
125
|
+
this._accumulatedMessage = {
|
|
126
|
+
...event.message,
|
|
127
|
+
threadId: this._params.threadId,
|
|
128
|
+
spaceId: this._params.spaceId,
|
|
129
|
+
};
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'message_delta': {
|
|
134
|
+
if (this._accumulatedMessage === undefined) {
|
|
135
|
+
throw new Error('Received message delta without a message start');
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case 'message_stop': {
|
|
141
|
+
if (this._accumulatedMessage === undefined) {
|
|
142
|
+
throw new Error('Received message stop without a message start');
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case 'content_block_start': {
|
|
148
|
+
invariant(this._accumulatedMessage);
|
|
149
|
+
this._accumulatedMessage.content.push(event.content);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'content_block_delta': {
|
|
154
|
+
invariant(this._accumulatedMessage);
|
|
155
|
+
const snapshotContent = this._accumulatedMessage.content.at(event.index);
|
|
156
|
+
if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') {
|
|
157
|
+
snapshotContent.text += event.delta.text;
|
|
158
|
+
} else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') {
|
|
159
|
+
snapshotContent.inputJson ??= '';
|
|
160
|
+
snapshotContent.inputJson += event.delta.partial_json;
|
|
161
|
+
|
|
162
|
+
// TODO(dmaretskyi): Partial parsing.
|
|
163
|
+
// if (jsonBuf) {
|
|
164
|
+
// snapshotContent.input = partialParse(jsonBuf);
|
|
165
|
+
// }
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'content_block_stop': {
|
|
171
|
+
invariant(this._accumulatedMessage);
|
|
172
|
+
const lastBlock = this._accumulatedMessage.content.at(-1);
|
|
173
|
+
if (lastBlock?.type === 'tool_use') {
|
|
174
|
+
lastBlock.input =
|
|
175
|
+
lastBlock.inputJson == null || lastBlock.inputJson === '' ? {} : JSON.parse(lastBlock.inputJson ?? '');
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|