@dxos/assistant 0.8.0 → 0.8.1-main.a06ad20

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.
Files changed (65) hide show
  1. package/dist/lib/browser/index.mjs +976 -536
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +2 -2
  5. package/dist/lib/browser/testing/index.mjs.map +1 -1
  6. package/dist/lib/node/index.cjs +1172 -735
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +22 -22
  10. package/dist/lib/node/testing/index.cjs.map +2 -2
  11. package/dist/lib/node-esm/index.mjs +976 -536
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +2 -2
  15. package/dist/lib/node-esm/testing/index.mjs.map +1 -1
  16. package/dist/types/src/ai-service/edge-client.d.ts +19 -0
  17. package/dist/types/src/ai-service/edge-client.d.ts.map +1 -0
  18. package/dist/types/src/ai-service/index.d.ts +4 -1
  19. package/dist/types/src/ai-service/index.d.ts.map +1 -1
  20. package/dist/types/src/ai-service/message-collector.d.ts +88 -0
  21. package/dist/types/src/ai-service/message-collector.d.ts.map +1 -0
  22. package/dist/types/src/ai-service/ollama-client.d.ts +43 -0
  23. package/dist/types/src/ai-service/ollama-client.d.ts.map +1 -0
  24. package/dist/types/src/ai-service/parser.d.ts +3 -1
  25. package/dist/types/src/ai-service/parser.d.ts.map +1 -1
  26. package/dist/types/src/ai-service/service.d.ts +9 -0
  27. package/dist/types/src/ai-service/service.d.ts.map +1 -0
  28. package/dist/types/src/ai-service/stream.d.ts +2 -1
  29. package/dist/types/src/ai-service/stream.d.ts.map +1 -1
  30. package/dist/types/src/ai-service/testing/mock.d.ts +2 -2
  31. package/dist/types/src/ai-service/testing/mock.d.ts.map +1 -1
  32. package/dist/types/src/ai-service/types.d.ts +3 -3
  33. package/dist/types/src/ai-service/types.d.ts.map +1 -1
  34. package/dist/types/src/conversation/conversation.d.ts +3 -1
  35. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  36. package/dist/types/src/conversation/types.d.ts +1 -1
  37. package/dist/types/src/conversation/types.d.ts.map +1 -1
  38. package/dist/types/src/testing/query-promts.d.ts +1 -1
  39. package/dist/types/src/testing/query-promts.d.ts.map +1 -1
  40. package/dist/types/src/testing/test-data.d.ts +1 -1
  41. package/dist/types/src/testing/test-data.d.ts.map +1 -1
  42. package/dist/types/src/testing/test-schema.d.ts +1 -1
  43. package/dist/types/src/testing/test-schema.d.ts.map +1 -1
  44. package/package.json +17 -14
  45. package/src/ai-service/client.test.ts +94 -7
  46. package/src/ai-service/{client.ts → edge-client.ts} +15 -24
  47. package/src/ai-service/index.ts +4 -1
  48. package/src/ai-service/message-collector.ts +84 -0
  49. package/src/ai-service/ollama-client.ts +473 -0
  50. package/src/ai-service/parser.ts +7 -2
  51. package/src/ai-service/service.ts +15 -0
  52. package/src/ai-service/stream.ts +5 -3
  53. package/src/ai-service/testing/mock.ts +7 -2
  54. package/src/ai-service/types.ts +1 -1
  55. package/src/bin/chat.ts +2 -2
  56. package/src/conversation/conversation.test.ts +3 -3
  57. package/src/conversation/conversation.ts +3 -2
  58. package/src/conversation/types.ts +1 -1
  59. package/src/testing/artifacts.test.ts +2 -2
  60. package/src/testing/query-promts.ts +1 -1
  61. package/src/testing/query.test.ts +2 -2
  62. package/src/testing/test-data.ts +1 -1
  63. package/src/testing/test-schema.ts +1 -1
  64. package/dist/types/src/ai-service/client.d.ts +0 -25
  65. package/dist/types/src/ai-service/client.d.ts.map +0 -1
@@ -10,8 +10,25 @@ import {
10
10
  parseCypherQuery
11
11
  } from "./chunk-GOVFOBIB.mjs";
12
12
 
13
- // packages/experimental/assistant/src/ai-service/client.ts
14
- import { Schema as S } from "@effect/schema";
13
+ // packages/experimental/assistant/src/ai-service/defs.ts
14
+ var DEFAULT_LLM_MODEL = "@anthropic/claude-3-5-sonnet-20241022";
15
+ var DEFAULT_LLM_MODELS = [
16
+ // AI Gateway.
17
+ // https://developers.cloudflare.com/ai-gateway
18
+ "@anthropic/claude-3-5-haiku-20241022",
19
+ "@anthropic/claude-3-5-sonnet-20241022",
20
+ "@anthropic/claude-3-7-sonnet-20250219",
21
+ // Workers AI.
22
+ // https://developers.cloudflare.com/workers-ai/models
23
+ "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
24
+ "@hf/nousresearch/hermes-2-pro-mistral-7b",
25
+ "@ollama/llama-3-1-nemotron-70b-instruct",
26
+ "@ollama/llama-3-1-nemotron-mini-4b-instruct",
27
+ "@ollama/llama-3-2-3b"
28
+ ];
29
+
30
+ // packages/experimental/assistant/src/ai-service/edge-client.ts
31
+ import { Schema as S } from "effect";
15
32
  import { Message } from "@dxos/artifact";
16
33
  import { invariant } from "@dxos/invariant";
17
34
  import { log as log2 } from "@dxos/log";
@@ -271,7 +288,7 @@ var createGenerationStream = (response, controller = new AbortController()) => {
271
288
  raw: sse.raw
272
289
  }, {
273
290
  F: __dxlog_file,
274
- L: 21,
291
+ L: 22,
275
292
  S: this,
276
293
  C: (f, a) => f(...a)
277
294
  });
@@ -287,7 +304,7 @@ var createGenerationStream = (response, controller = new AbortController()) => {
287
304
  raw: sse.raw
288
305
  }, {
289
306
  F: __dxlog_file,
290
- L: 37,
307
+ L: 38,
291
308
  S: this,
292
309
  C: (f, a) => f(...a)
293
310
  });
@@ -302,9 +319,9 @@ var createGenerationStream = (response, controller = new AbortController()) => {
302
319
  }
303
320
  }
304
321
  };
305
- return new GenerationStream(controller, iterator);
322
+ return new GenerationStreamImpl(controller, iterator);
306
323
  };
307
- var GenerationStream = class {
324
+ var GenerationStreamImpl = class {
308
325
  constructor(_controller, _getIterator) {
309
326
  this._controller = _controller;
310
327
  this._getIterator = _getIterator;
@@ -343,6 +360,7 @@ var GenerationStream = class {
343
360
  this._done.wake();
344
361
  } catch (err) {
345
362
  this._done.throw(err);
363
+ throw err;
346
364
  }
347
365
  };
348
366
  return generator.call(this);
@@ -350,13 +368,13 @@ var GenerationStream = class {
350
368
  }
351
369
  };
352
370
 
353
- // packages/experimental/assistant/src/ai-service/client.ts
354
- var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/ai-service/client.ts";
355
- var AIServiceClientImpl = class {
371
+ // packages/experimental/assistant/src/ai-service/edge-client.ts
372
+ var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/ai-service/edge-client.ts";
373
+ var AIServiceEdgeClient = class {
356
374
  constructor({ endpoint }) {
357
375
  invariant(endpoint, "endpoint is required", {
358
376
  F: __dxlog_file2,
359
- L: 34,
377
+ L: 23,
360
378
  S: this,
361
379
  A: [
362
380
  "endpoint",
@@ -365,6 +383,10 @@ var AIServiceClientImpl = class {
365
383
  });
366
384
  this._endpoint = endpoint;
367
385
  }
386
+ /**
387
+ * @deprecated
388
+ */
389
+ // TODO(burdon): Remove.
368
390
  async appendMessages(messages) {
369
391
  const url = `${this._endpoint}/message`;
370
392
  const res = await fetch(url, {
@@ -384,15 +406,11 @@ var AIServiceClientImpl = class {
384
406
  * Process request and open message stream.
385
407
  */
386
408
  async exec(request) {
387
- request = {
388
- tools: request.tools ?? [],
389
- ...request
390
- };
391
409
  log2.info("requesting", {
392
410
  endpoint: this._endpoint
393
411
  }, {
394
412
  F: __dxlog_file2,
395
- L: 63,
413
+ L: 50,
396
414
  S: this,
397
415
  C: (f, a) => f(...a)
398
416
  });
@@ -403,11 +421,15 @@ var AIServiceClientImpl = class {
403
421
  headers: {
404
422
  "Content-Type": "application/json"
405
423
  },
406
- body: JSON.stringify(request)
424
+ body: JSON.stringify({
425
+ // TODO(dmaretskyi): Errors if tools are not provided.
426
+ tools: request.tools ?? [],
427
+ ...request
428
+ })
407
429
  });
408
430
  invariant(response.body instanceof ReadableStream, void 0, {
409
431
  F: __dxlog_file2,
410
- L: 72,
432
+ L: 63,
411
433
  S: this,
412
434
  A: [
413
435
  "response.body instanceof ReadableStream",
@@ -418,495 +440,81 @@ var AIServiceClientImpl = class {
418
440
  }
419
441
  };
420
442
 
421
- // packages/experimental/assistant/src/ai-service/defs.ts
422
- var DEFAULT_LLM_MODEL = "@anthropic/claude-3-5-sonnet-20241022";
423
- var DEFAULT_LLM_MODELS = [
424
- // AI Gateway.
425
- // https://developers.cloudflare.com/ai-gateway
426
- "@anthropic/claude-3-5-haiku-20241022",
427
- "@anthropic/claude-3-5-sonnet-20241022",
428
- "@anthropic/claude-3-7-sonnet-20250219",
429
- // Workers AI.
430
- // https://developers.cloudflare.com/workers-ai/models
431
- "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
432
- "@hf/nousresearch/hermes-2-pro-mistral-7b",
433
- "@ollama/llama-3-1-nemotron-70b-instruct",
434
- "@ollama/llama-3-1-nemotron-mini-4b-instruct",
435
- "@ollama/llama-3-2-3b"
436
- ];
437
-
438
- // packages/experimental/assistant/src/ai-service/parser.ts
439
- import { Message as Message2 } from "@dxos/artifact";
440
- import { Event } from "@dxos/async";
441
- import { createStatic } from "@dxos/echo-schema";
442
- import { invariant as invariant2 } from "@dxos/invariant";
443
- import { log as log3 } from "@dxos/log";
444
- import { isNotFalsy, safeParseJson } from "@dxos/util";
445
-
446
- // packages/experimental/assistant/src/ai-service/transform.ts
447
- import * as P from "parsimmon";
448
- var textChunk = P.regexp(/[^<]+/).map((content) => ({
449
- type: "text",
450
- content
451
- }));
452
- var attribute = P.seq(P.regexp(/[a-zA-Z_][a-zA-Z0-9_-]*/), P.string("=").trim(P.optWhitespace), P.alt(P.regexp(/"[^"]*"/).map((s) => s.slice(1, -1)), P.regexp(/'[^']*'/).map((s) => s.slice(1, -1)))).map(([name, _, value]) => [
453
- name,
454
- value
455
- ]);
456
- var tagName = P.regexp(/[a-zA-Z_][a-zA-Z0-9_-]*/);
457
- var selfClosingTag = P.seqMap(
458
- //
459
- P.string("<"),
460
- tagName,
461
- P.optWhitespace.then(attribute).many(),
462
- P.optWhitespace.then(P.string("/>")),
463
- P.optWhitespace,
464
- (_, tag, attributes, __) => ({
465
- type: "tag",
466
- tag,
467
- attributes: Object.fromEntries(attributes),
468
- content: [],
469
- selfClosing: true
470
- })
471
- );
472
- var openTag = P.seqMap(
473
- //
474
- P.string("<"),
475
- tagName,
476
- P.optWhitespace.then(attribute).many(),
477
- P.string(">"),
478
- P.regexp(/[^<]*/),
479
- (_, tag, attributes, __, content) => ({
480
- type: "tag",
481
- tag,
482
- attributes: Object.fromEntries(attributes),
483
- content: content.length ? [
484
- {
485
- type: "text",
486
- content
487
- }
488
- ] : []
489
- })
490
- );
491
- var closeTag = P.seqMap(
492
- //
493
- P.string("</"),
494
- tagName,
495
- P.string(">"),
496
- P.optWhitespace,
497
- (_, tag) => ({
498
- type: "tag",
499
- tag,
500
- content: [],
501
- closing: true
502
- })
503
- );
504
- var mixedChunk = P.alt(selfClosingTag, openTag, closeTag, textChunk);
505
- var StreamTransform = class {
506
- constructor() {
507
- this._buffer = "";
508
- }
509
- transform(chunk) {
510
- this._buffer += chunk;
511
- const results = [];
512
- const parser = mixedChunk.many().skip(P.optWhitespace);
513
- const result = parser.parse(this._buffer);
514
- if (result.status) {
515
- for (const chunk2 of result.value) {
516
- results.push(chunk2);
517
- }
518
- this._buffer = "";
519
- }
520
- return results;
521
- }
522
- // TODO(burdon): Process remaining buffer?
523
- flush() {
524
- const remaining = this._buffer;
525
- this._buffer = "";
526
- return remaining.length > 0 ? [
527
- {
528
- type: "text",
529
- content: remaining
530
- }
531
- ] : [];
532
- }
533
- };
534
-
535
- // packages/experimental/assistant/src/ai-service/parser.ts
536
- var __dxlog_file3 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/ai-service/parser.ts";
537
- var MixedStreamParser = class {
443
+ // packages/experimental/assistant/src/ai-service/message-collector.ts
444
+ var MessageCollector = class {
538
445
  constructor() {
539
- /**
540
- * New message.
541
- */
542
- this.message = new Event();
543
- /**
544
- * Complete block added to Message.
545
- */
546
- this.block = new Event();
547
- /**
548
- * Update partial block (while streaming).
549
- */
550
- this.update = new Event();
446
+ this._messages = [];
447
+ this._pendingMessage = void 0;
551
448
  }
552
- _emitBlock(contentBlock, streamBlock) {
553
- const messageBlock = streamBlock ? mergeMessageBlock(contentBlock, streamBlock) : contentBlock;
554
- if (messageBlock) {
555
- if (messageBlock.type === "text" && messageBlock.text.length === 0) {
556
- return;
557
- }
558
- invariant2(this._message, void 0, {
559
- F: __dxlog_file3,
560
- L: 46,
561
- S: this,
562
- A: [
563
- "this._message",
564
- ""
565
- ]
566
- });
567
- this._message.content.push(messageBlock);
568
- this.block.emit(messageBlock);
569
- }
449
+ get messages() {
450
+ return this._messages;
570
451
  }
571
- _emitUpdate(contentBlock, streamBlock) {
572
- const messageBlock = mergeMessageBlock(contentBlock, streamBlock);
573
- if (messageBlock) {
574
- messageBlock.pending = true;
575
- this.update.emit(messageBlock);
576
- }
452
+ get pendingMessage() {
453
+ return this._pendingMessage;
577
454
  }
578
- /**
579
- * Parse stream until end.
580
- */
581
- async parse(stream) {
582
- const transformer = new StreamTransform();
583
- let contentBlock;
584
- let streamBlock;
585
- const stack = [];
586
- const messagesCollected = [];
587
- for await (const event of stream) {
588
- switch (event.type) {
589
- //
590
- // Messages.
591
- //
592
- case "message_start": {
593
- if (this._message) {
594
- log3.warn("unexpected message_start", void 0, {
595
- F: __dxlog_file3,
596
- L: 88,
597
- S: this,
598
- C: (f, a) => f(...a)
599
- });
600
- }
601
- this._message = createStatic(Message2, {
602
- role: "assistant",
603
- content: []
604
- });
605
- this.message.emit(this._message);
606
- break;
455
+ push(event) {
456
+ switch (event.type) {
457
+ case "message_start":
458
+ this._pendingMessage = event.message;
459
+ break;
460
+ case "message_stop":
461
+ if (this._pendingMessage) {
462
+ this._messages.push(this._pendingMessage);
463
+ this._pendingMessage = void 0;
607
464
  }
608
- case "message_delta": {
609
- if (!this._message) {
610
- log3.warn("unexpected message_delta", void 0, {
611
- F: __dxlog_file3,
612
- L: 98,
613
- S: this,
614
- C: (f, a) => f(...a)
615
- });
616
- }
617
- break;
465
+ break;
466
+ case "content_block_start":
467
+ if (this._pendingMessage) {
468
+ this._pendingMessage.content = this._pendingMessage.content || [];
469
+ this._pendingMessage.content[event.index] = event.content;
618
470
  }
619
- case "message_stop": {
620
- if (!this._message) {
621
- log3.warn("unexpected message_stop", void 0, {
622
- F: __dxlog_file3,
623
- L: 106,
624
- S: this,
625
- C: (f, a) => f(...a)
626
- });
627
- } else {
628
- messagesCollected.push(this._message);
629
- this._message = void 0;
471
+ break;
472
+ case "content_block_delta":
473
+ if (this._pendingMessage && this._pendingMessage.content) {
474
+ const block = this._pendingMessage.content[event.index];
475
+ if (block && event.delta.type === "text_delta" && block.type === "text") {
476
+ block.text = (block.text || "") + event.delta.text;
630
477
  }
631
- break;
632
478
  }
633
- //
634
- // Content blocks.
635
- //
636
- case "content_block_start": {
637
- if (contentBlock) {
638
- log3.warn("unexpected content_block_start", {
639
- content: contentBlock
640
- }, {
641
- F: __dxlog_file3,
642
- L: 120,
643
- S: this,
644
- C: (f, a) => f(...a)
645
- });
479
+ break;
480
+ case "content_block_stop":
481
+ if (this._pendingMessage && this._pendingMessage.content) {
482
+ const block = this._pendingMessage.content[event.index];
483
+ if (block) {
484
+ block.pending = false;
646
485
  }
647
- contentBlock = event.content;
648
- break;
649
486
  }
650
- case "content_block_delta": {
651
- if (!contentBlock) {
652
- log3.warn("unexpected content_block_delta", void 0, {
653
- F: __dxlog_file3,
654
- L: 130,
655
- S: this,
656
- C: (f, a) => f(...a)
657
- });
658
- break;
659
- }
660
- switch (event.delta.type) {
661
- //
662
- // Text
663
- //
664
- case "text_delta": {
665
- const chunks = transformer.transform(event.delta.text);
666
- for (const chunk of chunks) {
667
- switch (streamBlock?.type) {
668
- //
669
- // XML Fragment.
670
- //
671
- case "tag": {
672
- if (chunk.type === "tag") {
673
- if (chunk.selfClosing) {
674
- streamBlock.content.push(chunk);
675
- } else if (chunk.closing) {
676
- if (stack.length > 0) {
677
- const top = stack.pop();
678
- invariant2(top && top.type === "tag", void 0, {
679
- F: __dxlog_file3,
680
- L: 154,
681
- S: this,
682
- A: [
683
- "top && top.type === 'tag'",
684
- ""
685
- ]
686
- });
687
- top.content.push(streamBlock);
688
- streamBlock = top;
689
- } else {
690
- this._emitBlock(contentBlock, streamBlock);
691
- streamBlock = void 0;
692
- }
693
- } else {
694
- stack.push(streamBlock);
695
- streamBlock = chunk;
696
- }
697
- } else {
698
- if (streamBlock.content.length === 0) {
699
- streamBlock.content.push(chunk);
700
- } else {
701
- const last = streamBlock.content.at(-1);
702
- invariant2(last, void 0, {
703
- F: __dxlog_file3,
704
- L: 171,
705
- S: this,
706
- A: [
707
- "last",
708
- ""
709
- ]
710
- });
711
- if (last.type === "text") {
712
- last.content += chunk.content;
713
- } else {
714
- streamBlock.content.push(chunk);
715
- }
716
- }
717
- }
718
- break;
719
- }
720
- //
721
- // Text Fragment.
722
- //
723
- case "text": {
724
- if (chunk.type === "tag") {
725
- this._emitBlock(contentBlock, streamBlock);
726
- if (chunk.selfClosing) {
727
- this._emitBlock(contentBlock, chunk);
728
- streamBlock = void 0;
729
- } else {
730
- streamBlock = chunk;
731
- }
732
- } else {
733
- streamBlock.content += chunk.content;
734
- }
735
- break;
736
- }
737
- //
738
- // No current chunk.
739
- //
740
- default: {
741
- if (chunk.type === "tag" && chunk.selfClosing) {
742
- this._emitBlock(contentBlock, chunk);
743
- } else {
744
- streamBlock = chunk;
745
- }
746
- }
747
- }
748
- }
749
- break;
750
- }
751
- //
752
- // JSON
753
- //
754
- case "input_json_delta": {
755
- if (!contentBlock) {
756
- log3.warn("unexpected input_json_delta", {
757
- contentBlock
758
- }, {
759
- F: __dxlog_file3,
760
- L: 221,
761
- S: this,
762
- C: (f, a) => f(...a)
763
- });
764
- break;
765
- }
766
- if (streamBlock?.type === "json") {
767
- streamBlock.content += event.delta.partial_json;
768
- } else {
769
- streamBlock = {
770
- type: "json",
771
- content: event.delta.partial_json
772
- };
773
- }
774
- break;
775
- }
776
- }
777
- break;
778
- }
779
- case "content_block_stop": {
780
- if (!contentBlock) {
781
- log3.warn("unexpected content_block_stop", void 0, {
782
- F: __dxlog_file3,
783
- L: 238,
784
- S: this,
785
- C: (f, a) => f(...a)
786
- });
787
- break;
788
- }
789
- this._emitBlock(contentBlock, streamBlock);
790
- contentBlock = void 0;
791
- streamBlock = void 0;
792
- break;
487
+ break;
488
+ case "message_delta":
489
+ if (this._pendingMessage && event.delta) {
490
+ Object.assign(this._pendingMessage, event.delta);
793
491
  }
794
- }
795
- if (contentBlock && streamBlock) {
796
- this._emitUpdate(contentBlock, streamBlock);
797
- }
492
+ break;
798
493
  }
799
- return messagesCollected;
800
494
  }
801
- };
802
- var mergeMessageBlock = (contentBlock, streamBlock) => {
803
- log3("mergeMessageBlock", {
804
- contentBlock,
805
- streamBlock
806
- }, {
807
- F: __dxlog_file3,
808
- L: 265,
809
- S: void 0,
810
- C: (f, a) => f(...a)
811
- });
812
- switch (streamBlock.type) {
813
- //
814
- // Text
815
- //
816
- case "text": {
817
- return {
818
- ...contentBlock,
819
- type: "text",
820
- text: streamBlock.content
821
- };
822
- }
823
- //
824
- // XML
825
- //
826
- case "tag": {
827
- switch (streamBlock.tag) {
828
- case "cot": {
829
- const content = streamBlock.content.map((block) => {
830
- switch (block.type) {
831
- case "text":
832
- return block.content;
833
- default:
834
- return null;
835
- }
836
- }).filter(isNotFalsy).join("\n");
837
- return {
838
- ...contentBlock,
839
- type: "text",
840
- disposition: "cot",
841
- text: content
842
- };
843
- }
844
- case "artifact": {
845
- const { attributes } = streamBlock;
846
- return {
847
- type: "json",
848
- disposition: "artifact",
849
- json: JSON.stringify(attributes)
850
- };
851
- }
852
- case "suggest": {
853
- if (streamBlock.content.length === 1 && streamBlock.content[0].type === "text") {
854
- return {
855
- type: "json",
856
- disposition: "suggest",
857
- json: JSON.stringify({
858
- text: streamBlock.content[0].content
859
- })
860
- };
861
- }
862
- break;
863
- }
864
- case "select": {
865
- return {
866
- type: "json",
867
- disposition: "select",
868
- json: JSON.stringify({
869
- options: streamBlock.content.flatMap((content) => content.type === "tag" && content.content.length === 1 && content.content[0].type === "text" ? [
870
- content.content[0].content
871
- ] : [])
872
- })
873
- };
874
- }
875
- case "tool-list": {
876
- return {
877
- type: "json",
878
- disposition: "tool_list",
879
- json: JSON.stringify({})
880
- };
881
- }
882
- }
883
- break;
884
- }
885
- //
886
- // JSON
887
- //
888
- case "json": {
889
- switch (contentBlock.type) {
890
- case "tool_use": {
891
- return {
892
- ...contentBlock,
893
- input: safeParseJson(streamBlock.content) ?? {}
894
- };
895
- }
896
- }
897
- return {
898
- ...contentBlock,
899
- type: "json",
900
- disposition: "artifact",
901
- json: streamBlock.content
902
- };
903
- }
495
+ pushMessage(message) {
496
+ this._messages.push(message);
904
497
  }
905
498
  };
499
+ function* emitMessageAsEvents(message) {
500
+ yield {
501
+ type: "message_start",
502
+ message
503
+ };
504
+ yield {
505
+ type: "message_stop"
506
+ };
507
+ }
508
+
509
+ // packages/experimental/assistant/src/ai-service/ollama-client.ts
510
+ import { defineTool, ToolResult as ToolResult2 } from "@dxos/artifact";
511
+ import { ObjectId as ObjectId3, S as S3 } from "@dxos/echo-schema";
512
+ import { invariant as invariant3 } from "@dxos/invariant";
513
+ import { log as log4 } from "@dxos/log";
906
514
 
907
515
  // packages/experimental/assistant/src/ai-service/types.ts
908
- import { Schema as S2 } from "@effect/schema";
909
- import { Tool, Message as Message3, SpaceIdSchema } from "@dxos/artifact";
516
+ import { Schema as S2 } from "effect";
517
+ import { Tool, Message as Message2, SpaceIdSchema } from "@dxos/artifact";
910
518
  import { ObjectId } from "@dxos/echo-schema";
911
519
  var createArtifactElement = (id) => `<artifact id=${id} />`;
912
520
  var LLMModel = S2.Literal(...DEFAULT_LLM_MODELS);
@@ -934,27 +542,27 @@ var GenerateRequest = S2.Struct({
934
542
  * History of messages to include in the context window.
935
543
  */
936
544
  // TODO(burdon): Rename messages.
937
- history: S2.optional(S2.Array(Message3)),
545
+ history: S2.optional(S2.Array(Message2)),
938
546
  /**
939
547
  * Current request.
940
548
  */
941
- prompt: S2.optional(Message3)
549
+ prompt: S2.optional(Message2)
942
550
  });
943
551
 
944
552
  // packages/experimental/assistant/src/conversation/conversation.ts
945
- import { ToolResult, Message as Message4 } from "@dxos/artifact";
946
- import { createStatic as createStatic2, ObjectId as ObjectId2 } from "@dxos/echo-schema";
947
- import { invariant as invariant3 } from "@dxos/invariant";
948
- import { log as log4 } from "@dxos/log";
949
- var __dxlog_file4 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/conversation/conversation.ts";
553
+ import { ToolResult, Message as Message3 } from "@dxos/artifact";
554
+ import { createStatic, ObjectId as ObjectId2 } from "@dxos/echo-schema";
555
+ import { invariant as invariant2 } from "@dxos/invariant";
556
+ import { log as log3 } from "@dxos/log";
557
+ var __dxlog_file3 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/conversation/conversation.ts";
950
558
  var runLLM = async (params) => {
951
559
  let conversationResult = null;
952
560
  const history = params.history ?? [];
953
561
  const generate = async () => {
954
- log4("llm generate", {
562
+ log3("llm generate", {
955
563
  tools: params.tools
956
564
  }, {
957
- F: __dxlog_file4,
565
+ F: __dxlog_file3,
958
566
  L: 44,
959
567
  S: void 0,
960
568
  C: (f, a) => f(...a)
@@ -974,8 +582,8 @@ var runLLM = async (params) => {
974
582
  await stream.complete();
975
583
  const messages = [];
976
584
  const message = messages.at(-1);
977
- invariant3(message, void 0, {
978
- F: __dxlog_file4,
585
+ invariant2(message, void 0, {
586
+ F: __dxlog_file3,
979
587
  L: 63,
980
588
  S: void 0,
981
589
  A: [
@@ -983,17 +591,17 @@ var runLLM = async (params) => {
983
591
  ""
984
592
  ]
985
593
  });
986
- log4("llm result", {
594
+ log3("llm result", {
987
595
  time: Date.now() - beginTs,
988
596
  message
989
597
  }, {
990
- F: __dxlog_file4,
598
+ F: __dxlog_file3,
991
599
  L: 65,
992
600
  S: void 0,
993
601
  C: (f, a) => f(...a)
994
602
  });
995
- invariant3(message, void 0, {
996
- F: __dxlog_file4,
603
+ invariant2(message, void 0, {
604
+ F: __dxlog_file3,
997
605
  L: 66,
998
606
  S: void 0,
999
607
  A: [
@@ -1012,8 +620,8 @@ var runLLM = async (params) => {
1012
620
  const isToolUse2 = message.content.at(-1)?.type === "tool_use";
1013
621
  if (isToolUse2) {
1014
622
  const toolCalls = message.content.filter((block) => block.type === "tool_use");
1015
- invariant3(toolCalls.length === 1, void 0, {
1016
- F: __dxlog_file4,
623
+ invariant2(toolCalls.length === 1, void 0, {
624
+ F: __dxlog_file3,
1017
625
  L: 77,
1018
626
  S: void 0,
1019
627
  A: [
@@ -1026,8 +634,8 @@ var runLLM = async (params) => {
1026
634
  if (!tool) {
1027
635
  throw new Error(`Tool not found: ${toolCall.name}`);
1028
636
  }
1029
- invariant3(tool.execute, void 0, {
1030
- F: __dxlog_file4,
637
+ invariant2(tool.execute, void 0, {
638
+ F: __dxlog_file3,
1031
639
  L: 84,
1032
640
  S: void 0,
1033
641
  A: [
@@ -1038,10 +646,10 @@ var runLLM = async (params) => {
1038
646
  const toolResult = await tool.execute(toolCall.input);
1039
647
  switch (toolResult.kind) {
1040
648
  case "error": {
1041
- log4.warn("tool error", {
649
+ log3.warn("tool error", {
1042
650
  message: toolResult.message
1043
651
  }, {
1044
- F: __dxlog_file4,
652
+ F: __dxlog_file3,
1045
653
  L: 88,
1046
654
  S: void 0,
1047
655
  C: (f, a) => f(...a)
@@ -1068,10 +676,10 @@ var runLLM = async (params) => {
1068
676
  return true;
1069
677
  }
1070
678
  case "success": {
1071
- log4("tool success", {
679
+ log3("tool success", {
1072
680
  result: toolResult.result
1073
681
  }, {
1074
- F: __dxlog_file4,
682
+ F: __dxlog_file3,
1075
683
  L: 110,
1076
684
  S: void 0,
1077
685
  C: (f, a) => f(...a)
@@ -1097,10 +705,10 @@ var runLLM = async (params) => {
1097
705
  return true;
1098
706
  }
1099
707
  case "break": {
1100
- log4("tool break", {
708
+ log3("tool break", {
1101
709
  result: toolResult.result
1102
710
  }, {
1103
- F: __dxlog_file4,
711
+ F: __dxlog_file3,
1104
712
  L: 131,
1105
713
  S: void 0,
1106
714
  C: (f, a) => f(...a)
@@ -1119,10 +727,10 @@ var runLLM = async (params) => {
1119
727
  history
1120
728
  };
1121
729
  };
1122
- var isToolUse = (message) => {
730
+ var isToolUse = (message, { onlyToolNames } = {}) => {
1123
731
  const block = message.content.at(-1);
1124
- invariant3(block, void 0, {
1125
- F: __dxlog_file4,
732
+ invariant2(block, void 0, {
733
+ F: __dxlog_file3,
1126
734
  L: 152,
1127
735
  S: void 0,
1128
736
  A: [
@@ -1130,12 +738,12 @@ var isToolUse = (message) => {
1130
738
  ""
1131
739
  ]
1132
740
  });
1133
- return block.type === "tool_use";
741
+ return block.type === "tool_use" && (!onlyToolNames || onlyToolNames.includes(block.name));
1134
742
  };
1135
743
  var runTools = async ({ message, tools, extensions }) => {
1136
744
  const toolCalls = message.content.filter((block) => block.type === "tool_use");
1137
- invariant3(toolCalls.length === 1, void 0, {
1138
- F: __dxlog_file4,
745
+ invariant2(toolCalls.length === 1, void 0, {
746
+ F: __dxlog_file3,
1139
747
  L: 174,
1140
748
  S: void 0,
1141
749
  A: [
@@ -1150,8 +758,8 @@ var runTools = async ({ message, tools, extensions }) => {
1150
758
  }
1151
759
  let toolResult;
1152
760
  try {
1153
- invariant3(tool.execute, void 0, {
1154
- F: __dxlog_file4,
761
+ invariant2(tool.execute, void 0, {
762
+ F: __dxlog_file3,
1155
763
  L: 183,
1156
764
  S: void 0,
1157
765
  A: [
@@ -1163,10 +771,10 @@ var runTools = async ({ message, tools, extensions }) => {
1163
771
  extensions
1164
772
  });
1165
773
  } catch (error) {
1166
- log4("tool error", {
774
+ log3("tool error", {
1167
775
  error
1168
776
  }, {
1169
- F: __dxlog_file4,
777
+ F: __dxlog_file3,
1170
778
  L: 186,
1171
779
  S: void 0,
1172
780
  C: (f, a) => f(...a)
@@ -1175,15 +783,15 @@ var runTools = async ({ message, tools, extensions }) => {
1175
783
  }
1176
784
  switch (toolResult.kind) {
1177
785
  case "error": {
1178
- log4("tool error", {
786
+ log3("tool error", {
1179
787
  message: toolResult.message
1180
788
  }, {
1181
- F: __dxlog_file4,
789
+ F: __dxlog_file3,
1182
790
  L: 192,
1183
791
  S: void 0,
1184
792
  C: (f, a) => f(...a)
1185
793
  });
1186
- const resultMessage = createStatic2(Message4, {
794
+ const resultMessage = createStatic(Message3, {
1187
795
  role: "user",
1188
796
  content: [
1189
797
  {
@@ -1200,22 +808,23 @@ var runTools = async ({ message, tools, extensions }) => {
1200
808
  };
1201
809
  }
1202
810
  case "success": {
1203
- log4("tool success", {
811
+ log3("tool success", {
1204
812
  result: toolResult.result
1205
813
  }, {
1206
- F: __dxlog_file4,
814
+ F: __dxlog_file3,
1207
815
  L: 213,
1208
816
  S: void 0,
1209
817
  C: (f, a) => f(...a)
1210
818
  });
1211
- const resultMessage = createStatic2(Message4, {
819
+ const resultMessage = createStatic(Message3, {
1212
820
  role: "user",
1213
821
  content: [
1214
822
  {
1215
823
  type: "tool_result",
1216
824
  toolUseId: toolCall.id,
1217
825
  content: typeof toolResult.result === "string" ? toolResult.result : JSON.stringify(toolResult.result) ?? ""
1218
- }
826
+ },
827
+ ...toolResult.extractContentBlocks ?? []
1219
828
  ]
1220
829
  });
1221
830
  return {
@@ -1224,11 +833,11 @@ var runTools = async ({ message, tools, extensions }) => {
1224
833
  };
1225
834
  }
1226
835
  case "break": {
1227
- log4("tool break", {
836
+ log3("tool break", {
1228
837
  result: toolResult.result
1229
838
  }, {
1230
- F: __dxlog_file4,
1231
- L: 233,
839
+ F: __dxlog_file3,
840
+ L: 234,
1232
841
  S: void 0,
1233
842
  C: (f, a) => f(...a)
1234
843
  });
@@ -1239,19 +848,850 @@ var runTools = async ({ message, tools, extensions }) => {
1239
848
  }
1240
849
  }
1241
850
  };
851
+
852
+ // packages/experimental/assistant/src/ai-service/ollama-client.ts
853
+ var __dxlog_file4 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/ai-service/ollama-client.ts";
854
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
855
+ var DEFAULT_OLLAMA_MODEL = "llama3.2:1b";
856
+ var OllamaClient = class _OllamaClient {
857
+ /**
858
+ * Create a test client with small local model and no temperature for predictable results.
859
+ */
860
+ static createClient(options) {
861
+ return new _OllamaClient({
862
+ tools: options?.tools,
863
+ overrides: {
864
+ model: DEFAULT_OLLAMA_MODEL,
865
+ temperature: 0
866
+ }
867
+ });
868
+ }
869
+ /**
870
+ * Check if Ollama server is running and accessible.
871
+ * @returns Promise that resolves to true if Ollama is running, false otherwise.
872
+ */
873
+ static async isRunning() {
874
+ try {
875
+ const response = await fetch(`${DEFAULT_OLLAMA_URL}/api/version`, {
876
+ method: "GET",
877
+ headers: {
878
+ "Content-Type": "application/json"
879
+ },
880
+ signal: AbortSignal.timeout(100)
881
+ });
882
+ return response.ok;
883
+ } catch (error) {
884
+ return false;
885
+ }
886
+ }
887
+ constructor({ endpoint, tools, overrides, maxToolInvocations }) {
888
+ this._endpoint = endpoint ?? DEFAULT_OLLAMA_URL;
889
+ this._tools = tools ?? [];
890
+ this._modelOverride = overrides?.model;
891
+ this._temperatureOverride = overrides?.temperature;
892
+ this._maxToolInvocations = maxToolInvocations ?? 3;
893
+ }
894
+ _getEmbededTools(clientTools) {
895
+ return [
896
+ ...this._tools,
897
+ ...clientTools.filter((tool) => tool.type !== void 0 && WELL_KNOWN_TOOLS[tool.type]).map((tool) => WELL_KNOWN_TOOLS[tool.type])
898
+ ];
899
+ }
900
+ async *_generateStream(request, signal) {
901
+ invariant3(request.prompt === void 0, "Prompt must be in history", {
902
+ F: __dxlog_file4,
903
+ L: 99,
904
+ S: this,
905
+ A: [
906
+ "request.prompt === undefined",
907
+ "'Prompt must be in history'"
908
+ ]
909
+ });
910
+ const ollamaRequest = {
911
+ model: this._modelOverride ?? request.model ?? "llama3",
912
+ messages: [],
913
+ stream: true,
914
+ options: {
915
+ temperature: this._temperatureOverride ?? 0.7
916
+ }
917
+ };
918
+ if (request.systemPrompt) {
919
+ ollamaRequest.messages.push({
920
+ role: "system",
921
+ content: request.systemPrompt
922
+ });
923
+ }
924
+ if (request.history?.length) {
925
+ for (const message of request.history) {
926
+ const role = message.role === "assistant" ? "assistant" : "user";
927
+ if (message.content.length === 0) {
928
+ continue;
929
+ }
930
+ for (const content of message.content) {
931
+ switch (content.type) {
932
+ case "text": {
933
+ ollamaRequest.messages.push({
934
+ role,
935
+ content: content.text
936
+ });
937
+ break;
938
+ }
939
+ case "tool_use": {
940
+ ollamaRequest.messages.push({
941
+ role: "assistant",
942
+ content: "",
943
+ tool_calls: [
944
+ {
945
+ function: {
946
+ name: content.name,
947
+ arguments: content.input
948
+ }
949
+ }
950
+ ]
951
+ });
952
+ break;
953
+ }
954
+ case "tool_result": {
955
+ ollamaRequest.messages.push({
956
+ role: "tool",
957
+ content: JSON.stringify({
958
+ type: "tool_result",
959
+ toolUseId: content.toolUseId,
960
+ content: content.content
961
+ })
962
+ });
963
+ break;
964
+ }
965
+ }
966
+ }
967
+ }
968
+ }
969
+ if (request.tools?.length || this._tools.length) {
970
+ const allTools = [
971
+ ...request.tools ?? [],
972
+ ...this._tools
973
+ ];
974
+ const withWellKnownTools = allTools.map((tool) => tool.type !== void 0 && WELL_KNOWN_TOOLS[tool.type] ? WELL_KNOWN_TOOLS[tool.type] : tool);
975
+ if (withWellKnownTools.length > 0) {
976
+ ollamaRequest.tools = withWellKnownTools.map((tool) => ({
977
+ type: "function",
978
+ function: {
979
+ name: tool.name,
980
+ description: tool.description,
981
+ parameters: {
982
+ type: "object",
983
+ properties: tool.parameters?.properties,
984
+ required: tool.parameters?.required
985
+ }
986
+ }
987
+ }));
988
+ }
989
+ }
990
+ log4.info("generate request", {
991
+ history: request.history,
992
+ prompt: request.prompt,
993
+ ollamaRequest
994
+ }, {
995
+ F: __dxlog_file4,
996
+ L: 193,
997
+ S: this,
998
+ C: (f, a) => f(...a)
999
+ });
1000
+ const response = await fetch(`${this._endpoint}/api/chat`, {
1001
+ method: "POST",
1002
+ headers: {
1003
+ "Content-Type": "application/json"
1004
+ },
1005
+ body: JSON.stringify(ollamaRequest),
1006
+ signal
1007
+ });
1008
+ if (!response.ok) {
1009
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
1010
+ }
1011
+ if (!response.body) {
1012
+ throw new Error("Ollama API returned no response body");
1013
+ }
1014
+ const reader = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new OllamaDecoderStream()).getReader();
1015
+ try {
1016
+ const messageId = ObjectId3.random();
1017
+ yield {
1018
+ type: "message_start",
1019
+ message: {
1020
+ id: messageId,
1021
+ role: "assistant",
1022
+ content: []
1023
+ }
1024
+ };
1025
+ const textBlock = {
1026
+ type: "text",
1027
+ text: ""
1028
+ };
1029
+ let currentBlockIndex = 0;
1030
+ yield {
1031
+ type: "content_block_start",
1032
+ index: currentBlockIndex,
1033
+ content: textBlock
1034
+ };
1035
+ while (true) {
1036
+ const { done, value: data } = await reader.read();
1037
+ if (done) {
1038
+ break;
1039
+ }
1040
+ log4.info("ollama data", {
1041
+ data
1042
+ }, {
1043
+ F: __dxlog_file4,
1044
+ L: 256,
1045
+ S: this,
1046
+ C: (f, a) => f(...a)
1047
+ });
1048
+ if (data.message?.content) {
1049
+ yield {
1050
+ type: "content_block_delta",
1051
+ index: 0,
1052
+ delta: {
1053
+ type: "text_delta",
1054
+ text: data.message.content
1055
+ }
1056
+ };
1057
+ }
1058
+ if (data.message?.tool_calls && data.message?.tool_calls?.length > 0) {
1059
+ if (data.message.tool_calls.length > 1) {
1060
+ log4.warn("Ollama returned multiple tool calls. Only the first one will be used.", void 0, {
1061
+ F: __dxlog_file4,
1062
+ L: 273,
1063
+ S: this,
1064
+ C: (f, a) => f(...a)
1065
+ });
1066
+ }
1067
+ yield {
1068
+ type: "content_block_stop",
1069
+ index: currentBlockIndex++
1070
+ };
1071
+ yield {
1072
+ type: "content_block_start",
1073
+ index: currentBlockIndex,
1074
+ content: {
1075
+ type: "tool_use",
1076
+ id: ObjectId3.random(),
1077
+ name: data.message.tool_calls[0].function.name,
1078
+ input: sanitizeToolArguments(data.message.tool_calls[0].function.arguments)
1079
+ }
1080
+ };
1081
+ yield {
1082
+ type: "message_delta",
1083
+ delta: {
1084
+ stopReason: "tool_use"
1085
+ }
1086
+ };
1087
+ }
1088
+ }
1089
+ yield {
1090
+ type: "content_block_stop",
1091
+ index: currentBlockIndex
1092
+ };
1093
+ yield {
1094
+ type: "message_stop"
1095
+ };
1096
+ } finally {
1097
+ reader.releaseLock();
1098
+ }
1099
+ }
1100
+ async exec(request) {
1101
+ const controller = new AbortController();
1102
+ try {
1103
+ return new GenerationStreamImpl(controller, async function* () {
1104
+ const collector = new MessageCollector();
1105
+ let toolInvocations = 0;
1106
+ while (true) {
1107
+ for await (const event of this._generateStream({
1108
+ ...request,
1109
+ history: [
1110
+ ...request.history ?? [],
1111
+ ...request.prompt ? [
1112
+ request.prompt
1113
+ ] : [],
1114
+ ...collector.messages
1115
+ ],
1116
+ prompt: void 0
1117
+ }, controller.signal)) {
1118
+ collector.push(event);
1119
+ yield event;
1120
+ }
1121
+ if (collector.messages.length > 0 && !isToolUse(collector.messages.at(-1), {
1122
+ onlyToolNames: this._getEmbededTools(request.tools ?? []).map((tool) => tool.name)
1123
+ })) {
1124
+ break;
1125
+ }
1126
+ if (++toolInvocations > this._maxToolInvocations) {
1127
+ throw new Error("Maximum number of tool invocations reached");
1128
+ }
1129
+ const result = await runTools({
1130
+ message: collector.messages.at(-1),
1131
+ tools: this._getEmbededTools(request.tools ?? [])
1132
+ });
1133
+ switch (result.type) {
1134
+ case "continue": {
1135
+ collector.pushMessage(result.message);
1136
+ yield* emitMessageAsEvents(result.message);
1137
+ break;
1138
+ }
1139
+ case "break": {
1140
+ throw new Error("Tool use breaks not allowed");
1141
+ }
1142
+ }
1143
+ }
1144
+ }.bind(this));
1145
+ } catch (error) {
1146
+ controller.abort();
1147
+ throw error;
1148
+ }
1149
+ }
1150
+ };
1151
+ var sanitizeToolArguments = (args) => {
1152
+ if (args.type === "object" && typeof args.properties === "object" && Array.isArray(args.required)) {
1153
+ return args.properties;
1154
+ }
1155
+ return args;
1156
+ };
1157
+ var SAMPLE_IMAGE_URL = "https://images.nightcafe.studio/jobs/BNmcRhHCM1JRKoUtqSei/BNmcRhHCM1JRKoUtqSei--1--5b9rv.jpg";
1158
+ var WELL_KNOWN_TOOLS = {
1159
+ [ToolTypes.TextToImage]: defineTool("system", {
1160
+ name: "text-to-image",
1161
+ description: "Generate an image from a text prompt",
1162
+ schema: S3.Struct({
1163
+ prompt: S3.String.annotations({
1164
+ description: "The text prompt describing the image to generate"
1165
+ })
1166
+ }),
1167
+ execute: async ({ prompt }) => {
1168
+ const image = await fetch(SAMPLE_IMAGE_URL).then(async (res) => Buffer.from(await res.arrayBuffer()));
1169
+ const imageBase64 = image.toString("base64");
1170
+ const id = ObjectId3.random();
1171
+ return ToolResult2.Success(`The generation was successful. Image ID: ${id} Prompt: ${prompt}`, [
1172
+ {
1173
+ type: "image",
1174
+ id,
1175
+ source: {
1176
+ type: "base64",
1177
+ mediaType: "image/jpeg",
1178
+ data: imageBase64
1179
+ }
1180
+ }
1181
+ ]);
1182
+ }
1183
+ })
1184
+ };
1185
+ var OllamaDecoderStream = class extends TransformStream {
1186
+ constructor() {
1187
+ super({
1188
+ transform: (chunk, controller) => {
1189
+ this._buffer += chunk;
1190
+ const lines = this._buffer.split("\n");
1191
+ this._buffer = lines.pop() || "";
1192
+ for (const line of lines) {
1193
+ controller.enqueue(JSON.parse(line.trim()));
1194
+ }
1195
+ },
1196
+ flush: (controller) => {
1197
+ this._buffer = this._buffer.trim();
1198
+ if (this._buffer.length > 0) {
1199
+ controller.enqueue(JSON.parse(this._buffer));
1200
+ }
1201
+ }
1202
+ });
1203
+ this._buffer = "";
1204
+ }
1205
+ };
1206
+
1207
+ // packages/experimental/assistant/src/ai-service/parser.ts
1208
+ import { Message as Message4 } from "@dxos/artifact";
1209
+ import { Event } from "@dxos/async";
1210
+ import { createStatic as createStatic2 } from "@dxos/echo-schema";
1211
+ import { invariant as invariant4 } from "@dxos/invariant";
1212
+ import { log as log5 } from "@dxos/log";
1213
+ import { isNotFalsy, safeParseJson } from "@dxos/util";
1214
+
1215
+ // packages/experimental/assistant/src/ai-service/transform.ts
1216
+ import * as P from "parsimmon";
1217
+ var textChunk = P.regexp(/[^<]+/).map((content) => ({
1218
+ type: "text",
1219
+ content
1220
+ }));
1221
+ var attribute = P.seq(P.regexp(/[a-zA-Z_][a-zA-Z0-9_-]*/), P.string("=").trim(P.optWhitespace), P.alt(P.regexp(/"[^"]*"/).map((s) => s.slice(1, -1)), P.regexp(/'[^']*'/).map((s) => s.slice(1, -1)))).map(([name, _, value]) => [
1222
+ name,
1223
+ value
1224
+ ]);
1225
+ var tagName = P.regexp(/[a-zA-Z_][a-zA-Z0-9_-]*/);
1226
+ var selfClosingTag = P.seqMap(
1227
+ //
1228
+ P.string("<"),
1229
+ tagName,
1230
+ P.optWhitespace.then(attribute).many(),
1231
+ P.optWhitespace.then(P.string("/>")),
1232
+ P.optWhitespace,
1233
+ (_, tag, attributes, __) => ({
1234
+ type: "tag",
1235
+ tag,
1236
+ attributes: Object.fromEntries(attributes),
1237
+ content: [],
1238
+ selfClosing: true
1239
+ })
1240
+ );
1241
+ var openTag = P.seqMap(
1242
+ //
1243
+ P.string("<"),
1244
+ tagName,
1245
+ P.optWhitespace.then(attribute).many(),
1246
+ P.string(">"),
1247
+ P.regexp(/[^<]*/),
1248
+ (_, tag, attributes, __, content) => ({
1249
+ type: "tag",
1250
+ tag,
1251
+ attributes: Object.fromEntries(attributes),
1252
+ content: content.length ? [
1253
+ {
1254
+ type: "text",
1255
+ content
1256
+ }
1257
+ ] : []
1258
+ })
1259
+ );
1260
+ var closeTag = P.seqMap(
1261
+ //
1262
+ P.string("</"),
1263
+ tagName,
1264
+ P.string(">"),
1265
+ P.optWhitespace,
1266
+ (_, tag) => ({
1267
+ type: "tag",
1268
+ tag,
1269
+ content: [],
1270
+ closing: true
1271
+ })
1272
+ );
1273
+ var mixedChunk = P.alt(selfClosingTag, openTag, closeTag, textChunk);
1274
+ var StreamTransform = class {
1275
+ constructor() {
1276
+ this._buffer = "";
1277
+ }
1278
+ transform(chunk) {
1279
+ this._buffer += chunk;
1280
+ const results = [];
1281
+ const parser = mixedChunk.many().skip(P.optWhitespace);
1282
+ const result = parser.parse(this._buffer);
1283
+ if (result.status) {
1284
+ for (const chunk2 of result.value) {
1285
+ results.push(chunk2);
1286
+ }
1287
+ this._buffer = "";
1288
+ }
1289
+ return results;
1290
+ }
1291
+ // TODO(burdon): Process remaining buffer?
1292
+ flush() {
1293
+ const remaining = this._buffer;
1294
+ this._buffer = "";
1295
+ return remaining.length > 0 ? [
1296
+ {
1297
+ type: "text",
1298
+ content: remaining
1299
+ }
1300
+ ] : [];
1301
+ }
1302
+ };
1303
+
1304
+ // packages/experimental/assistant/src/ai-service/parser.ts
1305
+ var __dxlog_file5 = "/home/runner/work/dxos/dxos/packages/experimental/assistant/src/ai-service/parser.ts";
1306
+ var MixedStreamParser = class {
1307
+ constructor() {
1308
+ /**
1309
+ * New message.
1310
+ */
1311
+ this.message = new Event();
1312
+ /**
1313
+ * Complete block added to Message.
1314
+ */
1315
+ this.block = new Event();
1316
+ /**
1317
+ * Update partial block (while streaming).
1318
+ */
1319
+ this.update = new Event();
1320
+ this.streamEvent = new Event();
1321
+ }
1322
+ _emitBlock(contentBlock, streamBlock) {
1323
+ const messageBlock = streamBlock ? mergeMessageBlock(contentBlock, streamBlock) : contentBlock;
1324
+ if (messageBlock) {
1325
+ if (messageBlock.type === "text" && messageBlock.text.length === 0) {
1326
+ return;
1327
+ }
1328
+ invariant4(this._message, void 0, {
1329
+ F: __dxlog_file5,
1330
+ L: 49,
1331
+ S: this,
1332
+ A: [
1333
+ "this._message",
1334
+ ""
1335
+ ]
1336
+ });
1337
+ this._message.content.push(messageBlock);
1338
+ this.block.emit(messageBlock);
1339
+ }
1340
+ }
1341
+ _emitUpdate(contentBlock, streamBlock) {
1342
+ const messageBlock = mergeMessageBlock(contentBlock, streamBlock);
1343
+ if (messageBlock) {
1344
+ messageBlock.pending = true;
1345
+ this.update.emit(messageBlock);
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Parse stream until end.
1350
+ */
1351
+ async parse(stream) {
1352
+ const transformer = new StreamTransform();
1353
+ let contentBlock;
1354
+ let streamBlock;
1355
+ const stack = [];
1356
+ const messagesCollected = [];
1357
+ for await (const event of stream) {
1358
+ this.streamEvent.emit(event);
1359
+ switch (event.type) {
1360
+ //
1361
+ // Messages.
1362
+ //
1363
+ case "message_start": {
1364
+ if (this._message) {
1365
+ log5.warn("unexpected message_start", void 0, {
1366
+ F: __dxlog_file5,
1367
+ L: 93,
1368
+ S: this,
1369
+ C: (f, a) => f(...a)
1370
+ });
1371
+ }
1372
+ this._message = createStatic2(Message4, {
1373
+ role: event.message.role,
1374
+ content: [
1375
+ ...event.message.content
1376
+ ]
1377
+ });
1378
+ this.message.emit(this._message);
1379
+ break;
1380
+ }
1381
+ case "message_delta": {
1382
+ if (!this._message) {
1383
+ log5.warn("unexpected message_delta", void 0, {
1384
+ F: __dxlog_file5,
1385
+ L: 103,
1386
+ S: this,
1387
+ C: (f, a) => f(...a)
1388
+ });
1389
+ }
1390
+ break;
1391
+ }
1392
+ case "message_stop": {
1393
+ if (!this._message) {
1394
+ log5.warn("unexpected message_stop", void 0, {
1395
+ F: __dxlog_file5,
1396
+ L: 111,
1397
+ S: this,
1398
+ C: (f, a) => f(...a)
1399
+ });
1400
+ } else {
1401
+ messagesCollected.push(this._message);
1402
+ this._message = void 0;
1403
+ }
1404
+ break;
1405
+ }
1406
+ //
1407
+ // Content blocks.
1408
+ //
1409
+ case "content_block_start": {
1410
+ if (contentBlock) {
1411
+ log5.warn("unexpected content_block_start", {
1412
+ content: contentBlock
1413
+ }, {
1414
+ F: __dxlog_file5,
1415
+ L: 125,
1416
+ S: this,
1417
+ C: (f, a) => f(...a)
1418
+ });
1419
+ }
1420
+ contentBlock = event.content;
1421
+ break;
1422
+ }
1423
+ case "content_block_delta": {
1424
+ if (!contentBlock) {
1425
+ log5.warn("unexpected content_block_delta", void 0, {
1426
+ F: __dxlog_file5,
1427
+ L: 135,
1428
+ S: this,
1429
+ C: (f, a) => f(...a)
1430
+ });
1431
+ break;
1432
+ }
1433
+ switch (event.delta.type) {
1434
+ //
1435
+ // Text
1436
+ //
1437
+ case "text_delta": {
1438
+ const chunks = transformer.transform(event.delta.text);
1439
+ for (const chunk of chunks) {
1440
+ switch (streamBlock?.type) {
1441
+ //
1442
+ // XML Fragment.
1443
+ //
1444
+ case "tag": {
1445
+ if (chunk.type === "tag") {
1446
+ if (chunk.selfClosing) {
1447
+ streamBlock.content.push(chunk);
1448
+ } else if (chunk.closing) {
1449
+ if (stack.length > 0) {
1450
+ const top = stack.pop();
1451
+ invariant4(top && top.type === "tag", void 0, {
1452
+ F: __dxlog_file5,
1453
+ L: 159,
1454
+ S: this,
1455
+ A: [
1456
+ "top && top.type === 'tag'",
1457
+ ""
1458
+ ]
1459
+ });
1460
+ top.content.push(streamBlock);
1461
+ streamBlock = top;
1462
+ } else {
1463
+ this._emitBlock(contentBlock, streamBlock);
1464
+ streamBlock = void 0;
1465
+ }
1466
+ } else {
1467
+ stack.push(streamBlock);
1468
+ streamBlock = chunk;
1469
+ }
1470
+ } else {
1471
+ if (streamBlock.content.length === 0) {
1472
+ streamBlock.content.push(chunk);
1473
+ } else {
1474
+ const last = streamBlock.content.at(-1);
1475
+ invariant4(last, void 0, {
1476
+ F: __dxlog_file5,
1477
+ L: 176,
1478
+ S: this,
1479
+ A: [
1480
+ "last",
1481
+ ""
1482
+ ]
1483
+ });
1484
+ if (last.type === "text") {
1485
+ last.content += chunk.content;
1486
+ } else {
1487
+ streamBlock.content.push(chunk);
1488
+ }
1489
+ }
1490
+ }
1491
+ break;
1492
+ }
1493
+ //
1494
+ // Text Fragment.
1495
+ //
1496
+ case "text": {
1497
+ if (chunk.type === "tag") {
1498
+ this._emitBlock(contentBlock, streamBlock);
1499
+ if (chunk.selfClosing) {
1500
+ this._emitBlock(contentBlock, chunk);
1501
+ streamBlock = void 0;
1502
+ } else {
1503
+ streamBlock = chunk;
1504
+ }
1505
+ } else {
1506
+ streamBlock.content += chunk.content;
1507
+ }
1508
+ break;
1509
+ }
1510
+ //
1511
+ // No current chunk.
1512
+ //
1513
+ default: {
1514
+ if (chunk.type === "tag" && chunk.selfClosing) {
1515
+ this._emitBlock(contentBlock, chunk);
1516
+ } else {
1517
+ streamBlock = chunk;
1518
+ }
1519
+ }
1520
+ }
1521
+ }
1522
+ break;
1523
+ }
1524
+ //
1525
+ // JSON
1526
+ //
1527
+ case "input_json_delta": {
1528
+ if (!contentBlock) {
1529
+ log5.warn("unexpected input_json_delta", {
1530
+ contentBlock
1531
+ }, {
1532
+ F: __dxlog_file5,
1533
+ L: 226,
1534
+ S: this,
1535
+ C: (f, a) => f(...a)
1536
+ });
1537
+ break;
1538
+ }
1539
+ if (streamBlock?.type === "json") {
1540
+ streamBlock.content += event.delta.partial_json;
1541
+ } else {
1542
+ streamBlock = {
1543
+ type: "json",
1544
+ content: event.delta.partial_json
1545
+ };
1546
+ }
1547
+ break;
1548
+ }
1549
+ }
1550
+ break;
1551
+ }
1552
+ case "content_block_stop": {
1553
+ if (!contentBlock) {
1554
+ log5.warn("unexpected content_block_stop", void 0, {
1555
+ F: __dxlog_file5,
1556
+ L: 243,
1557
+ S: this,
1558
+ C: (f, a) => f(...a)
1559
+ });
1560
+ break;
1561
+ }
1562
+ this._emitBlock(contentBlock, streamBlock);
1563
+ contentBlock = void 0;
1564
+ streamBlock = void 0;
1565
+ break;
1566
+ }
1567
+ }
1568
+ if (contentBlock && streamBlock) {
1569
+ this._emitUpdate(contentBlock, streamBlock);
1570
+ }
1571
+ }
1572
+ return messagesCollected;
1573
+ }
1574
+ };
1575
+ var mergeMessageBlock = (contentBlock, streamBlock) => {
1576
+ log5("mergeMessageBlock", {
1577
+ contentBlock,
1578
+ streamBlock
1579
+ }, {
1580
+ F: __dxlog_file5,
1581
+ L: 270,
1582
+ S: void 0,
1583
+ C: (f, a) => f(...a)
1584
+ });
1585
+ switch (streamBlock.type) {
1586
+ //
1587
+ // Text
1588
+ //
1589
+ case "text": {
1590
+ return {
1591
+ ...contentBlock,
1592
+ type: "text",
1593
+ text: streamBlock.content
1594
+ };
1595
+ }
1596
+ //
1597
+ // XML
1598
+ //
1599
+ case "tag": {
1600
+ switch (streamBlock.tag) {
1601
+ case "cot": {
1602
+ const content = streamBlock.content.map((block) => {
1603
+ switch (block.type) {
1604
+ case "text":
1605
+ return block.content;
1606
+ default:
1607
+ return null;
1608
+ }
1609
+ }).filter(isNotFalsy).join("\n");
1610
+ return {
1611
+ ...contentBlock,
1612
+ type: "text",
1613
+ disposition: "cot",
1614
+ text: content
1615
+ };
1616
+ }
1617
+ case "artifact": {
1618
+ const { attributes } = streamBlock;
1619
+ return {
1620
+ type: "json",
1621
+ disposition: "artifact",
1622
+ json: JSON.stringify(attributes)
1623
+ };
1624
+ }
1625
+ case "suggest": {
1626
+ if (streamBlock.content.length === 1 && streamBlock.content[0].type === "text") {
1627
+ return {
1628
+ type: "json",
1629
+ disposition: "suggest",
1630
+ json: JSON.stringify({
1631
+ text: streamBlock.content[0].content
1632
+ })
1633
+ };
1634
+ }
1635
+ break;
1636
+ }
1637
+ case "select": {
1638
+ return {
1639
+ type: "json",
1640
+ disposition: "select",
1641
+ json: JSON.stringify({
1642
+ options: streamBlock.content.flatMap((content) => content.type === "tag" && content.content.length === 1 && content.content[0].type === "text" ? [
1643
+ content.content[0].content
1644
+ ] : [])
1645
+ })
1646
+ };
1647
+ }
1648
+ case "tool-list": {
1649
+ return {
1650
+ type: "json",
1651
+ disposition: "tool_list",
1652
+ json: JSON.stringify({})
1653
+ };
1654
+ }
1655
+ }
1656
+ break;
1657
+ }
1658
+ //
1659
+ // JSON
1660
+ //
1661
+ case "json": {
1662
+ switch (contentBlock.type) {
1663
+ case "tool_use": {
1664
+ return {
1665
+ ...contentBlock,
1666
+ input: safeParseJson(streamBlock.content) ?? {}
1667
+ };
1668
+ }
1669
+ }
1670
+ return {
1671
+ ...contentBlock,
1672
+ type: "json",
1673
+ disposition: "artifact",
1674
+ json: streamBlock.content
1675
+ };
1676
+ }
1677
+ }
1678
+ };
1242
1679
  export {
1243
- AIServiceClientImpl,
1680
+ AIServiceEdgeClient,
1244
1681
  DEFAULT_LLM_MODEL,
1245
1682
  DEFAULT_LLM_MODELS,
1246
1683
  EchoDataSource,
1247
1684
  GenerateRequest,
1248
- GenerationStream,
1685
+ GenerationStreamImpl,
1249
1686
  LLMModel,
1687
+ MessageCollector,
1250
1688
  MixedStreamParser,
1689
+ OllamaClient,
1251
1690
  StreamTransform,
1252
1691
  ToolTypes,
1253
1692
  createArtifactElement,
1254
1693
  createGenerationStream,
1694
+ emitMessageAsEvents,
1255
1695
  executeQuery,
1256
1696
  executeQueryPlan,
1257
1697
  formatInferredRelationshipLabel,