@dxos/assistant 0.7.4 → 0.7.5-labs.071a3e2
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-AXUUNA7O.mjs +565 -0
- package/dist/lib/browser/chunk-AXUUNA7O.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1212 -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 +15 -9
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/chunk-QPHGQINW.cjs +577 -0
- package/dist/lib/node/chunk-QPHGQINW.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1212 -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 +21 -14
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/lib/node-esm/chunk-GOVFOBIB.mjs +567 -0
- package/dist/lib/node-esm/chunk-GOVFOBIB.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1212 -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 +15 -9
- 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/index.d.ts +4 -1
- package/dist/types/src/ai-service/index.d.ts.map +1 -1
- package/dist/types/src/ai-service/parser.d.ts +136 -0
- package/dist/types/src/ai-service/parser.d.ts.map +1 -0
- package/dist/types/src/ai-service/stream.d.ts +39 -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/testing.d.ts +7 -0
- package/dist/types/src/ai-service/testing.d.ts.map +1 -0
- package/dist/types/src/ai-service/transform.d.ts +27 -0
- package/dist/types/src/ai-service/transform.d.ts.map +1 -0
- package/dist/types/src/ai-service/types.d.ts +150 -0
- package/dist/types/src/ai-service/types.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 +61 -6
- 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/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -18
- package/src/ai-service/client.test.ts +69 -54
- package/src/ai-service/client.ts +29 -184
- package/src/ai-service/index.ts +4 -1
- package/src/ai-service/parser.ts +338 -0
- package/src/ai-service/stream.test.ts +101 -0
- package/src/ai-service/stream.ts +116 -0
- package/src/ai-service/testing.ts +93 -0
- package/src/ai-service/transform.ts +121 -0
- package/src/ai-service/types.ts +107 -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 +10 -11
- package/src/conversation/conversation.test.ts +16 -17
- package/src/conversation/conversation.ts +126 -20
- 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 +6 -2
- 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 +8 -2
- 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
package/src/ai-service/client.ts
CHANGED
|
@@ -2,41 +2,38 @@
|
|
|
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 {
|
|
7
|
+
import { Message, type Space, type Thread } from '@dxos/artifact';
|
|
10
8
|
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';
|
|
9
|
+
import { type SpaceId } from '@dxos/keys';
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
11
|
+
import { createGenerationStream, type GenerationStream } from './stream';
|
|
12
|
+
import { type GenerateRequest } from './types';
|
|
27
13
|
|
|
14
|
+
// TODO(burdon): Rename.
|
|
28
15
|
export interface AIServiceClient {
|
|
29
16
|
getSpace(spaceId: SpaceId): Promise<Space>;
|
|
30
17
|
getThread(spaceId: SpaceId, threadId: string): Promise<Thread>;
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
getMessages(spaceId: SpaceId, threadId: string): Promise<Message[]>;
|
|
19
|
+
appendMessages(messages: Message[]): Promise<void>;
|
|
33
20
|
updateMessage(spaceId: SpaceId, threadId: string, messageId: string, message: Message): Promise<void>;
|
|
34
21
|
generate(request: GenerateRequest): Promise<GenerationStream>;
|
|
35
22
|
}
|
|
36
23
|
|
|
24
|
+
export type AIServiceClientParams = {
|
|
25
|
+
endpoint: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Edge GPT client.
|
|
30
|
+
*/
|
|
31
|
+
// TODO(burdon): Create mock.
|
|
37
32
|
export class AIServiceClientImpl implements AIServiceClient {
|
|
38
33
|
private readonly _endpoint: string;
|
|
34
|
+
|
|
39
35
|
constructor({ endpoint }: AIServiceClientParams) {
|
|
36
|
+
invariant(endpoint, 'endpoint is required');
|
|
40
37
|
this._endpoint = endpoint;
|
|
41
38
|
}
|
|
42
39
|
|
|
@@ -48,13 +45,14 @@ export class AIServiceClientImpl implements AIServiceClient {
|
|
|
48
45
|
throw new Error('Not implemented');
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
async
|
|
48
|
+
async getMessages(spaceId: SpaceId, threadId: string): Promise<Message[]> {
|
|
52
49
|
const res = await fetch(`${this._endpoint}/space/${spaceId}/thread/${threadId}/message`);
|
|
53
50
|
return S.decodePromise(S.Array(Message).pipe(S.mutable))((await res.json()).data.results);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
async
|
|
57
|
-
const
|
|
53
|
+
async appendMessages(messages: Message[]): Promise<void> {
|
|
54
|
+
const url = `${this._endpoint}/message`;
|
|
55
|
+
const res = await fetch(url, {
|
|
58
56
|
method: 'POST',
|
|
59
57
|
headers: { 'Content-Type': 'application/json' },
|
|
60
58
|
body: JSON.stringify({
|
|
@@ -63,7 +61,7 @@ export class AIServiceClientImpl implements AIServiceClient {
|
|
|
63
61
|
});
|
|
64
62
|
|
|
65
63
|
if (!res.ok) {
|
|
66
|
-
throw new Error(await res.text());
|
|
64
|
+
throw new Error(`${await res.text()} [${url}]`);
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -72,172 +70,19 @@ export class AIServiceClientImpl implements AIServiceClient {
|
|
|
72
70
|
throw new Error('Not implemented');
|
|
73
71
|
}
|
|
74
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Open message stream.
|
|
75
|
+
*/
|
|
75
76
|
async generate(request: GenerateRequest): Promise<GenerationStream> {
|
|
77
|
+
const controller = new AbortController();
|
|
76
78
|
const response = await fetch(`${this._endpoint}/generate`, {
|
|
79
|
+
signal: controller.signal,
|
|
77
80
|
method: 'POST',
|
|
78
81
|
headers: { 'Content-Type': 'application/json' },
|
|
79
82
|
body: JSON.stringify(request),
|
|
80
83
|
});
|
|
81
|
-
invariant(response.body instanceof ReadableStream);
|
|
82
|
-
|
|
83
|
-
return GenerationStream.fromSSEResponse(
|
|
84
|
-
{
|
|
85
|
-
spaceId: request.spaceId,
|
|
86
|
-
threadId: request.threadId,
|
|
87
|
-
},
|
|
88
|
-
response,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
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
84
|
|
|
130
|
-
|
|
131
|
-
|
|
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();
|
|
85
|
+
invariant(response.body instanceof ReadableStream);
|
|
86
|
+
return createGenerationStream(response, controller);
|
|
242
87
|
}
|
|
243
88
|
}
|
package/src/ai-service/index.ts
CHANGED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Message, type MessageContentBlock } from '@dxos/artifact';
|
|
6
|
+
import { Event } from '@dxos/async';
|
|
7
|
+
import { createStatic } from '@dxos/echo-schema';
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { isNotFalsy, safeParseJson } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { type GenerationStream } from './stream';
|
|
13
|
+
import { StreamTransform, type StreamBlock } from './transform';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse mixed content of plain text, XML fragments, and JSON blocks.
|
|
17
|
+
*/
|
|
18
|
+
export class MixedStreamParser {
|
|
19
|
+
/**
|
|
20
|
+
* New message.
|
|
21
|
+
*/
|
|
22
|
+
public message = new Event<Message>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Complete block added to Message.
|
|
26
|
+
*/
|
|
27
|
+
public block = new Event<MessageContentBlock>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Update partial block (while streaming).
|
|
31
|
+
*/
|
|
32
|
+
public update = new Event<MessageContentBlock>();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Current message.
|
|
36
|
+
*/
|
|
37
|
+
private _message?: Message | undefined;
|
|
38
|
+
|
|
39
|
+
private _emitBlock(contentBlock: MessageContentBlock, streamBlock?: StreamBlock) {
|
|
40
|
+
const messageBlock = streamBlock ? mergeMessageBlock(contentBlock, streamBlock) : contentBlock;
|
|
41
|
+
if (messageBlock) {
|
|
42
|
+
if (messageBlock.type === 'text' && messageBlock.text.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
invariant(this._message);
|
|
47
|
+
this._message.content.push(messageBlock);
|
|
48
|
+
this.block.emit(messageBlock);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private _emitUpdate(contentBlock: MessageContentBlock, streamBlock: StreamBlock) {
|
|
53
|
+
const messageBlock = mergeMessageBlock(contentBlock, streamBlock);
|
|
54
|
+
if (messageBlock) {
|
|
55
|
+
messageBlock.pending = true;
|
|
56
|
+
this.update.emit(messageBlock);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse stream until end.
|
|
62
|
+
*/
|
|
63
|
+
async parse(stream: GenerationStream) {
|
|
64
|
+
const transformer = new StreamTransform();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Current content message block.
|
|
68
|
+
*/
|
|
69
|
+
let contentBlock: MessageContentBlock | undefined;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Current partial block used to accumulate content.
|
|
73
|
+
*/
|
|
74
|
+
let streamBlock: StreamBlock | undefined;
|
|
75
|
+
const stack: StreamBlock[] = [];
|
|
76
|
+
|
|
77
|
+
for await (const event of stream) {
|
|
78
|
+
// log.info('event', { type: event.type, event });
|
|
79
|
+
switch (event.type) {
|
|
80
|
+
//
|
|
81
|
+
// Messages.
|
|
82
|
+
//
|
|
83
|
+
|
|
84
|
+
case 'message_start': {
|
|
85
|
+
if (this._message) {
|
|
86
|
+
log.warn('unexpected message_start');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._message = createStatic(Message, { role: 'assistant', content: [] });
|
|
90
|
+
this.message.emit(this._message);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'message_delta': {
|
|
95
|
+
if (!this._message) {
|
|
96
|
+
log.warn('unexpected message_delta');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'message_stop': {
|
|
103
|
+
if (!this._message) {
|
|
104
|
+
log.warn('unexpected message_stop');
|
|
105
|
+
} else {
|
|
106
|
+
this._message = undefined;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//
|
|
112
|
+
// Content blocks.
|
|
113
|
+
//
|
|
114
|
+
|
|
115
|
+
case 'content_block_start': {
|
|
116
|
+
if (contentBlock) {
|
|
117
|
+
log.warn('unexpected content_block_start', { content: contentBlock });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// TODO(burdon): Parse initial content (factor out from content_block_delta).
|
|
121
|
+
contentBlock = event.content;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'content_block_delta': {
|
|
126
|
+
if (!contentBlock) {
|
|
127
|
+
log.warn('unexpected content_block_delta');
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
switch (event.delta.type) {
|
|
132
|
+
//
|
|
133
|
+
// Text
|
|
134
|
+
//
|
|
135
|
+
case 'text_delta': {
|
|
136
|
+
const chunks = transformer.transform(event.delta.text);
|
|
137
|
+
for (const chunk of chunks) {
|
|
138
|
+
// log.info('text_delta', { chunk, current });
|
|
139
|
+
|
|
140
|
+
switch (streamBlock?.type) {
|
|
141
|
+
//
|
|
142
|
+
// XML Fragment.
|
|
143
|
+
//
|
|
144
|
+
case 'tag': {
|
|
145
|
+
if (chunk.type === 'tag') {
|
|
146
|
+
if (chunk.selfClosing) {
|
|
147
|
+
streamBlock.content.push(chunk);
|
|
148
|
+
} else if (chunk.closing) {
|
|
149
|
+
if (stack.length > 0) {
|
|
150
|
+
const top = stack.pop();
|
|
151
|
+
invariant(top && top.type === 'tag');
|
|
152
|
+
top.content.push(streamBlock);
|
|
153
|
+
streamBlock = top;
|
|
154
|
+
} else {
|
|
155
|
+
this._emitBlock(contentBlock, streamBlock);
|
|
156
|
+
streamBlock = undefined;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
stack.push(streamBlock);
|
|
160
|
+
streamBlock = chunk;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Append text.
|
|
164
|
+
if (streamBlock.content.length === 0) {
|
|
165
|
+
streamBlock.content.push(chunk);
|
|
166
|
+
} else {
|
|
167
|
+
const last = streamBlock.content.at(-1);
|
|
168
|
+
invariant(last);
|
|
169
|
+
if (last.type === 'text') {
|
|
170
|
+
last.content += chunk.content;
|
|
171
|
+
} else {
|
|
172
|
+
streamBlock.content.push(chunk);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//
|
|
180
|
+
// Text Fragment.
|
|
181
|
+
//
|
|
182
|
+
case 'text': {
|
|
183
|
+
if (chunk.type === 'tag') {
|
|
184
|
+
this._emitBlock(contentBlock, streamBlock);
|
|
185
|
+
if (chunk.selfClosing) {
|
|
186
|
+
this._emitBlock(contentBlock, chunk);
|
|
187
|
+
streamBlock = undefined;
|
|
188
|
+
} else {
|
|
189
|
+
streamBlock = chunk;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Append text.
|
|
193
|
+
streamBlock.content += chunk.content;
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//
|
|
199
|
+
// No current chunk.
|
|
200
|
+
//
|
|
201
|
+
default: {
|
|
202
|
+
if (chunk.type === 'tag' && chunk.selfClosing) {
|
|
203
|
+
this._emitBlock(contentBlock, chunk);
|
|
204
|
+
} else {
|
|
205
|
+
streamBlock = chunk;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//
|
|
214
|
+
// JSON
|
|
215
|
+
//
|
|
216
|
+
case 'input_json_delta': {
|
|
217
|
+
if (!contentBlock) {
|
|
218
|
+
log.warn('unexpected input_json_delta', { contentBlock });
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (streamBlock?.type === 'json') {
|
|
223
|
+
streamBlock.content += event.delta.partial_json;
|
|
224
|
+
} else {
|
|
225
|
+
streamBlock = { type: 'json', content: event.delta.partial_json };
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'content_block_stop': {
|
|
234
|
+
if (!contentBlock) {
|
|
235
|
+
log.warn('unexpected content_block_stop');
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this._emitBlock(contentBlock, streamBlock);
|
|
240
|
+
contentBlock = undefined;
|
|
241
|
+
streamBlock = undefined;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (contentBlock && streamBlock) {
|
|
247
|
+
this._emitUpdate(contentBlock, streamBlock);
|
|
248
|
+
}
|
|
249
|
+
} // for.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Convert stream block to message content block.
|
|
255
|
+
*/
|
|
256
|
+
export const mergeMessageBlock = (
|
|
257
|
+
contentBlock: MessageContentBlock,
|
|
258
|
+
streamBlock: StreamBlock,
|
|
259
|
+
): MessageContentBlock | undefined => {
|
|
260
|
+
log('mergeMessageBlock', { contentBlock, streamBlock });
|
|
261
|
+
|
|
262
|
+
switch (streamBlock.type) {
|
|
263
|
+
//
|
|
264
|
+
// Text
|
|
265
|
+
//
|
|
266
|
+
case 'text': {
|
|
267
|
+
return { ...contentBlock, type: 'text', text: streamBlock.content };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//
|
|
271
|
+
// XML
|
|
272
|
+
//
|
|
273
|
+
case 'tag': {
|
|
274
|
+
switch (streamBlock.tag) {
|
|
275
|
+
case 'cot': {
|
|
276
|
+
const content = streamBlock.content
|
|
277
|
+
.map((block) => {
|
|
278
|
+
switch (block.type) {
|
|
279
|
+
case 'text':
|
|
280
|
+
return block.content;
|
|
281
|
+
default:
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
.filter(isNotFalsy)
|
|
286
|
+
.join('\n');
|
|
287
|
+
|
|
288
|
+
return { ...contentBlock, type: 'text', disposition: 'cot', text: content };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'artifact': {
|
|
292
|
+
const { attributes } = streamBlock;
|
|
293
|
+
return { type: 'json', disposition: 'artifact', json: JSON.stringify(attributes) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case 'suggest': {
|
|
297
|
+
if (streamBlock.content.length === 1 && streamBlock.content[0].type === 'text') {
|
|
298
|
+
return {
|
|
299
|
+
type: 'json',
|
|
300
|
+
disposition: 'suggest',
|
|
301
|
+
json: JSON.stringify({ text: streamBlock.content[0].content }),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case 'select': {
|
|
308
|
+
return {
|
|
309
|
+
type: 'json',
|
|
310
|
+
disposition: 'select',
|
|
311
|
+
json: JSON.stringify({
|
|
312
|
+
options: streamBlock.content.flatMap((content) =>
|
|
313
|
+
content.type === 'tag' && content.content.length === 1 && content.content[0].type === 'text'
|
|
314
|
+
? [content.content[0].content]
|
|
315
|
+
: [],
|
|
316
|
+
),
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//
|
|
326
|
+
// JSON
|
|
327
|
+
//
|
|
328
|
+
case 'json': {
|
|
329
|
+
switch (contentBlock.type) {
|
|
330
|
+
case 'tool_use': {
|
|
331
|
+
return { ...contentBlock, input: safeParseJson(streamBlock.content) ?? {} };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { ...contentBlock, type: 'json', disposition: 'artifact', json: streamBlock.content };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type Message } from '@dxos/artifact';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
|
|
10
|
+
import { MixedStreamParser } from './parser';
|
|
11
|
+
import { createGenerationStream } from './stream';
|
|
12
|
+
import { createTestSSEStream } from './testing';
|
|
13
|
+
import { type GenerationStreamEvent } from './types';
|
|
14
|
+
|
|
15
|
+
// TODO(burdon): Tool request/response.
|
|
16
|
+
|
|
17
|
+
const TEST_BLOCKS = [
|
|
18
|
+
[
|
|
19
|
+
// Text
|
|
20
|
+
'some text',
|
|
21
|
+
'on multiple lines.',
|
|
22
|
+
|
|
23
|
+
// XML
|
|
24
|
+
'<cot>',
|
|
25
|
+
' 1. Analyze content.',
|
|
26
|
+
' 2. Create plan.',
|
|
27
|
+
' 3. Generate artifacts.',
|
|
28
|
+
'</cot>',
|
|
29
|
+
|
|
30
|
+
// Text
|
|
31
|
+
'some more text.',
|
|
32
|
+
|
|
33
|
+
// XML
|
|
34
|
+
'<artifact id="123" />',
|
|
35
|
+
|
|
36
|
+
// XML
|
|
37
|
+
'<artifact id="456" />',
|
|
38
|
+
|
|
39
|
+
// Text
|
|
40
|
+
'and some more text.',
|
|
41
|
+
|
|
42
|
+
'<suggest>Plan me a trip to the Rio.</suggest>',
|
|
43
|
+
'<suggest>Show me steak recipes.</suggest>',
|
|
44
|
+
'<select><option>Yes</option><option>No</option></select>',
|
|
45
|
+
].join('\n'),
|
|
46
|
+
|
|
47
|
+
// JSON
|
|
48
|
+
{
|
|
49
|
+
type: 'input',
|
|
50
|
+
value: {
|
|
51
|
+
level: 1,
|
|
52
|
+
optional: true,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
describe('GenerationStream', () => {
|
|
58
|
+
it('should generate a stream of blocks', async ({ expect }) => {
|
|
59
|
+
const stream = createGenerationStream(new Response(createTestSSEStream(TEST_BLOCKS)));
|
|
60
|
+
const events: GenerationStreamEvent[] = [];
|
|
61
|
+
for await (const event of stream) {
|
|
62
|
+
events.push(event);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(events.map((event) => event.type === 'content_block_start').filter(Boolean)).to.have.length(2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for (const splitBy of ['word'] as const) {
|
|
69
|
+
it(`should emit xml blocks (splitBy: ${splitBy})`, async ({ expect }) => {
|
|
70
|
+
const stream = createGenerationStream(new Response(createTestSSEStream(TEST_BLOCKS, { splitBy })));
|
|
71
|
+
const parser = new MixedStreamParser();
|
|
72
|
+
|
|
73
|
+
let message: Message | undefined;
|
|
74
|
+
parser.message.on((_message) => {
|
|
75
|
+
message = _message;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await parser.parse(stream);
|
|
79
|
+
|
|
80
|
+
for (const block of message?.content ?? []) {
|
|
81
|
+
log.info('block', { block });
|
|
82
|
+
}
|
|
83
|
+
expect(message?.content.map((block) => block.type)).to.deep.eq([
|
|
84
|
+
//
|
|
85
|
+
'text',
|
|
86
|
+
'text',
|
|
87
|
+
'text',
|
|
88
|
+
'json',
|
|
89
|
+
'text',
|
|
90
|
+
'json',
|
|
91
|
+
'text',
|
|
92
|
+
'json',
|
|
93
|
+
'text',
|
|
94
|
+
'json',
|
|
95
|
+
'text',
|
|
96
|
+
'json',
|
|
97
|
+
'json',
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|