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