@gammatech/aijsx 0.1.3 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,14 +47,14 @@ __export(src_exports, {
47
47
  SystemMessage: () => SystemMessage,
48
48
  UserMessage: () => UserMessage,
49
49
  attachedContextSymbol: () => attachedContextSymbol,
50
- childrenToConversationMessage: () => childrenToConversationMessage,
51
50
  computeUsage: () => computeUsage,
52
51
  countAnthropicTokens: () => import_tokenizer3.countTokens,
53
52
  createAIElement: () => createAIElement,
54
53
  createContext: () => createContext,
55
54
  createRenderContext: () => createRenderContext,
56
55
  defaultMaxTokens: () => defaultMaxTokens,
57
- tokenCountForConversationMessage: () => tokenCountForConversationMessage,
56
+ tokenCountForOpenAIMessage: () => tokenCountForOpenAIMessage,
57
+ tokenCountForOpenAIVisionMessage: () => tokenCountForOpenAIVisionMessage,
58
58
  tokenLimitForChatModel: () => tokenLimitForChatModel,
59
59
  tokenizer: () => tokenizer
60
60
  });
@@ -70,32 +70,9 @@ var UserMessage = (props) => {
70
70
  var AssistantMessage = (props) => {
71
71
  return props.children;
72
72
  };
73
- var childrenToConversationMessage = (c) => {
74
- const children = Array.isArray(c) ? c : [c];
75
- return children.map((child) => {
76
- if (child.tag.name === "UserMessage") {
77
- return {
78
- type: "user",
79
- element: child
80
- };
81
- } else if (child.tag.name === "SystemMessage") {
82
- return {
83
- type: "system",
84
- element: child
85
- };
86
- } else if (child.tag.name === "AssistantMessage") {
87
- return {
88
- type: "assistant",
89
- element: child
90
- };
91
- } else {
92
- throw new Error("OpenAI: unknown message type");
93
- }
94
- });
95
- };
96
73
  var computeUsage = (messages) => {
97
- const prompt = messages.filter((m) => m.type === "user" || m.type === "system").reduce((acc, m) => acc + m.tokens, 0);
98
- const completion = messages.filter((m) => m.type === "assistant").reduce((acc, m) => acc + m.tokens, 0);
74
+ const prompt = messages.filter((m) => m.role === "user" || m.role === "system").reduce((acc, m) => acc + m.tokens, 0);
75
+ const completion = messages.filter((m) => m.role === "assistant").reduce((acc, m) => acc + m.tokens, 0);
99
76
  return {
100
77
  prompt,
101
78
  completion,
@@ -125,7 +102,7 @@ function createAIElement(tag, props, ...children) {
125
102
  return result;
126
103
  }
127
104
  function isAIElement(value) {
128
- return value !== null && typeof value === "object" && "tag" in value;
105
+ return value !== null && typeof value === "object" && "tag" in value && "render" in value;
129
106
  }
130
107
  function isLiteral(value) {
131
108
  return typeof value === "string" || typeof value === "number" || typeof value === "undefined" || typeof value === "boolean" || // capture null + undefined
@@ -314,6 +291,108 @@ function createRenderContext({
314
291
  // src/types.ts
315
292
  var attachedContextSymbol = Symbol("AI.attachedContext");
316
293
 
294
+ // src/xml.ts
295
+ var import_fast_xml_parser = require("fast-xml-parser");
296
+ var XmlNode = class {
297
+ constructor(parent, nodeName, attributes, value, childNodes) {
298
+ this.parent = parent;
299
+ this.nodeName = nodeName;
300
+ this.attributes = attributes;
301
+ this.value = value;
302
+ this.childNodes = childNodes;
303
+ this.childNodes = childNodes;
304
+ if (childNodes) {
305
+ childNodes.forEach((n) => n.parent = this);
306
+ }
307
+ }
308
+ toObject() {
309
+ if (this.value) {
310
+ return {
311
+ nodeName: this.nodeName,
312
+ value: this.value
313
+ };
314
+ }
315
+ if (this.childNodes) {
316
+ return {
317
+ nodeName: this.nodeName,
318
+ attributes: this.attributes,
319
+ childNodes: this.childNodes.map((n) => n.toObject())
320
+ };
321
+ }
322
+ }
323
+ get textContent() {
324
+ return this.value || this.childNodes?.map((n) => n.textContent).join("");
325
+ }
326
+ collapse(nodes) {
327
+ if (this.value !== void 0) {
328
+ return this;
329
+ }
330
+ if (this.childNodes) {
331
+ this.childNodes.forEach((n) => n.collapse(nodes));
332
+ }
333
+ if (
334
+ // is the root node
335
+ !this.parent || // is a preserved node tag
336
+ nodes.includes(this.nodeName)
337
+ ) {
338
+ return this;
339
+ }
340
+ const parent = this.parent;
341
+ const index = parent.childNodes.indexOf(this);
342
+ parent.childNodes.splice(index, 1, ...this.childNodes);
343
+ return this;
344
+ }
345
+ };
346
+ var XmlFragment = class extends XmlNode {
347
+ // initialize with value here because ts isn't smart enough
348
+ // to understand that the 4th argument to super initializes
349
+ // childNodes
350
+ childNodes = [];
351
+ constructor(childNodes) {
352
+ super(null, "#fragment", {}, void 0, childNodes);
353
+ }
354
+ };
355
+ function parseXml(input) {
356
+ const parser = new import_fast_xml_parser.XMLParser({
357
+ isArray: (_tag, path) => {
358
+ if (path.indexOf(".") === -1) {
359
+ return false;
360
+ }
361
+ return true;
362
+ },
363
+ ignoreAttributes: false,
364
+ attributeNamePrefix: "",
365
+ preserveOrder: true,
366
+ trimValues: false
367
+ });
368
+ const constructNode = (parent, nodeObject) => {
369
+ if (nodeObject.hasOwnProperty("#text")) {
370
+ return new XmlNode(parent, "#text", {}, nodeObject["#text"]);
371
+ }
372
+ const nodeName = Object.keys(nodeObject)[0];
373
+ const attributeName = Object.keys(nodeObject)[1];
374
+ const childObjects = nodeObject[nodeName];
375
+ const attributes = Object.entries(nodeObject[attributeName] || {}).reduce((acc, [key, value]) => {
376
+ try {
377
+ acc[key] = JSON.parse(value);
378
+ } catch (e) {
379
+ console.error(
380
+ `Error parsing attribute value (attr=${key} value=${value}) for element ${nodeName}`
381
+ );
382
+ }
383
+ return acc;
384
+ }, {});
385
+ const node = parent === null ? new XmlFragment([]) : new XmlNode(parent, nodeName, attributes, void 0, []);
386
+ node.childNodes = childObjects.map((child) => constructNode(node, child));
387
+ return node;
388
+ };
389
+ const parsed = parser.parse(`<#fragment>${input}</#fragment>`);
390
+ return constructNode(null, parsed[0]);
391
+ }
392
+ function escape(html) {
393
+ return html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
394
+ }
395
+
317
396
  // src/context.ts
318
397
  var LoggerContext = createContext(
319
398
  new NoopLogImplementation()
@@ -469,8 +548,11 @@ var StreamRenderContext = class _StreamRenderContext {
469
548
  this.contextValues = contextValues;
470
549
  const logImpl = this.getContext(LoggerContext);
471
550
  this.logger = new BoundLogger(logImpl, this);
472
- this.render = (renderable) => {
473
- const generator = this.renderStream(renderable);
551
+ this.render = (renderable, opts) => {
552
+ const generator = this.renderStream(
553
+ renderable,
554
+ opts || { preserveTags: false }
555
+ );
474
556
  const result = {
475
557
  then: (onFulfilled, onRejected) => accumResults(generator).then(onFulfilled, onRejected),
476
558
  [Symbol.asyncIterator]: () => generator
@@ -478,9 +560,14 @@ var StreamRenderContext = class _StreamRenderContext {
478
560
  return result;
479
561
  };
480
562
  const self = this;
481
- this.renderStream = async function* (renderable) {
563
+ this.renderStream = async function* (renderable, opts) {
564
+ const preserveTags = opts.preserveTags ?? false;
565
+ const renderedProps = opts.renderedProps || {};
566
+ const renderEscaped = (val) => {
567
+ return preserveTags ? escape(val) : val;
568
+ };
482
569
  if (isLiteral(renderable)) {
483
- yield renderLiteral(renderable);
570
+ yield renderEscaped(renderLiteral(renderable));
484
571
  return;
485
572
  }
486
573
  if (isAIElement(renderable)) {
@@ -488,8 +575,29 @@ var StreamRenderContext = class _StreamRenderContext {
488
575
  const childRenderId = uuidv4();
489
576
  const newCtx = self.enter(renderable, childRenderId, ctxValues);
490
577
  const logger = newCtx.logger;
578
+ const isFragment = renderable.tag.name === "AIFragment";
491
579
  try {
492
- return yield* newCtx.render(renderable.render(newCtx));
580
+ if (preserveTags && !isFragment) {
581
+ const propsToRender = Object.entries(
582
+ renderable.props
583
+ ).reduce(
584
+ // create an array of escaped + json serialized key="{value}"s for attributes
585
+ (acc, [name, value]) => {
586
+ if (renderedProps[renderable.tag.name]?.[name]) {
587
+ acc.push(`${name}="${escape(JSON.stringify(value))}"`);
588
+ }
589
+ return acc;
590
+ },
591
+ []
592
+ ).join(" ");
593
+ const attrs = propsToRender ? ` ${propsToRender}` : "";
594
+ yield `<${renderable.tag.name}${attrs}>`;
595
+ }
596
+ yield* newCtx.renderStream(renderable.render(newCtx), opts);
597
+ if (preserveTags && !isFragment) {
598
+ yield `</${renderable.tag.name}>`;
599
+ }
600
+ return;
493
601
  } catch (ex) {
494
602
  logger.logException(ex);
495
603
  throw ex;
@@ -497,10 +605,10 @@ var StreamRenderContext = class _StreamRenderContext {
497
605
  }
498
606
  if (Array.isArray(renderable)) {
499
607
  if (renderable.every((r) => isLiteral(r))) {
500
- yield renderable.map((r) => renderLiteral(r)).join("");
608
+ yield renderable.map((r) => renderEscaped(renderLiteral(r))).join("");
501
609
  return;
502
610
  }
503
- const streams = renderable.filter((a) => !!a).map((r) => self.renderStream(r));
611
+ const streams = renderable.filter((a) => !!a).map((r) => self.renderStream(r, opts));
504
612
  const result = coalesceParallelStreams(streams);
505
613
  while (true) {
506
614
  const { value, done } = await result.next();
@@ -521,7 +629,7 @@ var StreamRenderContext = class _StreamRenderContext {
521
629
  const next = await renderable.then(
522
630
  (r) => r
523
631
  );
524
- return yield* self.render(next);
632
+ return yield* self.renderStream(next, opts);
525
633
  };
526
634
  }
527
635
  render;
@@ -623,23 +731,44 @@ function tokenLimitForChatModel(model) {
623
731
  }
624
732
  }
625
733
  }
626
- function tokenCountForConversationMessage(message) {
734
+ function tokenCountForOpenAIMessage(message) {
627
735
  const TOKENS_PER_MESSAGE = 3;
628
- switch (message.type) {
736
+ switch (message.role) {
629
737
  case "assistant":
630
738
  case "system":
631
739
  case "user":
632
- return TOKENS_PER_MESSAGE + tokenizer.encode(message.content).length;
740
+ return (
741
+ // TODO this isn't working for vision
742
+ TOKENS_PER_MESSAGE + tokenizer.encode(message.content).length
743
+ );
633
744
  }
634
745
  }
635
-
636
- // src/jsx-runtime.ts
637
- function jsx(type, config, maybeKey) {
638
- const configWithKey = maybeKey !== void 0 ? { ...config, key: maybeKey } : config;
639
- const children = config && Array.isArray(config.children) ? config.children : [];
640
- return createAIElement(type, configWithKey, ...children);
746
+ function tokenCountForOpenAIVisionMessage(message) {
747
+ const TOKENS_PER_MESSAGE = 3;
748
+ const textCost = (content) => {
749
+ return TOKENS_PER_MESSAGE + tokenizer.encode(content).length;
750
+ };
751
+ switch (message.role) {
752
+ case "assistant":
753
+ case "system":
754
+ return textCost(message.content || "");
755
+ case "user":
756
+ if (typeof message.content === "string") {
757
+ return textCost(message.content);
758
+ }
759
+ return message.content.reduce((acc, part) => {
760
+ if (part.type === "text") {
761
+ return acc + textCost(part.text);
762
+ } else {
763
+ if (!part.image_url.detail || part.image_url.detail === "low") {
764
+ return acc + 85;
765
+ } else {
766
+ return acc + (170 * 4 + 85);
767
+ }
768
+ }
769
+ }, 0);
770
+ }
641
771
  }
642
- var jsxs = jsx;
643
772
 
644
773
  // src/lib/openai/OpenAI.tsx
645
774
  var defaultClient = null;
@@ -651,35 +780,60 @@ var OpenAIClientContext = createContext(() => {
651
780
  defaultClient = new import_openai.OpenAI({ apiKey });
652
781
  return defaultClient;
653
782
  });
783
+ function buildOpenAIMessages(childrenXml) {
784
+ const messages = [];
785
+ const chatMessageTags2 = ["UserMessage", "AssistantMessage", "SystemMessage"];
786
+ const parsed = parseXml(childrenXml).collapse(chatMessageTags2);
787
+ const topLevelValid = parsed.childNodes.every(
788
+ (node) => chatMessageTags2.includes(node.nodeName)
789
+ );
790
+ if (!topLevelValid) {
791
+ throw new Error("Invalid top level chat message tags");
792
+ }
793
+ for (const node of parsed.childNodes) {
794
+ if (node.nodeName === "UserMessage") {
795
+ messages.push({
796
+ content: node.textContent,
797
+ role: "user"
798
+ });
799
+ } else if (node.nodeName === "AssistantMessage") {
800
+ messages.push({
801
+ content: node.textContent,
802
+ role: "assistant"
803
+ });
804
+ } else if (node.nodeName === "SystemMessage") {
805
+ messages.push({
806
+ content: node.textContent,
807
+ role: "system"
808
+ });
809
+ }
810
+ }
811
+ return messages;
812
+ }
654
813
  async function* OpenAIChatCompletion(props, { logger, render, getContext }) {
655
814
  const startTime = performance.now();
656
815
  const client = getContext(OpenAIClientContext)();
657
816
  if (!client) {
658
817
  throw new Error("[OpenAI] must supply OpenAI model via context");
659
818
  }
660
- const renderedMessages = await Promise.all(
661
- childrenToConversationMessage(props.children).map(async (message) => {
662
- const partiallyRendered = {
663
- ...message,
664
- content: await render(message.element)
665
- };
666
- return {
667
- ...partiallyRendered,
668
- tokens: tokenCountForConversationMessage(partiallyRendered)
669
- };
819
+ const openAIMessages = buildOpenAIMessages(
820
+ await render(props.children, {
821
+ preserveTags: true
670
822
  })
671
823
  );
672
- const chatMessages = renderedMessages.map((m) => {
824
+ const renderedMessages = openAIMessages.map((message) => {
673
825
  return {
674
- content: m.content,
675
- role: m.type
826
+ role: message.role,
827
+ // TODO support gpt4 vision
828
+ content: message.content,
829
+ tokens: tokenCountForOpenAIMessage(message)
676
830
  };
677
831
  });
678
832
  const chatCompletionRequest = {
679
833
  model: props.model,
680
834
  max_tokens: props.maxTokens,
681
835
  temperature: props.temperature,
682
- messages: chatMessages,
836
+ messages: openAIMessages,
683
837
  stream: true
684
838
  };
685
839
  const logRequestData = {
@@ -721,11 +875,10 @@ async function* OpenAIChatCompletion(props, { logger, render, getContext }) {
721
875
  }
722
876
  }
723
877
  const outputMessage = {
724
- type: "assistant",
725
- element: /* @__PURE__ */ jsx(AssistantMessage, { children: content }),
878
+ role: "assistant",
726
879
  content,
727
- tokens: tokenCountForConversationMessage({
728
- type: "assistant",
880
+ tokens: tokenCountForOpenAIMessage({
881
+ role: "assistant",
729
882
  content
730
883
  })
731
884
  };
@@ -758,6 +911,48 @@ var AnthropicClientContext = createContext(
758
911
  }
759
912
  );
760
913
  var defaultMaxTokens = 4096;
914
+ var chatMessageTags = ["UserMessage", "AssistantMessage", "SystemMessage"];
915
+ function buildChatMessages(childrenXml) {
916
+ const messages = [];
917
+ const parsed = parseXml(childrenXml).collapse(chatMessageTags);
918
+ const topLevelValid = parsed.childNodes.every(
919
+ (node) => chatMessageTags.includes(node.nodeName)
920
+ );
921
+ if (!topLevelValid) {
922
+ throw new Error("Invalid top level chat message tags");
923
+ }
924
+ for (const node of parsed.childNodes) {
925
+ if (node.nodeName === "UserMessage") {
926
+ const content = `${import_sdk.default.HUMAN_PROMPT} ${node.textContent}`;
927
+ messages.push({
928
+ role: "user",
929
+ content,
930
+ tokens: (0, import_tokenizer2.countTokens)(content)
931
+ });
932
+ } else if (node.nodeName === "AssistantMessage") {
933
+ const content = `${import_sdk.default.AI_PROMPT} ${node.textContent}`;
934
+ messages.push({
935
+ role: "assistant",
936
+ content,
937
+ tokens: (0, import_tokenizer2.countTokens)(content)
938
+ });
939
+ } else if (node.nodeName === "SystemMessage") {
940
+ const userContent = `${import_sdk.default.HUMAN_PROMPT} For subsequent replies you will adhere to the following instructions: ${node.textContent}`;
941
+ messages.push({
942
+ role: "user",
943
+ content: userContent,
944
+ tokens: (0, import_tokenizer2.countTokens)(userContent)
945
+ });
946
+ const assistantContent = `${import_sdk.default.AI_PROMPT} Okay, I will do that.`;
947
+ messages.push({
948
+ role: "assistant",
949
+ content: assistantContent,
950
+ tokens: (0, import_tokenizer2.countTokens)(assistantContent)
951
+ });
952
+ }
953
+ }
954
+ return messages;
955
+ }
761
956
  async function* AnthropicChatCompletion(props, { render, logger, getContext }) {
762
957
  const startTime = performance.now();
763
958
  const client = getContext(AnthropicClientContext)();
@@ -766,41 +961,17 @@ async function* AnthropicChatCompletion(props, { render, logger, getContext }) {
766
961
  "[AnthropicChatCompletion] must supply AnthropicClient via context"
767
962
  );
768
963
  }
769
- const renderedMessages = await Promise.all(
770
- childrenToConversationMessage(props.children).flatMap((message) => {
771
- if (message.type === "system") {
772
- return [
773
- {
774
- type: "user",
775
- element: /* @__PURE__ */ jsxs(UserMessage, { children: [
776
- "For subsequent replies you will adhere to the following instructions: ",
777
- message.element
778
- ] })
779
- },
780
- {
781
- type: "assistant",
782
- element: /* @__PURE__ */ jsx(AssistantMessage, { children: "Okay, I will do that." })
783
- }
784
- ];
785
- }
786
- return [message];
787
- }).map(async (message) => {
788
- const prefix = message.type === "user" ? import_sdk.default.HUMAN_PROMPT : import_sdk.default.AI_PROMPT;
789
- const rendered = await render(message.element);
790
- const content2 = `${prefix} ${rendered.trim()}`;
791
- return {
792
- ...message,
793
- content: content2,
794
- tokens: (0, import_tokenizer2.countTokens)(content2)
795
- };
964
+ const inputMessages = buildChatMessages(
965
+ await render(props.children, {
966
+ preserveTags: true
796
967
  })
797
968
  );
798
- const chatMessages = renderedMessages.map((m) => {
799
- return m.content;
800
- });
801
- chatMessages.push(import_sdk.default.AI_PROMPT);
969
+ const prompt = [
970
+ ...inputMessages.map((message) => message.content),
971
+ import_sdk.default.AI_PROMPT
972
+ ].join("");
802
973
  const anthropicCompletionRequest = {
803
- prompt: chatMessages.join("\n\n"),
974
+ prompt,
804
975
  max_tokens_to_sample: props.maxTokens ?? defaultMaxTokens,
805
976
  temperature: props.temperature,
806
977
  model: props.model,
@@ -811,7 +982,7 @@ async function* AnthropicChatCompletion(props, { render, logger, getContext }) {
811
982
  model: props.model,
812
983
  provider: props.provider,
813
984
  providerRegion: props.providerRegion,
814
- inputMessages: renderedMessages,
985
+ inputMessages,
815
986
  request: anthropicCompletionRequest
816
987
  };
817
988
  logger.chatCompletionRequest("anthropic", logRequestData);
@@ -843,8 +1014,7 @@ async function* AnthropicChatCompletion(props, { render, logger, getContext }) {
843
1014
  yield text;
844
1015
  }
845
1016
  const outputMessage = {
846
- type: "assistant",
847
- element: /* @__PURE__ */ jsx(AssistantMessage, { children: content }),
1017
+ role: "assistant",
848
1018
  content,
849
1019
  tokens: (0, import_tokenizer2.countTokens)(content)
850
1020
  };
@@ -853,7 +1023,7 @@ async function* AnthropicChatCompletion(props, { render, logger, getContext }) {
853
1023
  finishReason: "stop",
854
1024
  latency: performance.now() - startTime,
855
1025
  outputMessage,
856
- tokensUsed: computeUsage([...renderedMessages, outputMessage])
1026
+ tokensUsed: computeUsage([...inputMessages, outputMessage])
857
1027
  };
858
1028
  logger.chatCompletionResponse("anthropic", responseData);
859
1029
  }
@@ -881,14 +1051,14 @@ var import_tokenizer3 = require("@anthropic-ai/tokenizer");
881
1051
  SystemMessage,
882
1052
  UserMessage,
883
1053
  attachedContextSymbol,
884
- childrenToConversationMessage,
885
1054
  computeUsage,
886
1055
  countAnthropicTokens,
887
1056
  createAIElement,
888
1057
  createContext,
889
1058
  createRenderContext,
890
1059
  defaultMaxTokens,
891
- tokenCountForConversationMessage,
1060
+ tokenCountForOpenAIMessage,
1061
+ tokenCountForOpenAIVisionMessage,
892
1062
  tokenLimitForChatModel,
893
1063
  tokenizer
894
1064
  });