utilitas 1998.2.35 → 1998.2.36

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
@@ -743,14 +743,18 @@ const packResp = async (resp, options) => {
743
743
  };
744
744
  };
745
745
 
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
+
746
752
  const packGptResp = async (resp, options) => {
747
753
  // simple mode is not recommended for streaming responses
748
- let text = resp?.choices?.[0]?.message?.content // ChatGPT
749
- || resp?.choices?.[0]?.message?.audio?.transcript // ChatGPT audio mode
754
+ let text = resp.text // ChatGPT / Claude
750
755
  || (Function.isFunction(resp?.text) ? resp.text() : resp?.text) // Gemini
751
- || resp?.content?.find(x => x.type === TEXT)?.text // Claude
752
756
  || resp?.message?.content || ''; // Ollama
753
- const audio = resp?.choices?.[0]?.message?.audio?.data; // ChatGPT audio mode
757
+ const audio = resp?.message?.audio?.data; // ChatGPT audio mode
754
758
  if (options?.raw) { return resp; }
755
759
  else if (options?.simple && options?.jsonMode) { return parseJson(text); }
756
760
  else if (options?.simple && options?.audioMode) { return audio; }
@@ -769,24 +773,24 @@ const packGptResp = async (resp, options) => {
769
773
  };
770
774
 
771
775
  const handleToolsCall = async (msg, options) => {
772
- let [content, preRes, input, packMsg, toolsResponse]
773
- = [[], [], [], null, options?.result ? options?.result.trim() : ''];
774
- const resp = async (msg) => {
775
- msg = `\n${msg}`;
776
- toolsResponse = (toolsResponse + msg).trim();
777
- // @todo: handle more flavors by @LeaskH
778
- await ignoreErrFunc(async () => await options?.stream?.(await packGptResp({
779
- choices: [{ message: { content: options?.delta ? msg : toolsResponse } }]
780
- }, { ...options || {}, processing: true })), LOG);
776
+ let [content, preRes, input, packMsg, toolsResponse, responded] = [
777
+ [], [], [], null, options?.result ? options?.result.trim() : '', false
778
+ ];
779
+ const resp = async m => {
780
+ m = `\n${m}`;
781
+ responded || (m = `\n\n${TOOLS_STR}${m}`);
782
+ responded = true;
783
+ toolsResponse = (toolsResponse + m).trim();
784
+ await streamResp({ text: options?.delta ? m : toolsResponse }, options);
781
785
  };
782
- if (msg?.tool_calls?.length) {
783
- await resp(`\n${TOOLS_STR}`);
786
+ const calls = msg.tool_calls || msg.content || [];
787
+ if (calls.length) {
784
788
  switch (options?.flavor) {
785
- case CLAUDE: preRes.push({ role: assistant, content: msg?.tool_calls }); break;
789
+ case CLAUDE: preRes.push(msg); break;
786
790
  case GEMINI: preRes.push({ role: MODEL, parts: msg?.tool_calls.map(x => ({ functionCall: x })) }); break;
787
- case CHATGPT: default: preRes.push({ role: assistant, ...msg }); break;
791
+ case CHATGPT: default: preRes.push(msg); break;
788
792
  }
789
- for (const fn of msg.tool_calls) {
793
+ for (const fn of calls) {
790
794
  switch (options?.flavor) {
791
795
  case CLAUDE:
792
796
  input = fn.input = String.isString(fn?.input) ? parseJson(fn.input) : fn?.input;
@@ -813,12 +817,15 @@ const handleToolsCall = async (msg, options) => {
813
817
  break;
814
818
  }
815
819
  const name = fn?.function?.name || fn?.name;
820
+ if (!name) { continue; }
816
821
  await resp(`\nName: ${name}`);
817
822
  const f = tools.find(x => insensitiveCompare(
818
823
  x.def?.function?.name || x?.def?.name, name
819
824
  ));
820
825
  if (!f?.func) {
821
- content.push(packMsg(`Function call failed: invalid function name ${name}`, true));
826
+ const rt = `Failed: invalid function name \`${name}\``;
827
+ content.push(packMsg(rt, true));
828
+ await resp(rt);
822
829
  continue;
823
830
  }
824
831
  const description = f.def?.function?.description || f.def?.description;
@@ -830,20 +837,25 @@ const handleToolsCall = async (msg, options) => {
830
837
  content.push(packMsg(output));
831
838
  await resp(f.showRes ? `Output: ${output}` : `Status: OK`);
832
839
  } catch (err) {
833
- content.push(packMsg(`Function call failed: ${err.message}`, true));
834
- await resp(`Failed: ${err.message}`);
835
- log(`Function call failed: ${err.message}`);
840
+ const rt = `Failed: ${err.message}`;
841
+ content.push(packMsg(rt, true));
842
+ await resp(rt);
843
+ log(rt);
836
844
  }
837
845
  }
838
846
  switch (options?.flavor) {
839
- case CLAUDE: content = [{ role: user, content }]; break;
847
+ case CLAUDE: content = content.length ? [{ role: user, content }] : []; break;
840
848
  case GEMINI: content = [{ role: user, parts: content }]; break;
841
849
  }
842
- await resp(`\n${TOOLS_END}`);
850
+ responded && await resp(`\n${TOOLS_END}`);
843
851
  }
844
- return { toolsResult: [...preRes, ...content], toolsResponse };
852
+ return { toolsResult: [...content.length ? preRes : [], ...content], toolsResponse };
845
853
  };
846
854
 
855
+ const mergeMsgs = (resp, calls) => [resp, ...calls.length ? [
856
+ `⚠️ Tools recursion limit reached: ${MAX_TOOL_RECURSION}`
857
+ ] : []].map(x => x.trim()).join('\n\n');
858
+
847
859
  const promptChatGPT = async (content, options = {}) => {
848
860
  const { client } = await getOpenAIClient(options);
849
861
  // https://github.com/openai/openai-node?tab=readme-ov-file#streaming-responses
@@ -871,10 +883,8 @@ const promptChatGPT = async (content, options = {}) => {
871
883
  options?.reasoning && !MODELS[options.model]?.reasoning
872
884
  ), `This model does not support reasoning: ${options.model}`);
873
885
  [options.audioMimeType, options.suffix] = [pcm16, 'pcm.wav'];
874
- let [result, resultAudio, chunk, resultTools] = [
875
- options?.result ? `${options?.result}\n\n` : '',
876
- Buffer.alloc(0), null, []
877
- ];
886
+ let [result, resultAudio, event, resultTools, responded]
887
+ = [options?.result ?? '', Buffer.alloc(0), null, [], false];
878
888
  const resp = await client.chat.completions.create({
879
889
  modalities, audio: options?.audio || (
880
890
  modalities?.find?.(x => x === AUDIO)
@@ -888,53 +898,43 @@ const promptChatGPT = async (content, options = {}) => {
888
898
  } : {}, model: options.model, stream: true,
889
899
  store: true, tool_choice: 'auto',
890
900
  });
891
- for await (chunk of resp) {
892
- const deltaText = chunk.choices[0]?.delta?.content
893
- || chunk.choices[0]?.delta?.audio?.transcript || '';
894
- const deltaAudio = chunk.choices[0]?.delta?.audio?.data ? await convert(
895
- chunk.choices[0].delta.audio.data, { input: BASE64, expected: BUFFER }
901
+ for await (event of resp) {
902
+ event = event?.choices?.[0] || {};
903
+ const delta = event.delta || {};
904
+ let deltaText = delta.content || delta.audio?.transcript || '';
905
+ const deltaAudio = delta.audio?.data ? await convert(
906
+ delta.audio.data, { input: BASE64, expected: BUFFER }
896
907
  ) : Buffer.alloc(0);
897
- const deltaFunc = chunk.choices[0]?.delta?.tool_calls || [];
898
- for (const x in deltaFunc) {
899
- let curFunc = resultTools.find(z => z.index === deltaFunc[x].index);
908
+ for (const x of delta.tool_calls || []) {
909
+ let curFunc = resultTools.find(y => y.index === x.index);
900
910
  curFunc || (resultTools.push(curFunc = {}));
901
- isSet(deltaFunc[x].index, true) && (curFunc.index = deltaFunc[x].index);
902
- deltaFunc[x].id && (curFunc.id = deltaFunc[x].id);
903
- deltaFunc[x].type && (curFunc.type = deltaFunc[x].type);
911
+ isSet(x.index, true) && (curFunc.index = x.index);
912
+ x.id && (curFunc.id = x.id);
913
+ x.type && (curFunc.type = x.type);
904
914
  curFunc.function || (curFunc.function = { name: '', arguments: '' });
905
- if (deltaFunc[x].function) {
906
- deltaFunc[x].function.name && (curFunc.function.name += deltaFunc[x].function.name);
907
- deltaFunc[x].function.arguments && (curFunc.function.arguments += deltaFunc[x].function.arguments);
908
- }
915
+ x?.function?.name && (curFunc.function.name += x.function.name);
916
+ x?.function?.arguments && (curFunc.function.arguments += x.function.arguments);
909
917
  }
910
- if (deltaText === '' && !deltaAudio.length) { continue; }
918
+ deltaText && (responded = responded || (deltaText = `\n\n${deltaText}`));
911
919
  result += deltaText;
912
920
  resultAudio = Buffer.concat([resultAudio, deltaAudio]);
913
921
  const respAudio = options?.delta ? deltaAudio : resultAudio;
914
- chunk.choices[0].message = {
915
- content: options?.delta ? deltaText : result,
922
+ await streamResp({
923
+ text: options?.delta ? deltaText : result,
916
924
  ...respAudio.length ? { audio: { data: respAudio } } : {},
917
- };
918
- await ignoreErrFunc(async () => await options?.stream?.(
919
- await packGptResp(chunk, { ...options, processing: true })
920
- ), LOG);
925
+ }, options);
921
926
  }
922
- chunk.choices?.[0] || (chunk.choices = [{}]); // handle empty choices for Azure APIs
923
- chunk.choices[0].message = {
924
- content: result, tool_calls: resultTools,
927
+ event = {
928
+ role: assistant, text: result, tool_calls: resultTools,
925
929
  ...resultAudio.length ? { audio: { data: resultAudio } } : {},
926
930
  };
927
- const { toolsResult, toolsResponse } = await handleToolsCall(
928
- chunk?.choices?.[0]?.message, { ...options, result }
929
- );
930
-
931
+ const { toolsResult, toolsResponse }
932
+ = await handleToolsCall(event, { ...options, result });
931
933
  if (toolsResult.length && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
932
934
  return promptChatGPT(content, { ...options, toolsResult, result: toolsResponse });
933
935
  }
934
- chunk.choices[0].message.content = [toolsResponse, ...toolsResult.length ? [
935
- `⚠️ Tools recursion limit reached: ${MAX_TOOL_RECURSION}`
936
- ] : []].map(x => x.trim()).join('\n\n');
937
- return await packGptResp(chunk, options);
936
+ event.text = mergeMsgs(toolsResponse, toolsResult);
937
+ return await packGptResp(event, options);
938
938
  };
939
939
 
940
940
  const promptAzure = async (content, options = {}) =>
@@ -966,13 +966,13 @@ const promptOllama = async (content, options = {}) => {
966
966
  const promptClaude = async (content, options = {}) => {
967
967
  const { client } = await getClaudeClient(options);
968
968
  options.model = options?.model || DEFAULT_MODELS[CLAUDE];
969
- const reasoning = options?.reasoning ?? MODELS[options.model]?.reasoning;
970
969
  const resp = await client.messages.create({
971
970
  model: options.model, max_tokens: MODELS[options.model].maxOutputTokens,
972
971
  messages: [
973
972
  ...options?.messages || [], buildClaudeMessage(content, options),
974
973
  ...options?.toolsResult || [],
975
- ], stream: true, ...reasoning ? {
974
+ ], stream: true,
975
+ ...options?.reasoning ?? MODELS[options.model]?.reasoning ? {
976
976
  thinking: options?.thinking || { type: 'enabled', budget_tokens: 1024 },
977
977
  } : {}, // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
978
978
  ...MODELS[options.model]?.tools ? {
@@ -980,57 +980,42 @@ const promptClaude = async (content, options = {}) => {
980
980
  tool_choice: { type: 'auto' },
981
981
  } : {},
982
982
  });
983
- let [event, text, thinking, signature, result, thinkEnd, tool_calls]
984
- = [null, '', '', '', options?.toolsResponse || '', '', []];
985
- for await (event of resp) {
986
- let [thkDelta, txtDelta] = [
987
- event?.content_block?.thinking || event?.delta?.thinking || '',
988
- event?.content_block?.text || event?.delta?.text || '',
989
- ];
983
+ let [event, text, thinking, signature, result, thinkEnd, tool_use]
984
+ = [null, '', '', '', options?.result ?? '', '', []];
985
+ for await (const chunk of resp) {
986
+ event = chunk?.content_block || chunk?.delta || {};
987
+ let [thkDelta, txtDelta] = [event.thinking || '', event.text || ''];
990
988
  text += txtDelta;
991
989
  thinking += thkDelta;
992
- signature = signature || event?.content_block?.signature || event?.delta?.signature || '';
993
- if (reasoning) {
994
- thkDelta && (thkDelta === thinking)
995
- && (thkDelta = `${THINK_STR}\n${thkDelta}`);
996
- thinking && txtDelta && !thinkEnd
997
- && (thinkEnd = thkDelta = `${thkDelta}\n${THINK_END}\n\n`);
990
+ signature = signature || event?.signature || '';
991
+ thkDelta && thkDelta === thinking
992
+ && (thkDelta = `${THINK_STR}\n${thkDelta}`);
993
+ thinking && txtDelta && !thinkEnd
994
+ && (thinkEnd = thkDelta = `${thkDelta}\n${THINK_END}\n\n`);
995
+ if (event?.type === 'tool_use') {
996
+ tool_use.push({ ...event, input: '' });
997
+ } else if (event.partial_json) {
998
+ tool_use[tool_use.length - 1].input += event.partial_json;
998
999
  }
999
- if (event?.content_block?.type === 'tool_use') {
1000
- tool_calls.push({ ...event?.content_block, input: '' });
1001
- } else if (event?.delta?.partial_json) {
1002
- tool_calls[tool_calls.length - 1].input += event?.delta?.partial_json;
1003
- }
1004
- const delta = thkDelta + txtDelta;
1005
- if (delta === '') { continue; }
1006
- result += delta;
1007
- event.content = [{ type: TEXT, text: options?.delta ? delta : result }];
1008
- await ignoreErrFunc(async () => await options?.stream?.(
1009
- await packGptResp(event, { ...options, processing: true })
1010
- ), LOG);
1000
+ txtDelta = thkDelta + txtDelta;
1001
+ result += txtDelta;
1002
+ await streamResp({ text: options?.delta ? txtDelta : result }, options);
1011
1003
  }
1012
- event.content = [{ type: TEXT, text }];
1013
- tool_calls.length && thinking
1014
- && event.content.unshift({ type: THINKING, thinking, signature });
1004
+ event = {
1005
+ role: assistant, content: [
1006
+ ...thinking ? [{ type: THINKING, thinking, signature }] : [],
1007
+ ...text ? [{ type: TEXT, text }] : [], ...tool_use,
1008
+ ]
1009
+ };
1015
1010
  const { toolsResult, toolsResponse } = await handleToolsCall(
1016
- { tool_calls }, { ...options, toolsResponse: result, flavor: CLAUDE },
1011
+ event, { ...options, result, flavor: CLAUDE },
1017
1012
  );
1018
- if (toolsResult.length && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1019
- toolsResult[0].content.unshift(
1020
- ...event?.content.filter(x => x?.type !== 'tool_use')
1021
- );
1013
+ if (tool_use.length && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1022
1014
  return await promptClaude(content, {
1023
- ...options, toolsResult, toolsResponse,
1015
+ ...options, toolsResult: [...options?.toolsResult || [], ...toolsResult], result: toolsResponse,
1024
1016
  });
1025
1017
  }
1026
- const textPart = event.content.find(x => x.type == TEXT);
1027
- textPart.text = [
1028
- ...options?.toolsResponse ? [options?.toolsResponse] : [],
1029
- textPart.text, ...toolsResult.length ? [
1030
- `⚠️ Tools recursion limit reached: ${MAX_TOOL_RECURSION}`
1031
- ] : [],
1032
- ].map(x => x.trim()).join('\n\n')
1033
- return packGptResp(event, options);
1018
+ return packGptResp({ text: mergeMsgs(toolsResponse, tool_use) }, options);
1034
1019
  };
1035
1020
 
1036
1021
  const uploadFile = async (input, options) => {
package/lib/manifest.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  const manifest = {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "1998.2.35",
4
+ "version": "1998.2.36",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "1998.2.35",
4
+ "version": "1998.2.36",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",