utilitas 1998.2.37 → 1998.2.39

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/lib/alan.mjs CHANGED
@@ -54,7 +54,7 @@ const [
54
54
  CLAUDE_35_HAIKU, CLOUD_37_SONNET, AUDIO, WAV, CHATGPT_MINI, ATTACHMENTS,
55
55
  CHAT, OPENAI_VOICE, MEDIUM, LOW, HIGH, GPT_REASONING_EFFORT, THINK,
56
56
  THINK_STR, THINK_END, AZURE, TOOLS_STR, TOOLS_END, TOOLS, TEXT, THINKING,
57
- OK,
57
+ OK, FUNC,
58
58
  ] = [
59
59
  'OPENAI', 'GEMINI', 'CHATGPT', 'OPENAI_EMBEDDING', 'GEMINI_EMEDDING',
60
60
  'OPENAI_TRAINING', 'OLLAMA', 'CLAUDE', 'gpt-4o-mini', 'gpt-4o', 'o1',
@@ -66,7 +66,7 @@ const [
66
66
  'claude-3-7-sonnet@20250219', 'audio', 'wav', 'CHATGPT_MINI',
67
67
  '[ATTACHMENTS]', 'CHAT', 'OPENAI_VOICE', 'medium', 'low', 'high',
68
68
  'medium', 'think', '<think>', '</think>', 'AZURE', '<tools>',
69
- '</tools>', 'tools', 'text', 'thinking', 'OK',
69
+ '</tools>', 'tools', 'text', 'thinking', 'OK', 'function',
70
70
  ];
71
71
 
72
72
  const [
@@ -90,7 +90,7 @@ const [tool, provider, messages, text] = [
90
90
  const [name, user, system, assistant, MODEL, JSON_OBJECT, TOOL, silent]
91
91
  = ['Alan', 'user', 'system', 'assistant', 'model', 'json_object', 'tool', true];
92
92
  const [CODE_INTERPRETER, RETRIEVAL, FUNCTION]
93
- = ['code_interpreter', 'retrieval', 'function'].map(tool);
93
+ = ['code_interpreter', 'retrieval', FUNC].map(tool);
94
94
  const [NOT_INIT, INVALID_FILE]
95
95
  = ['AI engine has not been initialized.', 'Invalid file data.'];
96
96
  const chatConfig
@@ -442,7 +442,10 @@ const tools = [
442
442
  parameters: {
443
443
  type: 'object',
444
444
  properties: {
445
- keyword: { type: 'string', description: 'The keyword you need to search for.' }
445
+ keyword: { type: 'string', description: 'The keyword you need to search for.' },
446
+ num: { type: 'integer', description: 'The number of search results you need, default `10`.' },
447
+ start: { type: 'integer', description: 'The start index of the search results, default `1`.' },
448
+ image: { type: 'boolean', description: 'Whether to search for images, default `false`.' },
446
449
  },
447
450
  required: ['keyword'],
448
451
  additionalProperties: false
@@ -495,26 +498,7 @@ const init = async (options) => {
495
498
  if (options?.apiKey) {
496
499
  const { GoogleGenerativeAI } = await need('@google/generative-ai');
497
500
  const genAi = new GoogleGenerativeAI(options.apiKey);
498
- const genModel = options?.model || DEFAULT_MODELS[GEMINI];
499
- clients[provider] = {
500
- generative: genAi.getGenerativeModel({
501
- model: genModel,
502
- systemInstruction: { role: system, parts: [{ text: INSTRUCTIONS }] },
503
- ...MODELS[genModel]?.tools ? (options?.tools ?? {
504
- tools: [
505
- // @todo: Gemini will failed when using these tools together.
506
- // https://ai.google.dev/gemini-api/docs/function-calling
507
- // { codeExecution: {} },
508
- // { googleSearch: {} },
509
- { functionDeclarations: toolsGemini.map(x => x.def) },
510
- ],
511
- toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
512
- }) : {},
513
- }),
514
- embedding: genAi.getGenerativeModel({
515
- model: DEFAULT_MODELS[GEMINI_EMEDDING],
516
- }), genModel,
517
- };
501
+ clients[provider] = { client: genAi };
518
502
  }
519
503
  break;
520
504
  case CLAUDE:
@@ -620,7 +604,7 @@ const buildGeminiParts = (text, attachments) => {
620
604
 
621
605
  const buildGeminiMessage = (content, options) => {
622
606
  content = content || '';
623
- const attachments = (options?.attachments || []).map(x => ({
607
+ const attachments = (options?.attachments?.length ? options.attachments : []).map(x => ({
624
608
  inlineData: { mimeType: x.mime_type, data: x.data }
625
609
  }));
626
610
  return String.isString(content) ? (options?.history ? {
@@ -631,7 +615,7 @@ const buildGeminiMessage = (content, options) => {
631
615
 
632
616
  const buildClaudeMessage = (text, options) => {
633
617
  assert(text, 'Text is required.');
634
- const attachments = (options?.attachments || []).map(x => {
618
+ const attachments = (options?.attachments?.length ? options?.attachments : []).map(x => {
635
619
  let type = '';
636
620
  if ([pdf].includes(x.mime_type)) {
637
621
  type = 'document';
@@ -666,9 +650,38 @@ const listOpenAIModels = async (options) => {
666
650
  return options?.raw ? resp : resp.data;
667
651
  };
668
652
 
653
+ const streamResp = async (resp, options) => {
654
+ const msg = await packResp(resp, { ...options, processing: true });
655
+ return options?.stream && (msg?.text || msg?.audio?.length)
656
+ && await ignoreErrFunc(async () => await options.stream(msg), LOG);
657
+ };
658
+
659
+ const getInfoEnd = text => Math.max(...[THINK_END, TOOLS_END].map(x => {
660
+ const keyEnd = text.indexOf(`${x}\n`);
661
+ return keyEnd >= 0 ? (keyEnd + x.length) : 0;
662
+ }));
663
+
669
664
  const packResp = async (resp, options) => {
670
- let { text: txt, audio, references }
671
- = String.isString(resp) ? { text: resp } : resp;
665
+ if (options?.raw) { return resp; }
666
+ let [
667
+ txt, audio, references, markdown, simpleText, referencesMarkdown, end,
668
+ json
669
+ ] = [
670
+ resp.text // ChatGPT / Claude / Gemini
671
+ || resp?.message?.content || '', // Ollama @tudo: Need to be updated
672
+ resp?.audio?.data, // ChatGPT audio mode
673
+ resp?.references, // Gemini references
674
+ '', '', '', null,
675
+ ];
676
+ markdown = simpleText = txt;
677
+ while ((end = getInfoEnd(simpleText))) {
678
+ simpleText = simpleText.slice(end).trim();
679
+ end = getInfoEnd(simpleText);
680
+ }
681
+ [THINK_STR, TOOLS_STR].map(x => {
682
+ const str = simpleText.indexOf(x);
683
+ str >= 0 && (simpleText = simpleText.slice(0, str).trim());
684
+ });
672
685
  audio && (audio = Buffer.isBuffer(audio) ? audio : await convert(audio, {
673
686
  input: BASE64, expected: BUFFER,
674
687
  })) && audio.length && (audio = Buffer.concat([
@@ -676,6 +689,14 @@ const packResp = async (resp, options) => {
676
689
  ])) && (audio = await convert(audio, {
677
690
  input: BUFFER, expected: BUFFER, ...options || {},
678
691
  }));
692
+ options?.jsonMode && !options?.delta && !options?.processing
693
+ && (json = parseJson(simpleText));
694
+ if (options?.simple && options?.audioMode) { return audio; }
695
+ else if (options?.simple && options?.jsonMode) { return json; }
696
+ else if (options?.simple) { return simpleText; }
697
+ else if (options?.jsonMode) {
698
+ markdown = `\`\`\`json\n${simpleText}\n\`\`\``;
699
+ }
679
700
  // references debug codes:
680
701
  // references = {
681
702
  // "segments": [
@@ -698,77 +719,127 @@ const packResp = async (resp, options) => {
698
719
  // },
699
720
  // ]
700
721
  // };
701
- let [richText, referencesMarkdown] = [null, null];
702
- if (!options?.jsonMode) {
703
- if (!options?.processing
704
- && references?.segments?.length && references?.links?.length) {
705
- richText = txt;
706
- for (let i = references.segments.length - 1; i >= 0; i--) {
707
- let idx = richText.indexOf(references.segments[i].text);
708
- if (idx < 0) { continue; }
709
- idx += references.segments[i].text.length;
710
- richText = richText.slice(0, idx)
711
- + references.segments[i].indices.map(y => ` (${y + 1})`).join('')
712
- + richText.slice(idx);
713
- }
714
- referencesMarkdown = 'References:\n\n' + references.links.map((x, i) => {
715
- return `${i + 1}. [${x.title}](${x.uri})`;
716
- }).join('\n');
722
+ if (references?.segments?.length && references?.links?.length) {
723
+ for (let i = references.segments.length - 1; i >= 0; i--) {
724
+ let idx = markdown.indexOf(references.segments[i].text);
725
+ if (idx < 0) { continue; }
726
+ idx += references.segments[i].text.length;
727
+ markdown = markdown.slice(0, idx)
728
+ + references.segments[i].indices.map(y => ` (${y + 1})`).join('')
729
+ + markdown.slice(idx);
717
730
  }
718
- let lines = (richText || txt).split('\n');
719
- for (let i in lines) {
720
- switch (lines[i]) {
721
- case THINK_STR:
722
- lines[i] = MD_CODE + THINK;
723
- break;
724
- case TOOLS_STR:
725
- lines[i] = MD_CODE + TOOLS;
726
- break;
727
- case THINK_END:
728
- case TOOLS_END:
729
- lines[i] = MD_CODE;
730
- }
731
+ referencesMarkdown = 'References:\n\n' + references.links.map(
732
+ (x, i) => `${i + 1}. [${x.title}](${x.uri})`
733
+ ).join('\n');
734
+ }
735
+ markdown = markdown.split('\n');
736
+ for (let i in markdown) {
737
+ switch (markdown[i]) {
738
+ case THINK_STR: markdown[i] = MD_CODE + THINK; break;
739
+ case TOOLS_STR: markdown[i] = MD_CODE + TOOLS; break;
740
+ case THINK_END: case TOOLS_END: markdown[i] = MD_CODE;
731
741
  }
732
- richText = lines.join('\n').trim();
742
+ }
743
+ markdown = markdown.join('\n');
744
+ if (!options?.delta && !options?.processing) {
745
+ txt = txt.trim();
746
+ markdown = markdown.trim();
733
747
  }
734
748
  return {
735
- ...text(txt), ...options?.jsonMode && !(
736
- options?.delta && options?.processing
737
- ) ? { json: parseJson(txt) } : {},
738
- ...richText ? { richText } : {},
739
- ...references ? { references } : {},
749
+ ...text(txt), ...options?.jsonMode ? { json } : {},
750
+ markdown, ...references ? { references } : {},
740
751
  ...referencesMarkdown ? { referencesMarkdown } : {},
741
752
  ...audio ? { audio, audioMimeType: options?.audioMimeType } : {},
753
+ processing: options?.processing,
742
754
  model: options?.model,
743
755
  };
744
756
  };
745
757
 
746
- const streamResp = async (resp, options) => {
747
- const msg = await packGptResp(resp, { ...options, processing: true });
748
- return options?.stream && (msg?.text || msg?.audio?.length)
749
- && await ignoreErrFunc(async () => await options.stream(msg), LOG);
750
- };
751
-
752
- const packGptResp = async (resp, options) => {
753
- // simple mode is not recommended for streaming responses
754
- let text = resp.text // ChatGPT / Claude / Gemini
755
- || resp?.message?.content || ''; // Ollama
756
- const audio = resp?.message?.audio?.data; // ChatGPT audio mode
757
- if (options?.raw) { return resp; }
758
- else if (options?.simple && options?.jsonMode) { return parseJson(text); }
759
- else if (options?.simple && options?.audioMode) { return audio; }
760
- else if (options?.simple) {
761
- for (const key of [[THINK_STR, THINK_END], [TOOLS_STR, TOOLS_END]]) {
762
- const [findStr, findEnd] = key.map(x => text.indexOf(x));
763
- if (findStr >= 0 && findEnd >= 0 && findStr < findEnd) {
764
- text = text.split('')
765
- text.splice(findStr, findEnd + THINK_END.length)
766
- text = text.join('').trim();
758
+ const buildPrompts = async (model, input, options = {}) => {
759
+ assert(!(
760
+ options.jsonMode && !model?.json
761
+ ), `This model does not support JSON output: ${options.model}`);
762
+ assert(!(
763
+ options.reasoning && !model?.reasoning
764
+ ), `This model does not support reasoning: ${options.model}`);
765
+ let [systemPrompt, history, content, prompt, _system, _user, _assistant] = [
766
+ null, null, input || ATTACHMENTS, null, [], null, // length hack: ATTACHMENTS
767
+ { role: system }, { role: user }, { role: assistant }
768
+ ];
769
+ options.systemPrompt = options.systemPrompt || INSTRUCTIONS;
770
+ options.attachments = (
771
+ options.attachments?.length ? options.attachments : []
772
+ ).filter(x => [
773
+ ...model?.supportedMimeTypes || [], ...model.supportedAudioTypes || []
774
+ ].includes(x.mime_type));
775
+ switch (options.flavor) {
776
+ case CHATGPT:
777
+ systemPrompt = buildGptMessage(options.systemPrompt, _system);
778
+ prompt = buildGptMessage(content, options);
779
+ break;
780
+ case CLAUDE:
781
+ systemPrompt = buildClaudeMessage(options.systemPrompt, _system);
782
+ prompt = buildClaudeMessage(content, options)
783
+ break;
784
+ case OLLAMA:
785
+ systemPrompt = buildOllamaMessage(options.systemPrompt, _system);
786
+ prompt = buildOllamaMessage(content, options);
787
+ break;
788
+ case GEMINI:
789
+ systemPrompt = buildGeminiHistory(options.systemPrompt, _system);
790
+ prompt = options.toolsResult?.[options.toolsResult?.length - 1]?.parts
791
+ || buildGeminiMessage(content, options)
792
+ break;
793
+ }
794
+ const msgBuilder = () => {
795
+ history = [];
796
+ (options.messages?.length ? options.messages : []).map(x => {
797
+ switch (options.flavor) {
798
+ case CHATGPT:
799
+ history.push(buildGptMessage(x.request, _user));
800
+ history.push(buildGptMessage(x.response, _assistant));
801
+ break;
802
+ case CLAUDE:
803
+ history.push(buildClaudeMessage(x.request, _user));
804
+ history.push(buildClaudeMessage(x.response, _assistant));
805
+ break;
806
+ case OLLAMA:
807
+ history.push(buildClaudeMessage(x.request, _user));
808
+ history.push(buildClaudeMessage(x.response, _assistant));
809
+ break;
810
+ case GEMINI:
811
+ if (options.attachments?.length) { return; }
812
+ history.push(buildGeminiHistory(x.request, _user));
813
+ history.push(buildGeminiHistory(x.response, { role: MODEL }));
814
+ break;
767
815
  }
816
+ });
817
+ switch (options.flavor) {
818
+ case CHATGPT: case CLAUDE: case OLLAMA:
819
+ history.push(prompt, ...options.toolsResult?.length
820
+ ? options.toolsResult : []);
821
+ history = messages(history);
822
+ break;
823
+ case GEMINI:
824
+ history.push(
825
+ ...options.toolsResult?.length ? [
826
+ buildGeminiHistory(content, { ...options, role: user }),
827
+ ...options.toolsResult.slice(0, options.toolsResult.length - 1)
828
+ ] : []
829
+ );
830
+ break;
768
831
  }
769
- return text;
770
- }
771
- return await packResp({ text, audio, references: resp?.references }, options);
832
+ };
833
+ msgBuilder();
834
+ await trimPrompt(() => [systemPrompt, history, prompt], () => {
835
+ if (options.messages.length) {
836
+ options.messages.shift();
837
+ msgBuilder();
838
+ } else {
839
+ content = trimTailing(trimTailing(content).slice(0, -1)) + '...';
840
+ }
841
+ }, model.maxInputTokens - options.attachments?.length * ATTACHMENT_TOKEN_COST);
842
+ return { systemPrompt, history, prompt };
772
843
  };
773
844
 
774
845
  const handleToolsCall = async (msg, options) => {
@@ -792,9 +863,11 @@ const handleToolsCall = async (msg, options) => {
792
863
  for (const fn of calls) {
793
864
  switch (options?.flavor) {
794
865
  case CLAUDE:
795
- input = fn.input = String.isString(fn?.input) ? parseJson(fn.input) : fn?.input;
866
+ input = fn.input = String.isString(fn?.input)
867
+ ? parseJson(fn.input) : fn?.input;
796
868
  packMsg = (content, is_error) => ({
797
- type: 'tool_result', tool_use_id: fn.id, content, is_error,
869
+ type: 'tool_result', tool_use_id: fn.id,
870
+ content, is_error,
798
871
  });
799
872
  break;
800
873
  case GEMINI:
@@ -802,7 +875,8 @@ const handleToolsCall = async (msg, options) => {
802
875
  packMsg = (t, e) => ({
803
876
  functionResponse: {
804
877
  name: fn?.functionCall?.name, response: {
805
- name: fn?.functionCall?.name, content: e ? `[Error] ${t}` : t,
878
+ name: fn?.functionCall?.name,
879
+ content: e ? `[Error] ${t}` : t,
806
880
  }
807
881
  }
808
882
  });
@@ -842,13 +916,18 @@ const handleToolsCall = async (msg, options) => {
842
916
  log(rt);
843
917
  }
844
918
  }
845
- switch (options?.flavor) {
846
- case CLAUDE: content = content.length ? [{ role: user, content }] : []; break;
847
- case GEMINI: content = [{ role: user, parts: content }]; break;
919
+ if (content.length) {
920
+ switch (options?.flavor) {
921
+ case CLAUDE: content = [{ role: user, content }]; break;
922
+ case GEMINI: content = [{ role: FUNC, parts: content }]; break;
923
+ }
848
924
  }
849
925
  responded && await resp(`\n${TOOLS_END}`);
850
926
  }
851
- return { toolsResult: [...content.length ? preRes : [], ...content], toolsResponse };
927
+ return {
928
+ toolsResult: [...content.length ? preRes : [], ...content],
929
+ toolsResponse,
930
+ };
852
931
  };
853
932
 
854
933
  const mergeMsgs = (resp, calls) => [resp, ...calls.length ? [
@@ -856,43 +935,32 @@ const mergeMsgs = (resp, calls) => [resp, ...calls.length ? [
856
935
  ] : []].map(x => x.trim()).join('\n\n');
857
936
 
858
937
  const promptChatGPT = async (content, options = {}) => {
859
- const { client } = await getOpenAIClient(options);
860
- // https://github.com/openai/openai-node?tab=readme-ov-file#streaming-responses
861
- // custom api endpoint not supported vision apis @todo by @Leask
862
- // Structured Outputs: https://openai.com/index/introducing-structured-outputs-in-the-api/
863
- client.baseURL !== OPENAI_BASE_URL
864
- && options?.attachments?.length && (options.attachments = []);
865
- if (options?.model) { } else if (options?.provider === AZURE) {
938
+ if (options.model) { } else if (options.provider === AZURE) {
866
939
  options.model = DEFAULT_MODELS[AZURE];
867
- } else if (options?.reasoning) {
940
+ } else if (options.reasoning) {
868
941
  options.model = DEFAULT_MODELS[CHATGPT_REASONING];
869
942
  } else {
870
943
  options.model = DEFAULT_MODELS[CHATGPT];
871
944
  }
872
- options?.reasoning && !options?.reasoning_effort
945
+ let [_MODEL, result, resultAudio, event, resultTools, responded] = [
946
+ MODELS[options.model], options?.result ?? '', Buffer.alloc(0), null, [],
947
+ false
948
+ ];
949
+ options.reasoning && !options.reasoning_effort
873
950
  && (options.reasoning_effort = GPT_REASONING_EFFORT);
874
- const message = buildGptMessage(content, options);
875
- const modalities = options?.modalities || (
876
- options?.audioMode ? [TEXT, AUDIO] : undefined
877
- );
878
- assert(!(
879
- options?.jsonMode && !MODELS[options.model]?.json
880
- ), `This model does not support JSON output: ${options.model}`);
881
- assert(!(
882
- options?.reasoning && !MODELS[options.model]?.reasoning
883
- ), `This model does not support reasoning: ${options.model}`);
951
+ const { client } = await getOpenAIClient(options);
952
+ const { history }
953
+ = await buildPrompts(_MODEL, content, { ...options, flavor: CHATGPT });
954
+ const modalities = options.modalities
955
+ || (options.audioMode ? [TEXT, AUDIO] : undefined);
884
956
  [options.audioMimeType, options.suffix] = [pcm16, 'pcm.wav'];
885
- let [result, resultAudio, event, resultTools, responded]
886
- = [options?.result ?? '', Buffer.alloc(0), null, [], false];
887
957
  const resp = await client.chat.completions.create({
888
- modalities, audio: options?.audio || (
958
+ modalities, audio: options.audio || (
889
959
  modalities?.find?.(x => x === AUDIO)
890
960
  && { voice: DEFAULT_MODELS[OPENAI_VOICE], format: 'pcm16' }
891
- ), ...messages([
892
- ...options?.messages || [], message, ...options?.toolsResult || [],
893
- ]), ...MODELS[options.model]?.tools ? {
894
- tools: options?.tools ?? tools.map(x => x.def),
895
- } : {}, ...options?.jsonMode ? {
961
+ ), ...history, ..._MODEL?.tools ? {
962
+ tools: options.tools ?? tools.map(x => x.def),
963
+ } : {}, ...options.jsonMode ? {
896
964
  response_format: { type: JSON_OBJECT }
897
965
  } : {}, model: options.model, stream: true,
898
966
  store: true, tool_choice: 'auto',
@@ -914,12 +982,13 @@ const promptChatGPT = async (content, options = {}) => {
914
982
  x?.function?.name && (curFunc.function.name += x.function.name);
915
983
  x?.function?.arguments && (curFunc.function.arguments += x.function.arguments);
916
984
  }
917
- deltaText && (responded = responded || (deltaText = `\n\n${deltaText}`));
985
+ options.result && deltaText
986
+ && (responded = responded || (deltaText = `\n\n${deltaText}`));
918
987
  result += deltaText;
919
988
  resultAudio = Buffer.concat([resultAudio, deltaAudio]);
920
- const respAudio = options?.delta ? deltaAudio : resultAudio;
921
- await streamResp({
922
- text: options?.delta ? deltaText : result,
989
+ const respAudio = options.delta ? deltaAudio : resultAudio;
990
+ (deltaText || deltaAudio?.length) && await streamResp({
991
+ text: options.delta ? deltaText : result,
923
992
  ...respAudio.length ? { audio: { data: respAudio } } : {},
924
993
  }, options);
925
994
  }
@@ -933,7 +1002,7 @@ const promptChatGPT = async (content, options = {}) => {
933
1002
  return promptChatGPT(content, { ...options, toolsResult, result: toolsResponse });
934
1003
  }
935
1004
  event.text = mergeMsgs(toolsResponse, toolsResult);
936
- return await packGptResp(event, options);
1005
+ return await packResp(event, options);
937
1006
  };
938
1007
 
939
1008
  const promptAzure = async (content, options = {}) =>
@@ -944,61 +1013,56 @@ const promptOllama = async (content, options = {}) => {
944
1013
  // https://github.com/ollama/ollama-js
945
1014
  // https://github.com/jmorganca/ollama/blob/main/examples/typescript-simplechat/client.ts
946
1015
  options.model = options?.model || model;
947
- const resp = await client.chat({
948
- model: options.model, stream: true,
949
- ...messages([...options?.messages || [], buildOllamaMessage(content)]),
950
- })
951
- let [chunk, result] = [null, ''];
1016
+ let [_MODEL, chunk, result] = [MODELS[options.model], null, ''];
1017
+ const { history: h }
1018
+ = await buildPrompts(_MODEL, content, { ...options, flavor: OLLAMA });
1019
+ const resp = await client.chat({ model: options.model, stream: true, ...h });
952
1020
  for await (chunk of resp) {
953
1021
  const delta = chunk.message.content || '';
954
- if (delta === '') { continue; }
955
1022
  result += delta;
956
- chunk.message.content = options?.delta ? delta : result;
957
- await ignoreErrFunc(async () => await options?.stream?.(
958
- await packGptResp(chunk, { ...options || {}, processing: true })
959
- ), LOG);
1023
+ delta && await streamResp({
1024
+ text: options.delta ? delta : result,
1025
+ }, options);
960
1026
  }
961
- chunk.message.content = result;
962
- return await packGptResp(chunk, options);
1027
+ return await packResp({ text: result }, options);
963
1028
  };
964
1029
 
965
1030
  const promptClaude = async (content, options = {}) => {
1031
+ options.model = options.model || DEFAULT_MODELS[CLAUDE];
1032
+ let [_MODEL, event, text, thinking, signature, result, thinkEnd, tool_use]
1033
+ = [MODELS[options.model], null, '', '', '', options.result ?? '', '', []];
966
1034
  const { client } = await getClaudeClient(options);
967
- options.model = options?.model || DEFAULT_MODELS[CLAUDE];
1035
+ const { history }
1036
+ = await buildPrompts(_MODEL, content, { ...options, flavor: CLAUDE });
968
1037
  const resp = await client.messages.create({
969
- model: options.model, max_tokens: MODELS[options.model].maxOutputTokens,
970
- messages: [
971
- ...options?.messages || [], buildClaudeMessage(content, options),
972
- ...options?.toolsResult || [],
973
- ], stream: true,
974
- ...options?.reasoning ?? MODELS[options.model]?.reasoning ? {
975
- thinking: options?.thinking || { type: 'enabled', budget_tokens: 1024 },
976
- } : {}, // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
977
- ...MODELS[options.model]?.tools ? {
978
- tools: options?.tools ?? toolsClaude.map(x => x.def),
1038
+ model: options.model, max_tokens: _MODEL.maxOutputTokens, ...history,
1039
+ stream: true, ...options.reasoning ?? _MODEL?.reasoning ? {
1040
+ thinking: options.thinking || { type: 'enabled', budget_tokens: 1024 },
1041
+ } : {}, ..._MODEL?.tools ? { // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
1042
+ tools: options.tools ?? toolsClaude.map(x => x.def),
979
1043
  tool_choice: { type: 'auto' },
980
1044
  } : {},
981
1045
  });
982
- let [event, text, thinking, signature, result, thinkEnd, tool_use]
983
- = [null, '', '', '', options?.result ?? '', '', []];
984
1046
  for await (const chunk of resp) {
985
1047
  event = chunk?.content_block || chunk?.delta || {};
986
- let [thkDelta, txtDelta] = [event.thinking || '', event.text || ''];
987
- text += txtDelta;
988
- thinking += thkDelta;
1048
+ let [deltaThink, deltaText] = [event.thinking || '', event.text || ''];
1049
+ text += deltaText;
1050
+ thinking += deltaThink;
989
1051
  signature = signature || event?.signature || '';
990
- thkDelta && thkDelta === thinking
991
- && (thkDelta = `${THINK_STR}\n${thkDelta}`);
992
- thinking && txtDelta && !thinkEnd
993
- && (thinkEnd = thkDelta = `${thkDelta}\n${THINK_END}\n\n`);
1052
+ deltaThink && deltaThink === thinking
1053
+ && (deltaThink = `${THINK_STR}\n${deltaThink}`);
1054
+ thinking && deltaText && !thinkEnd
1055
+ && (thinkEnd = deltaThink = `${deltaThink}\n${THINK_END}\n\n`);
994
1056
  if (event?.type === 'tool_use') {
995
1057
  tool_use.push({ ...event, input: '' });
996
1058
  } else if (event.partial_json) {
997
1059
  tool_use[tool_use.length - 1].input += event.partial_json;
998
1060
  }
999
- txtDelta = thkDelta + txtDelta;
1000
- result += txtDelta;
1001
- await streamResp({ text: options?.delta ? txtDelta : result }, options);
1061
+ deltaText = deltaThink + deltaText;
1062
+ result += deltaText;
1063
+ deltaText && await streamResp({
1064
+ text: options.delta ? deltaText : result,
1065
+ }, options);
1002
1066
  }
1003
1067
  event = {
1004
1068
  role: assistant, content: [
@@ -1011,11 +1075,11 @@ const promptClaude = async (content, options = {}) => {
1011
1075
  );
1012
1076
  if (tool_use.length && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1013
1077
  return await promptClaude(content, {
1014
- ...options, toolsResult: [...options?.toolsResult || [],
1078
+ ...options, toolsResult: [...options.toolsResult || [],
1015
1079
  ...toolsResult], result: toolsResponse,
1016
1080
  });
1017
1081
  }
1018
- return packGptResp({ text: mergeMsgs(toolsResponse, tool_use) }, options);
1082
+ return packResp({ text: mergeMsgs(toolsResponse, tool_use) }, options);
1019
1083
  };
1020
1084
 
1021
1085
  const uploadFile = async (input, options) => {
@@ -1068,55 +1132,63 @@ const packGeminiReferences = (chunks, supports) => {
1068
1132
  };
1069
1133
 
1070
1134
  const promptGemini = async (content, options = {}) => {
1071
- const { generative, genModel } = await getGeminiClient(options);
1135
+ options.model || (options.model = DEFAULT_MODELS[GEMINI]);
1136
+ let [result, references, functionCalls, responded, _MODEL]
1137
+ = [options.result ?? '', null, null, false, MODELS[options.model]];
1138
+ const { client: _client } = await getGeminiClient(options);
1139
+ const { systemPrompt: systemInstruction, history, prompt }
1140
+ = await buildPrompts(_MODEL, content, { ...options, flavor: GEMINI });
1141
+ const client = _client.getGenerativeModel({
1142
+ model: options.model, systemInstruction,
1143
+ ...MODELS[options.model]?.tools && !options.jsonMode ? (
1144
+ options.tools ?? {
1145
+ tools: [
1146
+ // @todo: Gemini will failed when using these tools together.
1147
+ // https://ai.google.dev/gemini-api/docs/function-calling
1148
+ // { codeExecution: {} },
1149
+ // { googleSearch: {} },
1150
+ { functionDeclarations: toolsGemini.map(x => x.def) },
1151
+ ],
1152
+ toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
1153
+ }
1154
+ ) : {},
1155
+ });
1072
1156
  // https://github.com/google/generative-ai-js/blob/main/samples/node/advanced-chat.js
1073
1157
  // @todo: check this issue similar to Vertex AI:
1074
1158
  // Google's bug: history is not allowed while using inline_data?
1075
- assert(!(
1076
- options?.jsonMode && MODELS[genModel]?.json == false
1077
- ), `This model does not support JSON output: ${genModel} `);
1078
- options.model = genModel;
1079
- const chat = generative.startChat({
1080
- history: [
1081
- ...options?.messages && !options?.attachments?.length ? options.messages : [],
1082
- ...options?.toolsResult ? [
1083
- buildGeminiMessage(content, { ...options, history: true }),
1084
- ...options.toolsResult.slice(0, options.toolsResult.length - 1)
1085
- ] : []
1086
- ], ...generationConfig(options),
1087
- });
1088
- const resp = await chat.sendMessageStream(
1089
- options?.toolsResult?.[options?.toolsResult?.length]?.parts
1090
- || buildGeminiMessage(content, options)
1091
- );
1092
- let [result, references, functionCalls]
1093
- = [options?.result ?? '', null, null];
1159
+ const chat = client.startChat({ history, ...generationConfig(options) });
1160
+ const resp = await chat.sendMessageStream(prompt);
1094
1161
  for await (const chunk of resp.stream) {
1095
1162
  functionCalls || (functionCalls = chunk.functionCalls);
1096
- const delta = chunk?.text?.() || '';
1097
1163
  const rfc = packGeminiReferences(
1098
1164
  chunk.candidates[0]?.groundingMetadata?.groundingChunks,
1099
1165
  chunk.candidates[0]?.groundingMetadata?.groundingSupports
1100
1166
  );
1101
- if (delta === '' && !rfc) { continue; }
1167
+ rfc && (references = rfc);
1168
+ let delta = chunk?.text?.() || '';
1169
+ options.result && delta
1170
+ && (responded = responded || (delta = `\n\n${delta}`));
1102
1171
  result += delta;
1103
- references = rfc;
1104
- await streamResp({ text: options?.delta ? delta : result }, options);
1172
+ delta && await streamResp({
1173
+ text: options.delta ? delta : result,
1174
+ }, options);
1105
1175
  }
1106
1176
  const _resp = await resp.response;
1107
- functionCalls = (functionCalls() || _resp.functionCalls() || []).map(x => ({ functionCall: x }));
1177
+ functionCalls = (
1178
+ functionCalls() || _resp.functionCalls() || []
1179
+ ).map(x => ({ functionCall: x }));
1108
1180
  const { toolsResult, toolsResponse } = await handleToolsCall(
1109
1181
  { role: MODEL, parts: functionCalls },
1110
1182
  { ...options, result, flavor: GEMINI }
1111
1183
  );
1112
- if (functionCalls.length && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1184
+ if (toolsResult.length && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1113
1185
  return promptGemini(content, {
1114
1186
  ...options || {}, toolsResult: [...options?.toolsResult || [],
1115
1187
  ...toolsResult], result: toolsResponse,
1116
1188
  });
1117
1189
  }
1118
- return await packGptResp({
1119
- text: mergeMsgs(toolsResponse, functionCalls), references,
1190
+ return await packResp({
1191
+ text: mergeMsgs(toolsResponse, toolsResult), references,
1120
1192
  }, options);
1121
1193
  };
1122
1194
 
@@ -1148,9 +1220,9 @@ const createOpenAIEmbedding = async (input, options) => {
1148
1220
  };
1149
1221
 
1150
1222
  const createGeminiEmbedding = async (input, options) => {
1151
- const { embedding } = await getGeminiClient(options);
1223
+ const { client } = await getGeminiClient(options);
1152
1224
  const model = options?.model || DEFAULT_MODELS[GEMINI_EMEDDING];
1153
- const resp = await embedding.embedContent(
1225
+ const resp = await client.getGenerativeModel({ model }).embedContent(
1154
1226
  await checkEmbeddingInput(input, model)
1155
1227
  );
1156
1228
  return options?.raw ? resp : resp?.embedding.values;
@@ -1284,116 +1356,40 @@ const resetSession = async (sessionId, options) => {
1284
1356
 
1285
1357
  const packResult = resp => {
1286
1358
  const result = {
1287
- ...resp, richText: resp.richText || resp.text, spoken: renderText(
1288
- resp.text, { noCode: true, noLink: true }
1359
+ ...resp, spoken: renderText(
1360
+ resp.markdown, { noCode: true, noLink: true }
1289
1361
  ).replace(/\[\^\d\^\]/ig, ''),
1290
1362
  };
1291
- log(`Response (${result.model}): ${JSON.stringify(result.text)}`);
1363
+ log(`Response (${result.model}): ${JSON.stringify(result.markdown)}`);
1292
1364
  // log(result);
1293
1365
  return result;
1294
1366
  };
1295
1367
 
1296
1368
  const talk = async (input, options) => {
1297
- const engine = unifyEngine({
1298
- engine: Object.keys(chatConfig.engines)?.[0] || DEFAULT_MODELS[CHAT],
1299
- ...options,
1300
- });
1369
+ let [engine, chat, resp, sessionId] = [
1370
+ unifyEngine({
1371
+ engine: Object.keys(chatConfig.engines)?.[0] || DEFAULT_MODELS[CHAT],
1372
+ ...options,
1373
+ }), { request: input || ATTACHMENTS }, null,
1374
+ options?.sessionId || newSessionId(),
1375
+ ];
1301
1376
  assert(chatConfig.engines[engine], NOT_INIT);
1302
- const model = options?.model || chatConfig.engines[engine].model;
1303
- const _MODEL = MODELS[model];
1304
- const sessionId = options?.sessionId || newSessionId();
1305
1377
  const session = await getSession(sessionId, { engine, ...options });
1306
- let [resp, sys, messages, msgBuilder] = [null, [], [], null];
1307
- switch (engine) {
1308
- case CHATGPT: case AZURE:
1309
- sys.push(buildGptMessage(session.systemPrompt, { role: system }));
1310
- msgBuilder = () => {
1311
- messages = [];
1312
- session.messages.map(x => {
1313
- messages.push(buildGptMessage(x.request, { role: user }));
1314
- messages.push(buildGptMessage(x.response, { role: assistant }));
1315
- });
1316
- };
1317
- msgBuilder()
1318
- break;
1319
- case GEMINI:
1320
- // already set in the while client initialization:
1321
- // sys.push(buildGeminiHistory(session.systemPrompt, { role: user }));
1322
- msgBuilder = () => {
1323
- messages = [];
1324
- session.messages.map(x => {
1325
- messages.push(buildGeminiHistory(x.request, { role: user }));
1326
- messages.push(buildGeminiHistory(x.response, { role: MODEL }));
1327
- });
1328
- };
1329
- msgBuilder()
1330
- break;
1331
- case CLAUDE:
1332
- sys.push(buildClaudeMessage(session.systemPrompt, { role: system }));
1333
- msgBuilder = () => {
1334
- messages = [];
1335
- session.messages.map(x => {
1336
- messages.push(buildClaudeMessage(x.request, { role: user }));
1337
- messages.push(buildClaudeMessage(x.response, { role: assistant }));
1338
- });
1339
- };
1340
- msgBuilder()
1341
- break;
1342
- case OLLAMA:
1343
- sys.push(buildOllamaMessage(session.systemPrompt, { role: system }));
1344
- msgBuilder = () => {
1345
- messages = [];
1346
- session.messages.map(x => {
1347
- messages.push(buildOllamaMessage(x.request, { role: user }));
1348
- messages.push(buildOllamaMessage(x.response, { role: assistant }));
1349
- });
1350
- };
1351
- msgBuilder()
1352
- break;
1353
- default:
1354
- throwError(`Invalid AI engine: '${engine}'.`);
1355
- }
1356
- await trimPrompt(() => [...sys, ...messages, buildGeminiHistory(
1357
- input || ATTACHMENTS, { role: user } // length hack: ATTACHMENTS
1358
- )], () => {
1359
- if (messages.length) {
1360
- session.messages.shift();
1361
- msgBuilder && msgBuilder();
1362
- } else {
1363
- input = trimTailing(trimTailing(input).slice(0, -1)) + '...';
1364
- }
1365
- }, _MODEL.maxInputTokens - options?.attachments?.length * ATTACHMENT_TOKEN_COST);
1366
- const chat = { request: input || ATTACHMENTS };
1367
- const attachments = [];
1368
- (options?.attachments || []).filter(x => [
1369
- ..._MODEL?.supportedMimeTypes || [], ..._MODEL.supportedAudioTypes || []
1370
- ].includes(x.mime_type)).map(x => attachments.push(x));
1371
1378
  log(`Prompt (${engine}): ${JSON.stringify(input)}`);
1379
+ const pmtOptions = {
1380
+ messages: session.messages, model: chatConfig.engines[engine].model,
1381
+ ...options,
1382
+ };
1372
1383
  switch (engine) {
1373
- case CHATGPT:
1374
- resp = await promptChatGPT(input, {
1375
- messages, model, ...options, attachments,
1376
- });
1377
- break;
1378
- case GEMINI:
1379
- resp = await promptGemini(input, {
1380
- messages, ...options, attachments,
1381
- });
1382
- break;
1383
- case CLAUDE:
1384
- resp = await promptClaude(input, {
1385
- messages, model, ...options, attachments,
1386
- });
1387
- break;
1388
- case OLLAMA:
1389
- resp = await promptOllama(input, { messages, model, ...options });
1390
- break;
1391
- case AZURE:
1392
- resp = await promptAzure(input, { messages, model, ...options });
1393
- break;
1384
+ case CHATGPT: resp = await promptChatGPT(input, pmtOptions); break;
1385
+ case GEMINI: resp = await promptGemini(input, pmtOptions); break;
1386
+ case CLAUDE: resp = await promptClaude(input, pmtOptions); break;
1387
+ case OLLAMA: resp = await promptOllama(input, pmtOptions); break;
1388
+ case AZURE: resp = await promptAzure(input, pmtOptions); break;
1389
+ default: throwError(`Invalid AI engine: '${engine}'.`);
1394
1390
  }
1395
1391
  chat.response = resp.text;
1396
- chat?.request && chat?.response && session.messages.push(chat);
1392
+ chat.request && chat.response && session.messages.push(chat);
1397
1393
  await setSession(sessionId, session, options);
1398
1394
  return { sessionId, ...packResult(resp) };
1399
1395
  };