@saltcorn/large-language-model 0.9.11 → 1.0.0
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/constants.js +1 -3
- package/generate.js +263 -95
- package/index.js +44 -19
- package/package.json +12 -4
- package/tests/configs.js +34 -0
- package/tests/llm.test.js +200 -0
package/constants.js
CHANGED
package/generate.js
CHANGED
|
@@ -24,6 +24,8 @@ const {
|
|
|
24
24
|
} = require("ai");
|
|
25
25
|
const { createOpenAI } = require("@ai-sdk/openai");
|
|
26
26
|
const OpenAI = require("openai");
|
|
27
|
+
const { ElevenLabsClient } = require("@elevenlabs/elevenlabs-js");
|
|
28
|
+
|
|
27
29
|
let ollamaMod;
|
|
28
30
|
if (features.esm_plugins) ollamaMod = require("ollama");
|
|
29
31
|
|
|
@@ -36,7 +38,7 @@ const getEmbedding = async (config, opts) => {
|
|
|
36
38
|
apiKey: config.api_key,
|
|
37
39
|
embed_model: opts?.embed_model || config.embed_model || config.model,
|
|
38
40
|
},
|
|
39
|
-
opts
|
|
41
|
+
opts,
|
|
40
42
|
);
|
|
41
43
|
case "OpenAI":
|
|
42
44
|
return await getEmbeddingOpenAICompatible(
|
|
@@ -45,7 +47,7 @@ const getEmbedding = async (config, opts) => {
|
|
|
45
47
|
bearer: opts?.api_key || config.api_key,
|
|
46
48
|
embed_model: opts?.model || config.embed_model,
|
|
47
49
|
},
|
|
48
|
-
opts
|
|
50
|
+
opts,
|
|
49
51
|
);
|
|
50
52
|
case "OpenAI-compatible API":
|
|
51
53
|
return await getEmbeddingOpenAICompatible(
|
|
@@ -59,7 +61,7 @@ const getEmbedding = async (config, opts) => {
|
|
|
59
61
|
config.embed_model ||
|
|
60
62
|
config.model,
|
|
61
63
|
},
|
|
62
|
-
opts
|
|
64
|
+
opts,
|
|
63
65
|
);
|
|
64
66
|
case "Local Ollama":
|
|
65
67
|
if (config.embed_endpoint) {
|
|
@@ -72,14 +74,14 @@ const getEmbedding = async (config, opts) => {
|
|
|
72
74
|
config.embed_model ||
|
|
73
75
|
config.model,
|
|
74
76
|
},
|
|
75
|
-
opts
|
|
77
|
+
opts,
|
|
76
78
|
);
|
|
77
79
|
} else {
|
|
78
80
|
if (!ollamaMod) throw new Error("Not implemented for this backend");
|
|
79
81
|
|
|
80
82
|
const { Ollama } = ollamaMod;
|
|
81
83
|
const ollama = new Ollama(
|
|
82
|
-
config.ollama_host ? { host: config.ollama_host } : undefined
|
|
84
|
+
config.ollama_host ? { host: config.ollama_host } : undefined,
|
|
83
85
|
);
|
|
84
86
|
const olres = await ollama.embeddings({
|
|
85
87
|
model: opts?.model || config.embed_model || config.model,
|
|
@@ -110,7 +112,7 @@ const getImageGeneration = async (config, opts) => {
|
|
|
110
112
|
model: opts?.model || config.model,
|
|
111
113
|
responses_api: config.responses_api,
|
|
112
114
|
},
|
|
113
|
-
opts
|
|
115
|
+
opts,
|
|
114
116
|
);
|
|
115
117
|
default:
|
|
116
118
|
throw new Error("Image generation not implemented for this backend");
|
|
@@ -119,9 +121,22 @@ const getImageGeneration = async (config, opts) => {
|
|
|
119
121
|
|
|
120
122
|
const getAudioTranscription = async (
|
|
121
123
|
{ backend, apiKey, api_key, provider, ai_sdk_provider },
|
|
122
|
-
opts
|
|
124
|
+
opts,
|
|
123
125
|
) => {
|
|
124
|
-
switch (backend) {
|
|
126
|
+
switch (opts.backend || backend) {
|
|
127
|
+
case "ElevenLabs":
|
|
128
|
+
const transcription = await new ElevenLabsClient({
|
|
129
|
+
apiKey: opts?.api_key || api_key || apiKey,
|
|
130
|
+
}).speechToText.convert({
|
|
131
|
+
file: await (await File.findOne(opts.file)).get_contents(),
|
|
132
|
+
modelId: opts.model || "scribe_v2", // Model to use
|
|
133
|
+
tagAudioEvents: true, // Tag audio events like laughter, applause, etc.
|
|
134
|
+
languageCode: opts.languageCode || "eng", // Language of the audio file. If set to null, the model will detect the language automatically.
|
|
135
|
+
numSpeakers: opts.numSpeakers || null, // Language of the audio file. If set to null, the model will detect the language automatically.
|
|
136
|
+
diarize: !!opts.diarize, // Whether to annotate who is speaking
|
|
137
|
+
diarizationThreshold: opts.diarizationThreshold || null,
|
|
138
|
+
});
|
|
139
|
+
return transcription;
|
|
125
140
|
case "OpenAI":
|
|
126
141
|
const client = new OpenAI({
|
|
127
142
|
apiKey: opts?.api_key || api_key || apiKey,
|
|
@@ -129,10 +144,10 @@ const getAudioTranscription = async (
|
|
|
129
144
|
const fp = opts.file.location
|
|
130
145
|
? opts.file.location
|
|
131
146
|
: typeof opts.file === "string"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
? await (
|
|
148
|
+
await File.findOne(opts.file)
|
|
149
|
+
).location
|
|
150
|
+
: null;
|
|
136
151
|
const model = opts?.model || "whisper-1";
|
|
137
152
|
const diarize = model === "gpt-4o-transcribe-diarize";
|
|
138
153
|
const transcript1 = await client.audio.transcriptions.create({
|
|
@@ -156,8 +171,8 @@ const getAudioTranscription = async (
|
|
|
156
171
|
(Buffer.isBuffer(opts.file)
|
|
157
172
|
? opts.file
|
|
158
173
|
: typeof opts.file === "string"
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
? await (await File.findOne(opts.file)).get_contents()
|
|
175
|
+
: await opts.file.get_contents());
|
|
161
176
|
const extra = {};
|
|
162
177
|
if (opts.prompt)
|
|
163
178
|
extra.providerOptions = {
|
|
@@ -178,6 +193,104 @@ const getAudioTranscription = async (
|
|
|
178
193
|
}
|
|
179
194
|
};
|
|
180
195
|
|
|
196
|
+
const last = (xs) => xs[xs.length - 1];
|
|
197
|
+
|
|
198
|
+
const toolResponse = async (
|
|
199
|
+
{ backend, apiKey, api_key, provider, ai_sdk_provider, responses_api },
|
|
200
|
+
opts,
|
|
201
|
+
) => {
|
|
202
|
+
let chat = opts.chat;
|
|
203
|
+
let result = opts.prompt;
|
|
204
|
+
//console.log("chat", JSON.stringify(chat, null, 2));
|
|
205
|
+
switch (opts.backend || backend) {
|
|
206
|
+
case "OpenAI":
|
|
207
|
+
{
|
|
208
|
+
let tool_call_chat, tool_call;
|
|
209
|
+
if (!((opts.tool_call_id && opts.tool_name) || opts.tool_call)) {
|
|
210
|
+
if (opts.tool_call) tool_call_chat = opts.tool_call;
|
|
211
|
+
else
|
|
212
|
+
tool_call_chat = last(
|
|
213
|
+
chat.filter((c) => c.tool_calls || c.type === "function_call"),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
tool_call = tool_call_chat.tool_calls
|
|
217
|
+
? tool_call_chat.tool_calls[0] //original api
|
|
218
|
+
: tool_call_chat; //responses api
|
|
219
|
+
}
|
|
220
|
+
const content =
|
|
221
|
+
result && typeof result !== "string"
|
|
222
|
+
? JSON.stringify(result)
|
|
223
|
+
: result || "Action run";
|
|
224
|
+
const new_chat_item = responses_api
|
|
225
|
+
? {
|
|
226
|
+
type: "function_call_output",
|
|
227
|
+
call_id:
|
|
228
|
+
opts.tool_call?.tool_call_id ||
|
|
229
|
+
opts.tool_call_id ||
|
|
230
|
+
tool_call.call_id,
|
|
231
|
+
output: content,
|
|
232
|
+
}
|
|
233
|
+
: {
|
|
234
|
+
role: "tool",
|
|
235
|
+
tool_call_id:
|
|
236
|
+
opts.tool_call?.tool_call_id ||
|
|
237
|
+
opts.tool_call_id ||
|
|
238
|
+
tool_call.toolCallId ||
|
|
239
|
+
tool_call.id,
|
|
240
|
+
content,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
chat.push(new_chat_item);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case "AI SDK":
|
|
247
|
+
{
|
|
248
|
+
let tool_call, tc;
|
|
249
|
+
if (!((opts.tool_call_id && opts.tool_name) || opts.tool_call)) {
|
|
250
|
+
if (opts.tool_call) tool_call = opts.tool_call;
|
|
251
|
+
else
|
|
252
|
+
tool_call = last(
|
|
253
|
+
chat.filter(
|
|
254
|
+
(c) =>
|
|
255
|
+
c.role === "assistant" &&
|
|
256
|
+
Array.isArray(c.content) &&
|
|
257
|
+
c.content.some((cc) => cc.type === "tool-call"),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
tc = tool_call.content[0];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
chat.push({
|
|
265
|
+
role: "tool",
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "tool-result",
|
|
269
|
+
toolCallId:
|
|
270
|
+
opts.tool_call?.tool_call_id ||
|
|
271
|
+
opts.tool_call_id ||
|
|
272
|
+
tc.toolCallId,
|
|
273
|
+
toolName:
|
|
274
|
+
opts.tool_call?.tool_name || opts.tool_name || tc.toolName,
|
|
275
|
+
output:
|
|
276
|
+
!result || typeof result === "string"
|
|
277
|
+
? {
|
|
278
|
+
type: "text",
|
|
279
|
+
value: result || "Action run",
|
|
280
|
+
}
|
|
281
|
+
: {
|
|
282
|
+
type: "json",
|
|
283
|
+
value: JSON.parse(JSON.stringify(result)),
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
181
294
|
const getCompletion = async (config, opts) => {
|
|
182
295
|
switch (config.backend) {
|
|
183
296
|
case "AI SDK":
|
|
@@ -187,7 +300,7 @@ const getCompletion = async (config, opts) => {
|
|
|
187
300
|
apiKey: config.api_key,
|
|
188
301
|
model: opts?.model || config.model,
|
|
189
302
|
},
|
|
190
|
-
opts
|
|
303
|
+
opts,
|
|
191
304
|
);
|
|
192
305
|
case "OpenAI":
|
|
193
306
|
return await getCompletionOpenAICompatible(
|
|
@@ -199,7 +312,7 @@ const getCompletion = async (config, opts) => {
|
|
|
199
312
|
model: opts?.model || config.model,
|
|
200
313
|
responses_api: config.responses_api,
|
|
201
314
|
},
|
|
202
|
-
opts
|
|
315
|
+
opts,
|
|
203
316
|
);
|
|
204
317
|
case "OpenAI-compatible API":
|
|
205
318
|
return await getCompletionOpenAICompatible(
|
|
@@ -213,7 +326,7 @@ const getCompletion = async (config, opts) => {
|
|
|
213
326
|
apiKey: opts?.api_key || config.api_key,
|
|
214
327
|
model: opts?.model || config.model,
|
|
215
328
|
},
|
|
216
|
-
opts
|
|
329
|
+
opts,
|
|
217
330
|
);
|
|
218
331
|
case "Local Ollama":
|
|
219
332
|
return await getCompletionOpenAICompatible(
|
|
@@ -223,14 +336,14 @@ const getCompletion = async (config, opts) => {
|
|
|
223
336
|
: "http://localhost:11434/v1/chat/completions",
|
|
224
337
|
model: opts?.model || config.model,
|
|
225
338
|
},
|
|
226
|
-
opts
|
|
339
|
+
opts,
|
|
227
340
|
);
|
|
228
341
|
case "Local llama.cpp":
|
|
229
342
|
//TODO only check if unsafe plugins not allowed
|
|
230
343
|
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
231
344
|
if (!isRoot)
|
|
232
345
|
throw new Error(
|
|
233
|
-
"llama.cpp inference is not permitted on subdomain tenants"
|
|
346
|
+
"llama.cpp inference is not permitted on subdomain tenants",
|
|
234
347
|
);
|
|
235
348
|
let hyperStr = "";
|
|
236
349
|
if (opts.temperature) hyperStr += ` --temp ${opts.temperature}`;
|
|
@@ -240,7 +353,7 @@ const getCompletion = async (config, opts) => {
|
|
|
240
353
|
|
|
241
354
|
const { stdout, stderr } = await exec(
|
|
242
355
|
`./main -m ${config.model_path} -p "${opts.prompt}" ${nstr}${hyperStr}`,
|
|
243
|
-
{ cwd: config.llama_dir }
|
|
356
|
+
{ cwd: config.llama_dir },
|
|
244
357
|
);
|
|
245
358
|
return stdout;
|
|
246
359
|
case "Google Vertex AI":
|
|
@@ -271,12 +384,13 @@ const getCompletionAISDK = async (
|
|
|
271
384
|
systemPrompt,
|
|
272
385
|
prompt,
|
|
273
386
|
debugResult,
|
|
387
|
+
appendToChat,
|
|
274
388
|
debugCollector,
|
|
275
389
|
chat = [],
|
|
276
390
|
api_key,
|
|
277
391
|
endpoint,
|
|
278
392
|
...rest
|
|
279
|
-
}
|
|
393
|
+
},
|
|
280
394
|
) => {
|
|
281
395
|
const use_model_name = rest.model || model;
|
|
282
396
|
let model_obj = getAiSdkModel({
|
|
@@ -298,7 +412,7 @@ const getCompletionAISDK = async (
|
|
|
298
412
|
...(Array.isArray(chat.content) ? { content: chat.content.map(f) } : {}),
|
|
299
413
|
};
|
|
300
414
|
};
|
|
301
|
-
const newChat = chat.map(modifyChat);
|
|
415
|
+
const newChat = appendToChat ? chat : chat.map(modifyChat);
|
|
302
416
|
|
|
303
417
|
const body = {
|
|
304
418
|
...rest,
|
|
@@ -312,6 +426,9 @@ const getCompletionAISDK = async (
|
|
|
312
426
|
...(prompt ? [{ role: "user", content: prompt }] : []),
|
|
313
427
|
],
|
|
314
428
|
};
|
|
429
|
+
if (appendToChat && chat && prompt) {
|
|
430
|
+
chat.push({ role: "user", content: prompt });
|
|
431
|
+
}
|
|
315
432
|
if (rest.temperature || temperature) {
|
|
316
433
|
const str_or_num = rest.temperature || temperature;
|
|
317
434
|
body.temperature = +str_or_num;
|
|
@@ -327,6 +444,9 @@ const getCompletionAISDK = async (
|
|
|
327
444
|
"gpt-5",
|
|
328
445
|
"gpt-5-nano",
|
|
329
446
|
"gpt-5-mini",
|
|
447
|
+
"gpt-5.1",
|
|
448
|
+
"gpt-5.1-codex",
|
|
449
|
+
"gpt-5.2",
|
|
330
450
|
].includes(use_model_name)
|
|
331
451
|
)
|
|
332
452
|
body.temperature = 0.7;
|
|
@@ -335,9 +455,9 @@ const getCompletionAISDK = async (
|
|
|
335
455
|
const prevTools = [...body.tools];
|
|
336
456
|
body.tools = {};
|
|
337
457
|
prevTools.forEach((t) => {
|
|
338
|
-
body.tools[t.function.name] = tool({
|
|
339
|
-
description: t.function.description,
|
|
340
|
-
inputSchema: jsonSchema(t.function.parameters),
|
|
458
|
+
body.tools[t.name || t.function.name] = tool({
|
|
459
|
+
description: t.description || t.function.description,
|
|
460
|
+
inputSchema: jsonSchema(t.parameters || t.function.parameters),
|
|
341
461
|
});
|
|
342
462
|
});
|
|
343
463
|
}
|
|
@@ -352,11 +472,21 @@ const getCompletionAISDK = async (
|
|
|
352
472
|
let results;
|
|
353
473
|
if (rest.streamCallback) {
|
|
354
474
|
delete body.streamCallback;
|
|
355
|
-
|
|
356
|
-
for await (const textPart of
|
|
475
|
+
const results1 = await streamText(body);
|
|
476
|
+
for await (const textPart of results1.textStream) {
|
|
357
477
|
rest.streamCallback(textPart);
|
|
358
478
|
}
|
|
479
|
+
results = {
|
|
480
|
+
response: await results1.response,
|
|
481
|
+
text: await results1.text,
|
|
482
|
+
steps: await results1.steps,
|
|
483
|
+
};
|
|
359
484
|
} else results = await generateText(body);
|
|
485
|
+
|
|
486
|
+
if (appendToChat && chat) {
|
|
487
|
+
chat.push(...results.response.messages);
|
|
488
|
+
}
|
|
489
|
+
|
|
360
490
|
if (debugResult)
|
|
361
491
|
console.log("AI SDK response", JSON.stringify(results, null, 2));
|
|
362
492
|
else getState().log(6, `AI SDK response ${JSON.stringify(results)}`);
|
|
@@ -372,6 +502,14 @@ const getCompletionAISDK = async (
|
|
|
372
502
|
content: await results.text,
|
|
373
503
|
messages: (await results.response).messages,
|
|
374
504
|
ai_sdk: true,
|
|
505
|
+
hasToolCalls: allToolCalls.length,
|
|
506
|
+
getToolCalls() {
|
|
507
|
+
return allToolCalls.map((tc) => ({
|
|
508
|
+
tool_name: tc.toolName,
|
|
509
|
+
input: tc.input,
|
|
510
|
+
tool_call_id: tc.toolCallId,
|
|
511
|
+
}));
|
|
512
|
+
},
|
|
375
513
|
};
|
|
376
514
|
} else return results.text;
|
|
377
515
|
};
|
|
@@ -384,10 +522,11 @@ const getCompletionOpenAICompatible = async (
|
|
|
384
522
|
debugResult,
|
|
385
523
|
debugCollector,
|
|
386
524
|
chat = [],
|
|
525
|
+
appendToChat,
|
|
387
526
|
api_key,
|
|
388
527
|
endpoint,
|
|
389
528
|
...rest
|
|
390
|
-
}
|
|
529
|
+
},
|
|
391
530
|
) => {
|
|
392
531
|
const headers = {
|
|
393
532
|
"Content-Type": "application/json",
|
|
@@ -425,63 +564,67 @@ const getCompletionOpenAICompatible = async (
|
|
|
425
564
|
delete body.streamCallback;
|
|
426
565
|
}
|
|
427
566
|
if (responses_api) {
|
|
567
|
+
delete body.tool_choice;
|
|
428
568
|
for (const tool of body.tools || []) {
|
|
429
|
-
if (tool.type !== "function") continue;
|
|
569
|
+
if (tool.type !== "function" || !tool.function) continue;
|
|
430
570
|
tool.name = tool.function.name;
|
|
431
571
|
tool.description = tool.function.description;
|
|
432
572
|
tool.parameters = tool.function.parameters;
|
|
433
573
|
if (tool.function.required) tool.required = tool.function.required;
|
|
434
574
|
delete tool.function;
|
|
435
575
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
576
|
+
let newChat;
|
|
577
|
+
if (!appendToChat) {
|
|
578
|
+
newChat = [];
|
|
579
|
+
(chat || []).forEach((c) => {
|
|
580
|
+
if (c.tool_calls) {
|
|
581
|
+
c.tool_calls.forEach((tc) => {
|
|
582
|
+
newChat.push({
|
|
583
|
+
id: tc.id,
|
|
584
|
+
type: "function_call",
|
|
585
|
+
call_id: tc.call_id,
|
|
586
|
+
name: tc.name,
|
|
587
|
+
arguments: tc.arguments,
|
|
588
|
+
});
|
|
446
589
|
});
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
|
|
590
|
+
} else if (c.content?.image_calls) {
|
|
591
|
+
c.content.image_calls.forEach((ic) => {
|
|
592
|
+
newChat.push({
|
|
593
|
+
...ic,
|
|
594
|
+
result: undefined,
|
|
595
|
+
filename: undefined,
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
} else if (c.content?.mcp_calls) {
|
|
599
|
+
c.content.mcp_calls.forEach((ic) => {
|
|
600
|
+
newChat.push({
|
|
601
|
+
...ic,
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
} else if (c.role === "tool") {
|
|
450
605
|
newChat.push({
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
606
|
+
type: "function_call_output",
|
|
607
|
+
call_id: c.call_id,
|
|
608
|
+
output: c.content,
|
|
454
609
|
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
610
|
+
} else {
|
|
611
|
+
const fcontent = (c) => {
|
|
612
|
+
if (c.type === "image_url")
|
|
613
|
+
return {
|
|
614
|
+
type: "input_image",
|
|
615
|
+
image_url: c.image_url.url,
|
|
616
|
+
};
|
|
617
|
+
else return c;
|
|
618
|
+
};
|
|
458
619
|
newChat.push({
|
|
459
|
-
...
|
|
620
|
+
...c,
|
|
621
|
+
content: Array.isArray(c.content)
|
|
622
|
+
? c.content.map(fcontent)
|
|
623
|
+
: c.content,
|
|
460
624
|
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
type: "function_call_output",
|
|
465
|
-
call_id: c.call_id,
|
|
466
|
-
output: c.content,
|
|
467
|
-
});
|
|
468
|
-
} else {
|
|
469
|
-
const fcontent = (c) => {
|
|
470
|
-
if (c.type === "image_url")
|
|
471
|
-
return {
|
|
472
|
-
type: "input_image",
|
|
473
|
-
image_url: c.image_url.url,
|
|
474
|
-
};
|
|
475
|
-
else return c;
|
|
476
|
-
};
|
|
477
|
-
newChat.push({
|
|
478
|
-
...c,
|
|
479
|
-
content: Array.isArray(c.content)
|
|
480
|
-
? c.content.map(fcontent)
|
|
481
|
-
: c.content,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
});
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
} else newChat = chat;
|
|
485
628
|
body.input = [
|
|
486
629
|
{
|
|
487
630
|
role: "system",
|
|
@@ -502,6 +645,9 @@ const getCompletionOpenAICompatible = async (
|
|
|
502
645
|
...(prompt ? [{ role: "user", content: prompt }] : []),
|
|
503
646
|
];
|
|
504
647
|
}
|
|
648
|
+
if (appendToChat && chat && prompt) {
|
|
649
|
+
chat.push({ role: "user", content: prompt });
|
|
650
|
+
}
|
|
505
651
|
if (debugResult)
|
|
506
652
|
console.log(
|
|
507
653
|
"OpenAI request",
|
|
@@ -509,14 +655,14 @@ const getCompletionOpenAICompatible = async (
|
|
|
509
655
|
"to",
|
|
510
656
|
chatCompleteEndpoint,
|
|
511
657
|
"headers",
|
|
512
|
-
JSON.stringify(headers)
|
|
658
|
+
JSON.stringify(headers),
|
|
513
659
|
);
|
|
514
660
|
else
|
|
515
661
|
getState().log(
|
|
516
662
|
6,
|
|
517
663
|
`OpenAI request ${JSON.stringify(
|
|
518
|
-
body
|
|
519
|
-
)} to ${chatCompleteEndpoint} headers ${JSON.stringify(headers)}
|
|
664
|
+
body,
|
|
665
|
+
)} to ${chatCompleteEndpoint} headers ${JSON.stringify(headers)}`,
|
|
520
666
|
);
|
|
521
667
|
if (debugCollector) debugCollector.request = body;
|
|
522
668
|
const reqTimeStart = Date.now();
|
|
@@ -608,7 +754,7 @@ const getCompletionOpenAICompatible = async (
|
|
|
608
754
|
: streamParts.join("");
|
|
609
755
|
}
|
|
610
756
|
const results = await rawResponse.json();
|
|
611
|
-
|
|
757
|
+
|
|
612
758
|
if (debugResult)
|
|
613
759
|
console.log("OpenAI response", JSON.stringify(results, null, 2));
|
|
614
760
|
else getState().log(6, `OpenAI response ${JSON.stringify(results)}`);
|
|
@@ -618,36 +764,49 @@ const getCompletionOpenAICompatible = async (
|
|
|
618
764
|
}
|
|
619
765
|
|
|
620
766
|
if (results.error) throw new Error(`OpenAI error: ${results.error.message}`);
|
|
767
|
+
if (appendToChat && chat) {
|
|
768
|
+
if (responses_api) chat.push(...results.output);
|
|
769
|
+
else chat.push(results.choices[0].message);
|
|
770
|
+
}
|
|
621
771
|
if (responses_api) {
|
|
622
772
|
const textOutput = results.output
|
|
623
773
|
.filter((o) => o.type === "message")
|
|
624
774
|
.map((o) => o.content.map((c) => c.text).join(""))
|
|
625
775
|
.join("");
|
|
776
|
+
const tool_calls = emptyToUndefined(
|
|
777
|
+
results.output
|
|
778
|
+
.filter((o) => o.type === "function_call")
|
|
779
|
+
.map((o) => ({
|
|
780
|
+
function: { name: o.name, arguments: o.arguments },
|
|
781
|
+
...o,
|
|
782
|
+
})),
|
|
783
|
+
);
|
|
626
784
|
return results.output.some(
|
|
627
785
|
(o) =>
|
|
628
786
|
o.type === "function_call" ||
|
|
629
787
|
o.type === "image_generation_call" ||
|
|
630
788
|
o.type === "mcp_list_tools" ||
|
|
631
|
-
o.type === "mcp_call"
|
|
789
|
+
o.type === "mcp_call",
|
|
632
790
|
)
|
|
633
791
|
? {
|
|
634
|
-
tool_calls
|
|
635
|
-
results.output
|
|
636
|
-
.filter((o) => o.type === "function_call")
|
|
637
|
-
.map((o) => ({
|
|
638
|
-
function: { name: o.name, arguments: o.arguments },
|
|
639
|
-
...o,
|
|
640
|
-
}))
|
|
641
|
-
),
|
|
792
|
+
tool_calls,
|
|
642
793
|
image_calls: emptyToUndefined(
|
|
643
|
-
results.output.filter((o) => o.type === "image_generation_call")
|
|
794
|
+
results.output.filter((o) => o.type === "image_generation_call"),
|
|
644
795
|
),
|
|
645
796
|
mcp_calls: emptyToUndefined(
|
|
646
797
|
results.output.filter(
|
|
647
|
-
(o) => o.type === "mcp_call" || o.type === "mcp_list_tools"
|
|
648
|
-
)
|
|
798
|
+
(o) => o.type === "mcp_call" || o.type === "mcp_list_tools",
|
|
799
|
+
),
|
|
649
800
|
),
|
|
650
801
|
content: textOutput || null,
|
|
802
|
+
hasToolCalls: tool_calls?.length,
|
|
803
|
+
getToolCalls() {
|
|
804
|
+
return tool_calls.map((tc) => ({
|
|
805
|
+
tool_name: tc.function.name,
|
|
806
|
+
input: JSON.parse(tc.function.arguments),
|
|
807
|
+
tool_call_id: tc.call_id,
|
|
808
|
+
}));
|
|
809
|
+
},
|
|
651
810
|
}
|
|
652
811
|
: textOutput || null;
|
|
653
812
|
} else
|
|
@@ -655,6 +814,14 @@ const getCompletionOpenAICompatible = async (
|
|
|
655
814
|
? {
|
|
656
815
|
tool_calls: results?.choices?.[0]?.message?.tool_calls,
|
|
657
816
|
content: results?.choices?.[0]?.message?.content || null,
|
|
817
|
+
hasToolCalls: results?.choices?.[0]?.message?.tool_calls.length,
|
|
818
|
+
getToolCalls() {
|
|
819
|
+
return results?.choices?.[0]?.message?.tool_calls.map((tc) => ({
|
|
820
|
+
tool_name: tc.function.name,
|
|
821
|
+
input: JSON.parse(tc.function.arguments),
|
|
822
|
+
tool_call_id: tc.id,
|
|
823
|
+
}));
|
|
824
|
+
},
|
|
658
825
|
}
|
|
659
826
|
: results?.choices?.[0]?.message?.content || null;
|
|
660
827
|
};
|
|
@@ -673,7 +840,7 @@ const getImageGenOpenAICompatible = async (
|
|
|
673
840
|
n,
|
|
674
841
|
output_format,
|
|
675
842
|
response_format,
|
|
676
|
-
}
|
|
843
|
+
},
|
|
677
844
|
) => {
|
|
678
845
|
const { imageEndpoint, bearer, apiKey, image_model } = config;
|
|
679
846
|
const headers = {
|
|
@@ -710,7 +877,7 @@ const getImageGenOpenAICompatible = async (
|
|
|
710
877
|
|
|
711
878
|
const getEmbeddingOpenAICompatible = async (
|
|
712
879
|
config,
|
|
713
|
-
{ prompt, model, debugResult }
|
|
880
|
+
{ prompt, model, debugResult },
|
|
714
881
|
) => {
|
|
715
882
|
const { embeddingsEndpoint, bearer, apiKey, embed_model } = config;
|
|
716
883
|
const headers = {
|
|
@@ -747,7 +914,7 @@ const getEmbeddingAISDK = async (config, { prompt, model, debugResult }) => {
|
|
|
747
914
|
case "OpenAI":
|
|
748
915
|
const openai = createOpenAI({ apiKey: apiKey });
|
|
749
916
|
model_obj = openai.textEmbeddingModel(
|
|
750
|
-
model_name || "text-embedding-3-small"
|
|
917
|
+
model_name || "text-embedding-3-small",
|
|
751
918
|
);
|
|
752
919
|
//providerOptions.openai = {};
|
|
753
920
|
break;
|
|
@@ -800,7 +967,7 @@ const initOAuth2Client = async (config) => {
|
|
|
800
967
|
const oauth2Client = new google.auth.OAuth2(
|
|
801
968
|
client_id,
|
|
802
969
|
client_secret,
|
|
803
|
-
redirect_uri
|
|
970
|
+
redirect_uri,
|
|
804
971
|
);
|
|
805
972
|
oauth2Client.setCredentials(pluginCfg.tokens);
|
|
806
973
|
return oauth2Client;
|
|
@@ -868,7 +1035,7 @@ const getCompletionGoogleVertex = async (config, opts, oauth2Client) => {
|
|
|
868
1035
|
chatParams.tools = [
|
|
869
1036
|
{
|
|
870
1037
|
functionDeclarations: opts.tools.map((t) =>
|
|
871
|
-
prepFuncArgsForChat(t.function)
|
|
1038
|
+
prepFuncArgsForChat(t.function),
|
|
872
1039
|
),
|
|
873
1040
|
},
|
|
874
1041
|
];
|
|
@@ -910,7 +1077,7 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
|
|
|
910
1077
|
helpers.toValue({
|
|
911
1078
|
content: p,
|
|
912
1079
|
task_type: config.task_type || "RETRIEVAL_QUERY",
|
|
913
|
-
})
|
|
1080
|
+
}),
|
|
914
1081
|
);
|
|
915
1082
|
} else {
|
|
916
1083
|
instances = [
|
|
@@ -942,4 +1109,5 @@ module.exports = {
|
|
|
942
1109
|
getEmbedding,
|
|
943
1110
|
getImageGeneration,
|
|
944
1111
|
getAudioTranscription,
|
|
1112
|
+
toolResponse,
|
|
945
1113
|
};
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
getEmbedding,
|
|
12
12
|
getImageGeneration,
|
|
13
13
|
getAudioTranscription,
|
|
14
|
+
toolResponse
|
|
14
15
|
} = require("./generate");
|
|
15
16
|
const { OPENAI_MODELS } = require("./constants.js");
|
|
16
17
|
const { eval_expression } = require("@saltcorn/data/models/expression");
|
|
@@ -381,7 +382,10 @@ const functions = (config) => {
|
|
|
381
382
|
},
|
|
382
383
|
isAsync: true,
|
|
383
384
|
description: "Generate text with GPT",
|
|
384
|
-
arguments: [
|
|
385
|
+
arguments: [
|
|
386
|
+
{ name: "prompt", type: "String", required: true },
|
|
387
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
388
|
+
],
|
|
385
389
|
},
|
|
386
390
|
llm_image_generate: {
|
|
387
391
|
run: async (prompt, opts) => {
|
|
@@ -390,7 +394,10 @@ const functions = (config) => {
|
|
|
390
394
|
},
|
|
391
395
|
isAsync: true,
|
|
392
396
|
description: "Generate image",
|
|
393
|
-
arguments: [
|
|
397
|
+
arguments: [
|
|
398
|
+
{ name: "prompt", type: "String", required: true },
|
|
399
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
400
|
+
],
|
|
394
401
|
},
|
|
395
402
|
llm_embedding: {
|
|
396
403
|
run: async (prompt, opts) => {
|
|
@@ -399,7 +406,10 @@ const functions = (config) => {
|
|
|
399
406
|
},
|
|
400
407
|
isAsync: true,
|
|
401
408
|
description: "Get vector embedding",
|
|
402
|
-
arguments: [
|
|
409
|
+
arguments: [
|
|
410
|
+
{ name: "prompt", type: "String", required: true },
|
|
411
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
412
|
+
],
|
|
403
413
|
},
|
|
404
414
|
llm_transcribe: {
|
|
405
415
|
run: async (opts) => {
|
|
@@ -408,7 +418,21 @@ const functions = (config) => {
|
|
|
408
418
|
},
|
|
409
419
|
isAsync: true,
|
|
410
420
|
description: "Get vector embedding",
|
|
411
|
-
arguments: [
|
|
421
|
+
arguments: [
|
|
422
|
+
{ name: "options", type: "JSON", tstype: "any", required: true },
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
llm_add_tool_response: {
|
|
426
|
+
run: async (prompt, opts) => {
|
|
427
|
+
const result = await toolResponse(config, { prompt, ...opts });
|
|
428
|
+
return result;
|
|
429
|
+
},
|
|
430
|
+
isAsync: true,
|
|
431
|
+
description: "Insert the response to a tool call into a chat",
|
|
432
|
+
arguments: [
|
|
433
|
+
{ name: "prompt", type: "String", required: true },
|
|
434
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
435
|
+
],
|
|
412
436
|
},
|
|
413
437
|
};
|
|
414
438
|
};
|
|
@@ -432,7 +456,7 @@ const routes = (config) => {
|
|
|
432
456
|
const oauth2Client = new google.auth.OAuth2(
|
|
433
457
|
client_id,
|
|
434
458
|
client_secret,
|
|
435
|
-
redirect_uri
|
|
459
|
+
redirect_uri,
|
|
436
460
|
);
|
|
437
461
|
const authUrl = oauth2Client.generateAuthUrl({
|
|
438
462
|
access_type: "offline",
|
|
@@ -459,7 +483,7 @@ const routes = (config) => {
|
|
|
459
483
|
const oauth2Client = new google.auth.OAuth2(
|
|
460
484
|
client_id,
|
|
461
485
|
client_secret,
|
|
462
|
-
redirect_uri
|
|
486
|
+
redirect_uri,
|
|
463
487
|
);
|
|
464
488
|
let plugin = await Plugin.findOne({ name: "large-language-model" });
|
|
465
489
|
if (!plugin) {
|
|
@@ -475,8 +499,8 @@ const routes = (config) => {
|
|
|
475
499
|
req.flash(
|
|
476
500
|
"warning",
|
|
477
501
|
req.__(
|
|
478
|
-
"No refresh token received. Please revoke the plugin's access and try again."
|
|
479
|
-
)
|
|
502
|
+
"No refresh token received. Please revoke the plugin's access and try again.",
|
|
503
|
+
),
|
|
480
504
|
);
|
|
481
505
|
} else {
|
|
482
506
|
const newConfig = { ...(plugin.configuration || {}), tokens };
|
|
@@ -488,7 +512,7 @@ const routes = (config) => {
|
|
|
488
512
|
});
|
|
489
513
|
req.flash(
|
|
490
514
|
"success",
|
|
491
|
-
req.__("Authentication successful! You can now use Vertex AI.")
|
|
515
|
+
req.__("Authentication successful! You can now use Vertex AI."),
|
|
492
516
|
);
|
|
493
517
|
}
|
|
494
518
|
} catch (error) {
|
|
@@ -615,13 +639,13 @@ module.exports = {
|
|
|
615
639
|
prompt_formula,
|
|
616
640
|
row,
|
|
617
641
|
user,
|
|
618
|
-
"llm_generate prompt formula"
|
|
642
|
+
"llm_generate prompt formula",
|
|
619
643
|
);
|
|
620
644
|
else prompt = row[prompt_field];
|
|
621
645
|
const opts = {};
|
|
622
646
|
if (override_config) {
|
|
623
647
|
const altcfg = config.altconfigs.find(
|
|
624
|
-
(c) => c.name === override_config
|
|
648
|
+
(c) => c.name === override_config,
|
|
625
649
|
);
|
|
626
650
|
opts.endpoint = altcfg.endpoint;
|
|
627
651
|
opts.model = altcfg.model;
|
|
@@ -679,7 +703,8 @@ module.exports = {
|
|
|
679
703
|
{
|
|
680
704
|
name: "answer_field",
|
|
681
705
|
label: "Response variable",
|
|
682
|
-
sublabel:
|
|
706
|
+
sublabel:
|
|
707
|
+
"Set the generated response object to this context variable. The subfield <code>text</code> holds the string transcription",
|
|
683
708
|
type: "String",
|
|
684
709
|
required: true,
|
|
685
710
|
},
|
|
@@ -766,7 +791,7 @@ module.exports = {
|
|
|
766
791
|
else
|
|
767
792
|
await table.updateRow(
|
|
768
793
|
{ [answer_field]: ans.text },
|
|
769
|
-
row[table.pk_name]
|
|
794
|
+
row[table.pk_name],
|
|
770
795
|
);
|
|
771
796
|
},
|
|
772
797
|
},
|
|
@@ -879,7 +904,7 @@ module.exports = {
|
|
|
879
904
|
prompt_formula,
|
|
880
905
|
row,
|
|
881
906
|
user,
|
|
882
|
-
"llm_generate prompt formula"
|
|
907
|
+
"llm_generate prompt formula",
|
|
883
908
|
);
|
|
884
909
|
else prompt = row[prompt_field];
|
|
885
910
|
|
|
@@ -906,7 +931,7 @@ module.exports = {
|
|
|
906
931
|
"image/png",
|
|
907
932
|
imgContents,
|
|
908
933
|
user?.id,
|
|
909
|
-
min_role || 1
|
|
934
|
+
min_role || 1,
|
|
910
935
|
);
|
|
911
936
|
upd[answer_field] = file.path_to_serve;
|
|
912
937
|
}
|
|
@@ -998,7 +1023,7 @@ module.exports = {
|
|
|
998
1023
|
sublabel:
|
|
999
1024
|
"Use this context variable to store the chat history for subsequent prompts",
|
|
1000
1025
|
type: "String",
|
|
1001
|
-
}
|
|
1026
|
+
},
|
|
1002
1027
|
);
|
|
1003
1028
|
} else if (table) {
|
|
1004
1029
|
const jsonFields = table.fields
|
|
@@ -1022,7 +1047,7 @@ module.exports = {
|
|
|
1022
1047
|
type: "String",
|
|
1023
1048
|
required: true,
|
|
1024
1049
|
attributes: { options: jsonFields },
|
|
1025
|
-
}
|
|
1050
|
+
},
|
|
1026
1051
|
);
|
|
1027
1052
|
}
|
|
1028
1053
|
|
|
@@ -1051,7 +1076,7 @@ module.exports = {
|
|
|
1051
1076
|
input_type: "section_header",
|
|
1052
1077
|
label: "JSON fields to generate",
|
|
1053
1078
|
},
|
|
1054
|
-
fieldsField
|
|
1079
|
+
fieldsField,
|
|
1055
1080
|
);
|
|
1056
1081
|
return cfgFields;
|
|
1057
1082
|
},
|
|
@@ -1077,7 +1102,7 @@ module.exports = {
|
|
|
1077
1102
|
if (model) opts.model = model;
|
|
1078
1103
|
if (override_config) {
|
|
1079
1104
|
const altcfg = config.altconfigs.find(
|
|
1080
|
-
(c) => c.name === override_config
|
|
1105
|
+
(c) => c.name === override_config,
|
|
1081
1106
|
);
|
|
1082
1107
|
opts.endpoint = altcfg.endpoint;
|
|
1083
1108
|
opts.model = altcfg.model;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/large-language-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Large language models and functionality for Saltcorn",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"googleapis": "^144.0.0",
|
|
14
14
|
"ai": "5.0.44",
|
|
15
15
|
"@ai-sdk/openai": "2.0.30",
|
|
16
|
-
"openai": "6.16.0"
|
|
16
|
+
"openai": "6.16.0",
|
|
17
|
+
"@elevenlabs/elevenlabs-js": "2.31.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"jest": "^29.7.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "jest tests --runInBand"
|
|
17
24
|
},
|
|
18
25
|
"author": "Tom Nielsen",
|
|
19
26
|
"license": "MIT",
|
|
@@ -21,11 +28,12 @@
|
|
|
21
28
|
"eslintConfig": {
|
|
22
29
|
"extends": "eslint:recommended",
|
|
23
30
|
"parserOptions": {
|
|
24
|
-
"ecmaVersion":
|
|
31
|
+
"ecmaVersion": 2024
|
|
25
32
|
},
|
|
26
33
|
"env": {
|
|
27
34
|
"node": true,
|
|
28
|
-
"es6": true
|
|
35
|
+
"es6": true,
|
|
36
|
+
"jest/globals": true
|
|
29
37
|
},
|
|
30
38
|
"rules": {
|
|
31
39
|
"no-unused-vars": "off",
|
package/tests/configs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module.exports = [
|
|
2
|
+
{
|
|
3
|
+
name: "OpenAI completions",
|
|
4
|
+
model: "gpt-5.1",
|
|
5
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
6
|
+
backend: "OpenAI",
|
|
7
|
+
embed_model: "text-embedding-3-small",
|
|
8
|
+
image_model: "gpt-image-1",
|
|
9
|
+
temperature: 0.7,
|
|
10
|
+
responses_api: false,
|
|
11
|
+
ai_sdk_provider: "OpenAI",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "OpenAI responses",
|
|
15
|
+
model: "gpt-5.1",
|
|
16
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
17
|
+
backend: "OpenAI",
|
|
18
|
+
embed_model: "text-embedding-3-small",
|
|
19
|
+
image_model: "gpt-image-1",
|
|
20
|
+
temperature: 0.7,
|
|
21
|
+
responses_api: true,
|
|
22
|
+
ai_sdk_provider: "OpenAI",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "AI SDK OpenAI",
|
|
26
|
+
model: "gpt-5.1",
|
|
27
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
28
|
+
backend: "AI SDK",
|
|
29
|
+
embed_model: "text-embedding-3-small",
|
|
30
|
+
image_model: "gpt-image-1",
|
|
31
|
+
temperature: 0.7,
|
|
32
|
+
ai_sdk_provider: "OpenAI",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const View = require("@saltcorn/data/models/view");
|
|
3
|
+
const Table = require("@saltcorn/data/models/table");
|
|
4
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
5
|
+
|
|
6
|
+
const { mockReqRes } = require("@saltcorn/data/tests/mocks");
|
|
7
|
+
const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals");
|
|
8
|
+
|
|
9
|
+
afterAll(require("@saltcorn/data/db").close);
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await require("@saltcorn/data/db/reset_schema")();
|
|
12
|
+
await require("@saltcorn/data/db/fixtures")();
|
|
13
|
+
|
|
14
|
+
getState().registerPlugin("base", require("@saltcorn/data/base-plugin"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// run with:
|
|
18
|
+
// saltcorn dev:plugin-test -d ~/large-language-model/
|
|
19
|
+
|
|
20
|
+
jest.setTimeout(30000);
|
|
21
|
+
|
|
22
|
+
for (const nameconfig of require("./configs")) {
|
|
23
|
+
const { name, ...config } = nameconfig;
|
|
24
|
+
describe("llm_generate function with " + name, () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
getState().registerPlugin(
|
|
27
|
+
"@saltcorn/large-language-model",
|
|
28
|
+
require(".."),
|
|
29
|
+
config,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("generates text", async () => {
|
|
34
|
+
const answer = await getState().functions.llm_generate.run(
|
|
35
|
+
"What is the Capital of France?",
|
|
36
|
+
);
|
|
37
|
+
//console.log({ answer });
|
|
38
|
+
|
|
39
|
+
expect(typeof answer).toBe("string");
|
|
40
|
+
expect(answer).toContain("Paris");
|
|
41
|
+
});
|
|
42
|
+
it("generates text with system prompt", async () => {
|
|
43
|
+
const answer = await getState().functions.llm_generate.run(
|
|
44
|
+
"What is the name of the last week day in a normal work week?",
|
|
45
|
+
{
|
|
46
|
+
systemPrompt: "Answer in German, even when questions are in English",
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
//console.log({ answer });
|
|
50
|
+
|
|
51
|
+
expect(typeof answer).toBe("string");
|
|
52
|
+
expect(answer).toContain("Freitag");
|
|
53
|
+
});
|
|
54
|
+
it("generates text with chat history", async () => {
|
|
55
|
+
const chat = [
|
|
56
|
+
{
|
|
57
|
+
role: "user",
|
|
58
|
+
content: "What is the capital of France?",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
role: "assistant",
|
|
62
|
+
content: "Paris.",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
const answer = await getState().functions.llm_generate.run(
|
|
66
|
+
"What is the name of the river running through this city?",
|
|
67
|
+
{
|
|
68
|
+
chat,
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
//console.log({ answer });
|
|
72
|
+
|
|
73
|
+
expect(typeof answer).toBe("string");
|
|
74
|
+
expect(answer).toContain("Seine");
|
|
75
|
+
expect(chat.length).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
it("generates text with chat history and no prompt", async () => {
|
|
78
|
+
const answer = await getState().functions.llm_generate.run("", {
|
|
79
|
+
chat: [
|
|
80
|
+
{
|
|
81
|
+
role: "user",
|
|
82
|
+
content: "What is the capital of France?",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: "assistant",
|
|
86
|
+
content: "Paris.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
role: "user",
|
|
90
|
+
content: "What is the name of the river running through this city?",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
//console.log({ answer });
|
|
95
|
+
|
|
96
|
+
expect(typeof answer).toBe("string");
|
|
97
|
+
expect(answer).toContain("Seine");
|
|
98
|
+
});
|
|
99
|
+
it("uses tools", async () => {
|
|
100
|
+
const answer = await getState().functions.llm_generate.run(
|
|
101
|
+
"Generate a list of EU capitals in a structured format using the provided tool",
|
|
102
|
+
cities_tool,
|
|
103
|
+
);
|
|
104
|
+
expect(typeof answer).toBe("object");
|
|
105
|
+
const cities = answer.ai_sdk
|
|
106
|
+
? answer.tool_calls[0].input?.cities
|
|
107
|
+
: JSON.parse(answer.tool_calls[0].function.arguments).cities;
|
|
108
|
+
expect(cities.length).toBe(27);
|
|
109
|
+
});
|
|
110
|
+
it("appends to chat history", async () => {
|
|
111
|
+
const chat = [];
|
|
112
|
+
const answer1 = await getState().functions.llm_generate.run(
|
|
113
|
+
"What is the Capital of France?",
|
|
114
|
+
{
|
|
115
|
+
chat,
|
|
116
|
+
appendToChat: true,
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
const answer2 = await getState().functions.llm_generate.run(
|
|
120
|
+
"What is the name of the river running through this city?",
|
|
121
|
+
{
|
|
122
|
+
chat,
|
|
123
|
+
appendToChat: true,
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
//console.log({ answer });
|
|
127
|
+
|
|
128
|
+
expect(typeof answer2).toBe("string");
|
|
129
|
+
expect(answer2).toContain("Seine");
|
|
130
|
+
expect(chat.length).toBe(4);
|
|
131
|
+
});
|
|
132
|
+
it("tool use sequence", async () => {
|
|
133
|
+
const chat = [];
|
|
134
|
+
const answer = await getState().functions.llm_generate.run(
|
|
135
|
+
"Generate a list of EU capitals in a structured format using the provided tool",
|
|
136
|
+
{ chat, appendToChat: true, ...cities_tool, streamCallback() {} },
|
|
137
|
+
);
|
|
138
|
+
expect(typeof answer).toBe("object");
|
|
139
|
+
|
|
140
|
+
const tc = answer.getToolCalls()[0];
|
|
141
|
+
|
|
142
|
+
const cities = tc.input.cities;
|
|
143
|
+
expect(cities.length).toBe(27);
|
|
144
|
+
|
|
145
|
+
await getState().functions.llm_add_tool_response.run("List received", {
|
|
146
|
+
chat,
|
|
147
|
+
tool_call: tc,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const answer1 = await getState().functions.llm_generate.run(
|
|
151
|
+
"Make the same list in a structured format using the provided tool but for the original 12 member countries of the EU",
|
|
152
|
+
{ chat, appendToChat: true, ...cities_tool },
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const cities1 = answer1.getToolCalls()[0].input?.cities;
|
|
156
|
+
|
|
157
|
+
expect(cities1.length).toBe(12);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const cities_tool = {
|
|
163
|
+
tools: [
|
|
164
|
+
{
|
|
165
|
+
type: "function",
|
|
166
|
+
function: {
|
|
167
|
+
name: "cities",
|
|
168
|
+
description: "Provide a list of cities by country and city name",
|
|
169
|
+
parameters: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
cities: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
country_name: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Country name",
|
|
180
|
+
},
|
|
181
|
+
city_name: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "City name",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ["country_name", "city_name"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
tool_choice: {
|
|
195
|
+
type: "function",
|
|
196
|
+
function: {
|
|
197
|
+
name: "cities",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|