@saltcorn/large-language-model 0.9.12 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants.js +1 -3
- package/generate.js +301 -95
- package/index.js +57 -19
- package/package.json +10 -3
- package/tests/configs.js +34 -0
- package/tests/llm.test.js +209 -0
package/constants.js
CHANGED
package/generate.js
CHANGED
|
@@ -38,7 +38,7 @@ const getEmbedding = async (config, opts) => {
|
|
|
38
38
|
apiKey: config.api_key,
|
|
39
39
|
embed_model: opts?.embed_model || config.embed_model || config.model,
|
|
40
40
|
},
|
|
41
|
-
opts
|
|
41
|
+
opts,
|
|
42
42
|
);
|
|
43
43
|
case "OpenAI":
|
|
44
44
|
return await getEmbeddingOpenAICompatible(
|
|
@@ -47,7 +47,7 @@ const getEmbedding = async (config, opts) => {
|
|
|
47
47
|
bearer: opts?.api_key || config.api_key,
|
|
48
48
|
embed_model: opts?.model || config.embed_model,
|
|
49
49
|
},
|
|
50
|
-
opts
|
|
50
|
+
opts,
|
|
51
51
|
);
|
|
52
52
|
case "OpenAI-compatible API":
|
|
53
53
|
return await getEmbeddingOpenAICompatible(
|
|
@@ -61,7 +61,7 @@ const getEmbedding = async (config, opts) => {
|
|
|
61
61
|
config.embed_model ||
|
|
62
62
|
config.model,
|
|
63
63
|
},
|
|
64
|
-
opts
|
|
64
|
+
opts,
|
|
65
65
|
);
|
|
66
66
|
case "Local Ollama":
|
|
67
67
|
if (config.embed_endpoint) {
|
|
@@ -74,14 +74,14 @@ const getEmbedding = async (config, opts) => {
|
|
|
74
74
|
config.embed_model ||
|
|
75
75
|
config.model,
|
|
76
76
|
},
|
|
77
|
-
opts
|
|
77
|
+
opts,
|
|
78
78
|
);
|
|
79
79
|
} else {
|
|
80
80
|
if (!ollamaMod) throw new Error("Not implemented for this backend");
|
|
81
81
|
|
|
82
82
|
const { Ollama } = ollamaMod;
|
|
83
83
|
const ollama = new Ollama(
|
|
84
|
-
config.ollama_host ? { host: config.ollama_host } : undefined
|
|
84
|
+
config.ollama_host ? { host: config.ollama_host } : undefined,
|
|
85
85
|
);
|
|
86
86
|
const olres = await ollama.embeddings({
|
|
87
87
|
model: opts?.model || config.embed_model || config.model,
|
|
@@ -112,7 +112,7 @@ const getImageGeneration = async (config, opts) => {
|
|
|
112
112
|
model: opts?.model || config.model,
|
|
113
113
|
responses_api: config.responses_api,
|
|
114
114
|
},
|
|
115
|
-
opts
|
|
115
|
+
opts,
|
|
116
116
|
);
|
|
117
117
|
default:
|
|
118
118
|
throw new Error("Image generation not implemented for this backend");
|
|
@@ -121,7 +121,7 @@ const getImageGeneration = async (config, opts) => {
|
|
|
121
121
|
|
|
122
122
|
const getAudioTranscription = async (
|
|
123
123
|
{ backend, apiKey, api_key, provider, ai_sdk_provider },
|
|
124
|
-
opts
|
|
124
|
+
opts,
|
|
125
125
|
) => {
|
|
126
126
|
switch (opts.backend || backend) {
|
|
127
127
|
case "ElevenLabs":
|
|
@@ -134,7 +134,7 @@ const getAudioTranscription = async (
|
|
|
134
134
|
languageCode: opts.languageCode || "eng", // Language of the audio file. If set to null, the model will detect the language automatically.
|
|
135
135
|
numSpeakers: opts.numSpeakers || null, // Language of the audio file. If set to null, the model will detect the language automatically.
|
|
136
136
|
diarize: !!opts.diarize, // Whether to annotate who is speaking
|
|
137
|
-
diarizationThreshold: opts.diarizationThreshold || null
|
|
137
|
+
diarizationThreshold: opts.diarizationThreshold || null,
|
|
138
138
|
});
|
|
139
139
|
return transcription;
|
|
140
140
|
case "OpenAI":
|
|
@@ -144,10 +144,10 @@ const getAudioTranscription = async (
|
|
|
144
144
|
const fp = opts.file.location
|
|
145
145
|
? opts.file.location
|
|
146
146
|
: typeof opts.file === "string"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
? await (
|
|
148
|
+
await File.findOne(opts.file)
|
|
149
|
+
).location
|
|
150
|
+
: null;
|
|
151
151
|
const model = opts?.model || "whisper-1";
|
|
152
152
|
const diarize = model === "gpt-4o-transcribe-diarize";
|
|
153
153
|
const transcript1 = await client.audio.transcriptions.create({
|
|
@@ -171,8 +171,8 @@ const getAudioTranscription = async (
|
|
|
171
171
|
(Buffer.isBuffer(opts.file)
|
|
172
172
|
? opts.file
|
|
173
173
|
: typeof opts.file === "string"
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
? await (await File.findOne(opts.file)).get_contents()
|
|
175
|
+
: await opts.file.get_contents());
|
|
176
176
|
const extra = {};
|
|
177
177
|
if (opts.prompt)
|
|
178
178
|
extra.providerOptions = {
|
|
@@ -193,6 +193,156 @@ const getAudioTranscription = async (
|
|
|
193
193
|
}
|
|
194
194
|
};
|
|
195
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
|
+
|
|
294
|
+
const addImageMesssage = async (
|
|
295
|
+
{ backend, apiKey, api_key, provider, ai_sdk_provider, responses_api },
|
|
296
|
+
opts,
|
|
297
|
+
) => {
|
|
298
|
+
let chat = opts.chat;
|
|
299
|
+
let result = opts.prompt;
|
|
300
|
+
//console.log("chat", JSON.stringify(chat, null, 2));
|
|
301
|
+
let imageurl = opts.prompt;
|
|
302
|
+
switch (opts.backend || backend) {
|
|
303
|
+
case "OpenAI":
|
|
304
|
+
{
|
|
305
|
+
const new_chat_item = responses_api
|
|
306
|
+
? {
|
|
307
|
+
role: "user",
|
|
308
|
+
content: [
|
|
309
|
+
{
|
|
310
|
+
type: "input_image",
|
|
311
|
+
image_url: imageurl,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
}
|
|
315
|
+
: {
|
|
316
|
+
role: "user",
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: "image_url",
|
|
320
|
+
image_url: {
|
|
321
|
+
url: imageurl,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
chat.push(new_chat_item);
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
case "AI SDK":
|
|
331
|
+
chat.push({
|
|
332
|
+
role: "user",
|
|
333
|
+
content: [
|
|
334
|
+
{
|
|
335
|
+
type: "image",
|
|
336
|
+
image: imageurl,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
196
346
|
const getCompletion = async (config, opts) => {
|
|
197
347
|
switch (config.backend) {
|
|
198
348
|
case "AI SDK":
|
|
@@ -202,7 +352,7 @@ const getCompletion = async (config, opts) => {
|
|
|
202
352
|
apiKey: config.api_key,
|
|
203
353
|
model: opts?.model || config.model,
|
|
204
354
|
},
|
|
205
|
-
opts
|
|
355
|
+
opts,
|
|
206
356
|
);
|
|
207
357
|
case "OpenAI":
|
|
208
358
|
return await getCompletionOpenAICompatible(
|
|
@@ -214,7 +364,7 @@ const getCompletion = async (config, opts) => {
|
|
|
214
364
|
model: opts?.model || config.model,
|
|
215
365
|
responses_api: config.responses_api,
|
|
216
366
|
},
|
|
217
|
-
opts
|
|
367
|
+
opts,
|
|
218
368
|
);
|
|
219
369
|
case "OpenAI-compatible API":
|
|
220
370
|
return await getCompletionOpenAICompatible(
|
|
@@ -228,7 +378,7 @@ const getCompletion = async (config, opts) => {
|
|
|
228
378
|
apiKey: opts?.api_key || config.api_key,
|
|
229
379
|
model: opts?.model || config.model,
|
|
230
380
|
},
|
|
231
|
-
opts
|
|
381
|
+
opts,
|
|
232
382
|
);
|
|
233
383
|
case "Local Ollama":
|
|
234
384
|
return await getCompletionOpenAICompatible(
|
|
@@ -238,14 +388,14 @@ const getCompletion = async (config, opts) => {
|
|
|
238
388
|
: "http://localhost:11434/v1/chat/completions",
|
|
239
389
|
model: opts?.model || config.model,
|
|
240
390
|
},
|
|
241
|
-
opts
|
|
391
|
+
opts,
|
|
242
392
|
);
|
|
243
393
|
case "Local llama.cpp":
|
|
244
394
|
//TODO only check if unsafe plugins not allowed
|
|
245
395
|
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
246
396
|
if (!isRoot)
|
|
247
397
|
throw new Error(
|
|
248
|
-
"llama.cpp inference is not permitted on subdomain tenants"
|
|
398
|
+
"llama.cpp inference is not permitted on subdomain tenants",
|
|
249
399
|
);
|
|
250
400
|
let hyperStr = "";
|
|
251
401
|
if (opts.temperature) hyperStr += ` --temp ${opts.temperature}`;
|
|
@@ -255,7 +405,7 @@ const getCompletion = async (config, opts) => {
|
|
|
255
405
|
|
|
256
406
|
const { stdout, stderr } = await exec(
|
|
257
407
|
`./main -m ${config.model_path} -p "${opts.prompt}" ${nstr}${hyperStr}`,
|
|
258
|
-
{ cwd: config.llama_dir }
|
|
408
|
+
{ cwd: config.llama_dir },
|
|
259
409
|
);
|
|
260
410
|
return stdout;
|
|
261
411
|
case "Google Vertex AI":
|
|
@@ -286,12 +436,13 @@ const getCompletionAISDK = async (
|
|
|
286
436
|
systemPrompt,
|
|
287
437
|
prompt,
|
|
288
438
|
debugResult,
|
|
439
|
+
appendToChat,
|
|
289
440
|
debugCollector,
|
|
290
441
|
chat = [],
|
|
291
442
|
api_key,
|
|
292
443
|
endpoint,
|
|
293
444
|
...rest
|
|
294
|
-
}
|
|
445
|
+
},
|
|
295
446
|
) => {
|
|
296
447
|
const use_model_name = rest.model || model;
|
|
297
448
|
let model_obj = getAiSdkModel({
|
|
@@ -313,7 +464,7 @@ const getCompletionAISDK = async (
|
|
|
313
464
|
...(Array.isArray(chat.content) ? { content: chat.content.map(f) } : {}),
|
|
314
465
|
};
|
|
315
466
|
};
|
|
316
|
-
const newChat = chat.map(modifyChat);
|
|
467
|
+
const newChat = appendToChat ? chat : chat.map(modifyChat);
|
|
317
468
|
|
|
318
469
|
const body = {
|
|
319
470
|
...rest,
|
|
@@ -327,6 +478,9 @@ const getCompletionAISDK = async (
|
|
|
327
478
|
...(prompt ? [{ role: "user", content: prompt }] : []),
|
|
328
479
|
],
|
|
329
480
|
};
|
|
481
|
+
if (appendToChat && chat && prompt) {
|
|
482
|
+
chat.push({ role: "user", content: prompt });
|
|
483
|
+
}
|
|
330
484
|
if (rest.temperature || temperature) {
|
|
331
485
|
const str_or_num = rest.temperature || temperature;
|
|
332
486
|
body.temperature = +str_or_num;
|
|
@@ -342,6 +496,9 @@ const getCompletionAISDK = async (
|
|
|
342
496
|
"gpt-5",
|
|
343
497
|
"gpt-5-nano",
|
|
344
498
|
"gpt-5-mini",
|
|
499
|
+
"gpt-5.1",
|
|
500
|
+
"gpt-5.1-codex",
|
|
501
|
+
"gpt-5.2",
|
|
345
502
|
].includes(use_model_name)
|
|
346
503
|
)
|
|
347
504
|
body.temperature = 0.7;
|
|
@@ -350,9 +507,9 @@ const getCompletionAISDK = async (
|
|
|
350
507
|
const prevTools = [...body.tools];
|
|
351
508
|
body.tools = {};
|
|
352
509
|
prevTools.forEach((t) => {
|
|
353
|
-
body.tools[t.function.name] = tool({
|
|
354
|
-
description: t.function.description,
|
|
355
|
-
inputSchema: jsonSchema(t.function.parameters),
|
|
510
|
+
body.tools[t.name || t.function.name] = tool({
|
|
511
|
+
description: t.description || t.function.description,
|
|
512
|
+
inputSchema: jsonSchema(t.parameters || t.function.parameters),
|
|
356
513
|
});
|
|
357
514
|
});
|
|
358
515
|
}
|
|
@@ -367,11 +524,21 @@ const getCompletionAISDK = async (
|
|
|
367
524
|
let results;
|
|
368
525
|
if (rest.streamCallback) {
|
|
369
526
|
delete body.streamCallback;
|
|
370
|
-
|
|
371
|
-
for await (const textPart of
|
|
527
|
+
const results1 = await streamText(body);
|
|
528
|
+
for await (const textPart of results1.textStream) {
|
|
372
529
|
rest.streamCallback(textPart);
|
|
373
530
|
}
|
|
531
|
+
results = {
|
|
532
|
+
response: await results1.response,
|
|
533
|
+
text: await results1.text,
|
|
534
|
+
steps: await results1.steps,
|
|
535
|
+
};
|
|
374
536
|
} else results = await generateText(body);
|
|
537
|
+
|
|
538
|
+
if (appendToChat && chat) {
|
|
539
|
+
chat.push(...results.response.messages);
|
|
540
|
+
}
|
|
541
|
+
|
|
375
542
|
if (debugResult)
|
|
376
543
|
console.log("AI SDK response", JSON.stringify(results, null, 2));
|
|
377
544
|
else getState().log(6, `AI SDK response ${JSON.stringify(results)}`);
|
|
@@ -387,6 +554,14 @@ const getCompletionAISDK = async (
|
|
|
387
554
|
content: await results.text,
|
|
388
555
|
messages: (await results.response).messages,
|
|
389
556
|
ai_sdk: true,
|
|
557
|
+
hasToolCalls: allToolCalls.length,
|
|
558
|
+
getToolCalls() {
|
|
559
|
+
return allToolCalls.map((tc) => ({
|
|
560
|
+
tool_name: tc.toolName,
|
|
561
|
+
input: tc.input,
|
|
562
|
+
tool_call_id: tc.toolCallId,
|
|
563
|
+
}));
|
|
564
|
+
},
|
|
390
565
|
};
|
|
391
566
|
} else return results.text;
|
|
392
567
|
};
|
|
@@ -399,10 +574,11 @@ const getCompletionOpenAICompatible = async (
|
|
|
399
574
|
debugResult,
|
|
400
575
|
debugCollector,
|
|
401
576
|
chat = [],
|
|
577
|
+
appendToChat,
|
|
402
578
|
api_key,
|
|
403
579
|
endpoint,
|
|
404
580
|
...rest
|
|
405
|
-
}
|
|
581
|
+
},
|
|
406
582
|
) => {
|
|
407
583
|
const headers = {
|
|
408
584
|
"Content-Type": "application/json",
|
|
@@ -440,63 +616,67 @@ const getCompletionOpenAICompatible = async (
|
|
|
440
616
|
delete body.streamCallback;
|
|
441
617
|
}
|
|
442
618
|
if (responses_api) {
|
|
619
|
+
delete body.tool_choice;
|
|
443
620
|
for (const tool of body.tools || []) {
|
|
444
|
-
if (tool.type !== "function") continue;
|
|
621
|
+
if (tool.type !== "function" || !tool.function) continue;
|
|
445
622
|
tool.name = tool.function.name;
|
|
446
623
|
tool.description = tool.function.description;
|
|
447
624
|
tool.parameters = tool.function.parameters;
|
|
448
625
|
if (tool.function.required) tool.required = tool.function.required;
|
|
449
626
|
delete tool.function;
|
|
450
627
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
628
|
+
let newChat;
|
|
629
|
+
if (!appendToChat) {
|
|
630
|
+
newChat = [];
|
|
631
|
+
(chat || []).forEach((c) => {
|
|
632
|
+
if (c.tool_calls) {
|
|
633
|
+
c.tool_calls.forEach((tc) => {
|
|
634
|
+
newChat.push({
|
|
635
|
+
id: tc.id,
|
|
636
|
+
type: "function_call",
|
|
637
|
+
call_id: tc.call_id,
|
|
638
|
+
name: tc.name,
|
|
639
|
+
arguments: tc.arguments,
|
|
640
|
+
});
|
|
461
641
|
});
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
|
|
642
|
+
} else if (c.content?.image_calls) {
|
|
643
|
+
c.content.image_calls.forEach((ic) => {
|
|
644
|
+
newChat.push({
|
|
645
|
+
...ic,
|
|
646
|
+
result: undefined,
|
|
647
|
+
filename: undefined,
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
} else if (c.content?.mcp_calls) {
|
|
651
|
+
c.content.mcp_calls.forEach((ic) => {
|
|
652
|
+
newChat.push({
|
|
653
|
+
...ic,
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
} else if (c.role === "tool") {
|
|
465
657
|
newChat.push({
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
658
|
+
type: "function_call_output",
|
|
659
|
+
call_id: c.call_id,
|
|
660
|
+
output: c.content,
|
|
469
661
|
});
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
662
|
+
} else {
|
|
663
|
+
const fcontent = (c) => {
|
|
664
|
+
if (c.type === "image_url")
|
|
665
|
+
return {
|
|
666
|
+
type: "input_image",
|
|
667
|
+
image_url: c.image_url.url,
|
|
668
|
+
};
|
|
669
|
+
else return c;
|
|
670
|
+
};
|
|
473
671
|
newChat.push({
|
|
474
|
-
...
|
|
672
|
+
...c,
|
|
673
|
+
content: Array.isArray(c.content)
|
|
674
|
+
? c.content.map(fcontent)
|
|
675
|
+
: c.content,
|
|
475
676
|
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
type: "function_call_output",
|
|
480
|
-
call_id: c.call_id,
|
|
481
|
-
output: c.content,
|
|
482
|
-
});
|
|
483
|
-
} else {
|
|
484
|
-
const fcontent = (c) => {
|
|
485
|
-
if (c.type === "image_url")
|
|
486
|
-
return {
|
|
487
|
-
type: "input_image",
|
|
488
|
-
image_url: c.image_url.url,
|
|
489
|
-
};
|
|
490
|
-
else return c;
|
|
491
|
-
};
|
|
492
|
-
newChat.push({
|
|
493
|
-
...c,
|
|
494
|
-
content: Array.isArray(c.content)
|
|
495
|
-
? c.content.map(fcontent)
|
|
496
|
-
: c.content,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
});
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
} else newChat = chat;
|
|
500
680
|
body.input = [
|
|
501
681
|
{
|
|
502
682
|
role: "system",
|
|
@@ -517,6 +697,9 @@ const getCompletionOpenAICompatible = async (
|
|
|
517
697
|
...(prompt ? [{ role: "user", content: prompt }] : []),
|
|
518
698
|
];
|
|
519
699
|
}
|
|
700
|
+
if (appendToChat && chat && prompt) {
|
|
701
|
+
chat.push({ role: "user", content: prompt });
|
|
702
|
+
}
|
|
520
703
|
if (debugResult)
|
|
521
704
|
console.log(
|
|
522
705
|
"OpenAI request",
|
|
@@ -524,14 +707,14 @@ const getCompletionOpenAICompatible = async (
|
|
|
524
707
|
"to",
|
|
525
708
|
chatCompleteEndpoint,
|
|
526
709
|
"headers",
|
|
527
|
-
JSON.stringify(headers)
|
|
710
|
+
JSON.stringify(headers),
|
|
528
711
|
);
|
|
529
712
|
else
|
|
530
713
|
getState().log(
|
|
531
714
|
6,
|
|
532
715
|
`OpenAI request ${JSON.stringify(
|
|
533
|
-
body
|
|
534
|
-
)} to ${chatCompleteEndpoint} headers ${JSON.stringify(headers)}
|
|
716
|
+
body,
|
|
717
|
+
)} to ${chatCompleteEndpoint} headers ${JSON.stringify(headers)}`,
|
|
535
718
|
);
|
|
536
719
|
if (debugCollector) debugCollector.request = body;
|
|
537
720
|
const reqTimeStart = Date.now();
|
|
@@ -623,7 +806,7 @@ const getCompletionOpenAICompatible = async (
|
|
|
623
806
|
: streamParts.join("");
|
|
624
807
|
}
|
|
625
808
|
const results = await rawResponse.json();
|
|
626
|
-
|
|
809
|
+
|
|
627
810
|
if (debugResult)
|
|
628
811
|
console.log("OpenAI response", JSON.stringify(results, null, 2));
|
|
629
812
|
else getState().log(6, `OpenAI response ${JSON.stringify(results)}`);
|
|
@@ -633,36 +816,49 @@ const getCompletionOpenAICompatible = async (
|
|
|
633
816
|
}
|
|
634
817
|
|
|
635
818
|
if (results.error) throw new Error(`OpenAI error: ${results.error.message}`);
|
|
819
|
+
if (appendToChat && chat) {
|
|
820
|
+
if (responses_api) chat.push(...results.output);
|
|
821
|
+
else chat.push(results.choices[0].message);
|
|
822
|
+
}
|
|
636
823
|
if (responses_api) {
|
|
637
824
|
const textOutput = results.output
|
|
638
825
|
.filter((o) => o.type === "message")
|
|
639
826
|
.map((o) => o.content.map((c) => c.text).join(""))
|
|
640
827
|
.join("");
|
|
828
|
+
const tool_calls = emptyToUndefined(
|
|
829
|
+
results.output
|
|
830
|
+
.filter((o) => o.type === "function_call")
|
|
831
|
+
.map((o) => ({
|
|
832
|
+
function: { name: o.name, arguments: o.arguments },
|
|
833
|
+
...o,
|
|
834
|
+
})),
|
|
835
|
+
);
|
|
641
836
|
return results.output.some(
|
|
642
837
|
(o) =>
|
|
643
838
|
o.type === "function_call" ||
|
|
644
839
|
o.type === "image_generation_call" ||
|
|
645
840
|
o.type === "mcp_list_tools" ||
|
|
646
|
-
o.type === "mcp_call"
|
|
841
|
+
o.type === "mcp_call",
|
|
647
842
|
)
|
|
648
843
|
? {
|
|
649
|
-
tool_calls
|
|
650
|
-
results.output
|
|
651
|
-
.filter((o) => o.type === "function_call")
|
|
652
|
-
.map((o) => ({
|
|
653
|
-
function: { name: o.name, arguments: o.arguments },
|
|
654
|
-
...o,
|
|
655
|
-
}))
|
|
656
|
-
),
|
|
844
|
+
tool_calls,
|
|
657
845
|
image_calls: emptyToUndefined(
|
|
658
|
-
results.output.filter((o) => o.type === "image_generation_call")
|
|
846
|
+
results.output.filter((o) => o.type === "image_generation_call"),
|
|
659
847
|
),
|
|
660
848
|
mcp_calls: emptyToUndefined(
|
|
661
849
|
results.output.filter(
|
|
662
|
-
(o) => o.type === "mcp_call" || o.type === "mcp_list_tools"
|
|
663
|
-
)
|
|
850
|
+
(o) => o.type === "mcp_call" || o.type === "mcp_list_tools",
|
|
851
|
+
),
|
|
664
852
|
),
|
|
665
853
|
content: textOutput || null,
|
|
854
|
+
hasToolCalls: tool_calls?.length,
|
|
855
|
+
getToolCalls() {
|
|
856
|
+
return tool_calls.map((tc) => ({
|
|
857
|
+
tool_name: tc.function.name,
|
|
858
|
+
input: JSON.parse(tc.function.arguments),
|
|
859
|
+
tool_call_id: tc.call_id,
|
|
860
|
+
}));
|
|
861
|
+
},
|
|
666
862
|
}
|
|
667
863
|
: textOutput || null;
|
|
668
864
|
} else
|
|
@@ -670,6 +866,14 @@ const getCompletionOpenAICompatible = async (
|
|
|
670
866
|
? {
|
|
671
867
|
tool_calls: results?.choices?.[0]?.message?.tool_calls,
|
|
672
868
|
content: results?.choices?.[0]?.message?.content || null,
|
|
869
|
+
hasToolCalls: results?.choices?.[0]?.message?.tool_calls.length,
|
|
870
|
+
getToolCalls() {
|
|
871
|
+
return results?.choices?.[0]?.message?.tool_calls.map((tc) => ({
|
|
872
|
+
tool_name: tc.function.name,
|
|
873
|
+
input: JSON.parse(tc.function.arguments),
|
|
874
|
+
tool_call_id: tc.id,
|
|
875
|
+
}));
|
|
876
|
+
},
|
|
673
877
|
}
|
|
674
878
|
: results?.choices?.[0]?.message?.content || null;
|
|
675
879
|
};
|
|
@@ -688,7 +892,7 @@ const getImageGenOpenAICompatible = async (
|
|
|
688
892
|
n,
|
|
689
893
|
output_format,
|
|
690
894
|
response_format,
|
|
691
|
-
}
|
|
895
|
+
},
|
|
692
896
|
) => {
|
|
693
897
|
const { imageEndpoint, bearer, apiKey, image_model } = config;
|
|
694
898
|
const headers = {
|
|
@@ -725,7 +929,7 @@ const getImageGenOpenAICompatible = async (
|
|
|
725
929
|
|
|
726
930
|
const getEmbeddingOpenAICompatible = async (
|
|
727
931
|
config,
|
|
728
|
-
{ prompt, model, debugResult }
|
|
932
|
+
{ prompt, model, debugResult },
|
|
729
933
|
) => {
|
|
730
934
|
const { embeddingsEndpoint, bearer, apiKey, embed_model } = config;
|
|
731
935
|
const headers = {
|
|
@@ -762,7 +966,7 @@ const getEmbeddingAISDK = async (config, { prompt, model, debugResult }) => {
|
|
|
762
966
|
case "OpenAI":
|
|
763
967
|
const openai = createOpenAI({ apiKey: apiKey });
|
|
764
968
|
model_obj = openai.textEmbeddingModel(
|
|
765
|
-
model_name || "text-embedding-3-small"
|
|
969
|
+
model_name || "text-embedding-3-small",
|
|
766
970
|
);
|
|
767
971
|
//providerOptions.openai = {};
|
|
768
972
|
break;
|
|
@@ -815,7 +1019,7 @@ const initOAuth2Client = async (config) => {
|
|
|
815
1019
|
const oauth2Client = new google.auth.OAuth2(
|
|
816
1020
|
client_id,
|
|
817
1021
|
client_secret,
|
|
818
|
-
redirect_uri
|
|
1022
|
+
redirect_uri,
|
|
819
1023
|
);
|
|
820
1024
|
oauth2Client.setCredentials(pluginCfg.tokens);
|
|
821
1025
|
return oauth2Client;
|
|
@@ -883,7 +1087,7 @@ const getCompletionGoogleVertex = async (config, opts, oauth2Client) => {
|
|
|
883
1087
|
chatParams.tools = [
|
|
884
1088
|
{
|
|
885
1089
|
functionDeclarations: opts.tools.map((t) =>
|
|
886
|
-
prepFuncArgsForChat(t.function)
|
|
1090
|
+
prepFuncArgsForChat(t.function),
|
|
887
1091
|
),
|
|
888
1092
|
},
|
|
889
1093
|
];
|
|
@@ -925,7 +1129,7 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
|
|
|
925
1129
|
helpers.toValue({
|
|
926
1130
|
content: p,
|
|
927
1131
|
task_type: config.task_type || "RETRIEVAL_QUERY",
|
|
928
|
-
})
|
|
1132
|
+
}),
|
|
929
1133
|
);
|
|
930
1134
|
} else {
|
|
931
1135
|
instances = [
|
|
@@ -957,4 +1161,6 @@ module.exports = {
|
|
|
957
1161
|
getEmbedding,
|
|
958
1162
|
getImageGeneration,
|
|
959
1163
|
getAudioTranscription,
|
|
1164
|
+
toolResponse,
|
|
1165
|
+
addImageMesssage,
|
|
960
1166
|
};
|
package/index.js
CHANGED
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
getEmbedding,
|
|
12
12
|
getImageGeneration,
|
|
13
13
|
getAudioTranscription,
|
|
14
|
+
toolResponse,
|
|
15
|
+
addImageMesssage,
|
|
14
16
|
} = require("./generate");
|
|
15
17
|
const { OPENAI_MODELS } = require("./constants.js");
|
|
16
18
|
const { eval_expression } = require("@saltcorn/data/models/expression");
|
|
@@ -381,7 +383,10 @@ const functions = (config) => {
|
|
|
381
383
|
},
|
|
382
384
|
isAsync: true,
|
|
383
385
|
description: "Generate text with GPT",
|
|
384
|
-
arguments: [
|
|
386
|
+
arguments: [
|
|
387
|
+
{ name: "prompt", type: "String", required: true },
|
|
388
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
389
|
+
],
|
|
385
390
|
},
|
|
386
391
|
llm_image_generate: {
|
|
387
392
|
run: async (prompt, opts) => {
|
|
@@ -390,7 +395,10 @@ const functions = (config) => {
|
|
|
390
395
|
},
|
|
391
396
|
isAsync: true,
|
|
392
397
|
description: "Generate image",
|
|
393
|
-
arguments: [
|
|
398
|
+
arguments: [
|
|
399
|
+
{ name: "prompt", type: "String", required: true },
|
|
400
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
401
|
+
],
|
|
394
402
|
},
|
|
395
403
|
llm_embedding: {
|
|
396
404
|
run: async (prompt, opts) => {
|
|
@@ -399,7 +407,10 @@ const functions = (config) => {
|
|
|
399
407
|
},
|
|
400
408
|
isAsync: true,
|
|
401
409
|
description: "Get vector embedding",
|
|
402
|
-
arguments: [
|
|
410
|
+
arguments: [
|
|
411
|
+
{ name: "prompt", type: "String", required: true },
|
|
412
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
413
|
+
],
|
|
403
414
|
},
|
|
404
415
|
llm_transcribe: {
|
|
405
416
|
run: async (opts) => {
|
|
@@ -408,7 +419,33 @@ const functions = (config) => {
|
|
|
408
419
|
},
|
|
409
420
|
isAsync: true,
|
|
410
421
|
description: "Get vector embedding",
|
|
411
|
-
arguments: [
|
|
422
|
+
arguments: [
|
|
423
|
+
{ name: "options", type: "JSON", tstype: "any", required: true },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
llm_add_message: {
|
|
427
|
+
run: async (what, prompt, opts) => {
|
|
428
|
+
switch (what) {
|
|
429
|
+
case "tool_response":
|
|
430
|
+
return await toolResponse(config, { prompt, ...opts });
|
|
431
|
+
case "image":
|
|
432
|
+
return await addImageMesssage(config, { prompt, ...opts });
|
|
433
|
+
default:
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
isAsync: true,
|
|
438
|
+
description: "Insert a tool response or an image in a chat",
|
|
439
|
+
arguments: [
|
|
440
|
+
{
|
|
441
|
+
name: "what",
|
|
442
|
+
type: "String",
|
|
443
|
+
tstype: '"tool_response"|"image"',
|
|
444
|
+
required: true,
|
|
445
|
+
},
|
|
446
|
+
{ name: "prompt", type: "String", required: true },
|
|
447
|
+
{ name: "options", type: "JSON", tstype: "any" },
|
|
448
|
+
],
|
|
412
449
|
},
|
|
413
450
|
};
|
|
414
451
|
};
|
|
@@ -432,7 +469,7 @@ const routes = (config) => {
|
|
|
432
469
|
const oauth2Client = new google.auth.OAuth2(
|
|
433
470
|
client_id,
|
|
434
471
|
client_secret,
|
|
435
|
-
redirect_uri
|
|
472
|
+
redirect_uri,
|
|
436
473
|
);
|
|
437
474
|
const authUrl = oauth2Client.generateAuthUrl({
|
|
438
475
|
access_type: "offline",
|
|
@@ -459,7 +496,7 @@ const routes = (config) => {
|
|
|
459
496
|
const oauth2Client = new google.auth.OAuth2(
|
|
460
497
|
client_id,
|
|
461
498
|
client_secret,
|
|
462
|
-
redirect_uri
|
|
499
|
+
redirect_uri,
|
|
463
500
|
);
|
|
464
501
|
let plugin = await Plugin.findOne({ name: "large-language-model" });
|
|
465
502
|
if (!plugin) {
|
|
@@ -475,8 +512,8 @@ const routes = (config) => {
|
|
|
475
512
|
req.flash(
|
|
476
513
|
"warning",
|
|
477
514
|
req.__(
|
|
478
|
-
"No refresh token received. Please revoke the plugin's access and try again."
|
|
479
|
-
)
|
|
515
|
+
"No refresh token received. Please revoke the plugin's access and try again.",
|
|
516
|
+
),
|
|
480
517
|
);
|
|
481
518
|
} else {
|
|
482
519
|
const newConfig = { ...(plugin.configuration || {}), tokens };
|
|
@@ -488,7 +525,7 @@ const routes = (config) => {
|
|
|
488
525
|
});
|
|
489
526
|
req.flash(
|
|
490
527
|
"success",
|
|
491
|
-
req.__("Authentication successful! You can now use Vertex AI.")
|
|
528
|
+
req.__("Authentication successful! You can now use Vertex AI."),
|
|
492
529
|
);
|
|
493
530
|
}
|
|
494
531
|
} catch (error) {
|
|
@@ -615,13 +652,13 @@ module.exports = {
|
|
|
615
652
|
prompt_formula,
|
|
616
653
|
row,
|
|
617
654
|
user,
|
|
618
|
-
"llm_generate prompt formula"
|
|
655
|
+
"llm_generate prompt formula",
|
|
619
656
|
);
|
|
620
657
|
else prompt = row[prompt_field];
|
|
621
658
|
const opts = {};
|
|
622
659
|
if (override_config) {
|
|
623
660
|
const altcfg = config.altconfigs.find(
|
|
624
|
-
(c) => c.name === override_config
|
|
661
|
+
(c) => c.name === override_config,
|
|
625
662
|
);
|
|
626
663
|
opts.endpoint = altcfg.endpoint;
|
|
627
664
|
opts.model = altcfg.model;
|
|
@@ -679,7 +716,8 @@ module.exports = {
|
|
|
679
716
|
{
|
|
680
717
|
name: "answer_field",
|
|
681
718
|
label: "Response variable",
|
|
682
|
-
sublabel:
|
|
719
|
+
sublabel:
|
|
720
|
+
"Set the generated response object to this context variable. The subfield <code>text</code> holds the string transcription",
|
|
683
721
|
type: "String",
|
|
684
722
|
required: true,
|
|
685
723
|
},
|
|
@@ -766,7 +804,7 @@ module.exports = {
|
|
|
766
804
|
else
|
|
767
805
|
await table.updateRow(
|
|
768
806
|
{ [answer_field]: ans.text },
|
|
769
|
-
row[table.pk_name]
|
|
807
|
+
row[table.pk_name],
|
|
770
808
|
);
|
|
771
809
|
},
|
|
772
810
|
},
|
|
@@ -879,7 +917,7 @@ module.exports = {
|
|
|
879
917
|
prompt_formula,
|
|
880
918
|
row,
|
|
881
919
|
user,
|
|
882
|
-
"llm_generate prompt formula"
|
|
920
|
+
"llm_generate prompt formula",
|
|
883
921
|
);
|
|
884
922
|
else prompt = row[prompt_field];
|
|
885
923
|
|
|
@@ -906,7 +944,7 @@ module.exports = {
|
|
|
906
944
|
"image/png",
|
|
907
945
|
imgContents,
|
|
908
946
|
user?.id,
|
|
909
|
-
min_role || 1
|
|
947
|
+
min_role || 1,
|
|
910
948
|
);
|
|
911
949
|
upd[answer_field] = file.path_to_serve;
|
|
912
950
|
}
|
|
@@ -998,7 +1036,7 @@ module.exports = {
|
|
|
998
1036
|
sublabel:
|
|
999
1037
|
"Use this context variable to store the chat history for subsequent prompts",
|
|
1000
1038
|
type: "String",
|
|
1001
|
-
}
|
|
1039
|
+
},
|
|
1002
1040
|
);
|
|
1003
1041
|
} else if (table) {
|
|
1004
1042
|
const jsonFields = table.fields
|
|
@@ -1022,7 +1060,7 @@ module.exports = {
|
|
|
1022
1060
|
type: "String",
|
|
1023
1061
|
required: true,
|
|
1024
1062
|
attributes: { options: jsonFields },
|
|
1025
|
-
}
|
|
1063
|
+
},
|
|
1026
1064
|
);
|
|
1027
1065
|
}
|
|
1028
1066
|
|
|
@@ -1051,7 +1089,7 @@ module.exports = {
|
|
|
1051
1089
|
input_type: "section_header",
|
|
1052
1090
|
label: "JSON fields to generate",
|
|
1053
1091
|
},
|
|
1054
|
-
fieldsField
|
|
1092
|
+
fieldsField,
|
|
1055
1093
|
);
|
|
1056
1094
|
return cfgFields;
|
|
1057
1095
|
},
|
|
@@ -1077,7 +1115,7 @@ module.exports = {
|
|
|
1077
1115
|
if (model) opts.model = model;
|
|
1078
1116
|
if (override_config) {
|
|
1079
1117
|
const altcfg = config.altconfigs.find(
|
|
1080
|
-
(c) => c.name === override_config
|
|
1118
|
+
(c) => c.name === override_config,
|
|
1081
1119
|
);
|
|
1082
1120
|
opts.endpoint = altcfg.endpoint;
|
|
1083
1121
|
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.1",
|
|
4
4
|
"description": "Large language models and functionality for Saltcorn",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
@@ -16,17 +16,24 @@
|
|
|
16
16
|
"openai": "6.16.0",
|
|
17
17
|
"@elevenlabs/elevenlabs-js": "2.31.0"
|
|
18
18
|
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"jest": "^29.7.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "jest tests --runInBand"
|
|
24
|
+
},
|
|
19
25
|
"author": "Tom Nielsen",
|
|
20
26
|
"license": "MIT",
|
|
21
27
|
"repository": "github:saltcorn/large-language-model",
|
|
22
28
|
"eslintConfig": {
|
|
23
29
|
"extends": "eslint:recommended",
|
|
24
30
|
"parserOptions": {
|
|
25
|
-
"ecmaVersion":
|
|
31
|
+
"ecmaVersion": 2024
|
|
26
32
|
},
|
|
27
33
|
"env": {
|
|
28
34
|
"node": true,
|
|
29
|
-
"es6": true
|
|
35
|
+
"es6": true,
|
|
36
|
+
"jest/globals": true
|
|
30
37
|
},
|
|
31
38
|
"rules": {
|
|
32
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,209 @@
|
|
|
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
|
+
{
|
|
137
|
+
chat,
|
|
138
|
+
appendToChat: true,
|
|
139
|
+
...cities_tool,
|
|
140
|
+
//streamCallback() {}
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
expect(typeof answer).toBe("object");
|
|
144
|
+
|
|
145
|
+
const tc = answer.getToolCalls()[0];
|
|
146
|
+
|
|
147
|
+
const cities = tc.input.cities;
|
|
148
|
+
expect(cities.length).toBe(27);
|
|
149
|
+
|
|
150
|
+
await getState().functions.llm_add_message.run(
|
|
151
|
+
"tool_response",
|
|
152
|
+
"List received",
|
|
153
|
+
{
|
|
154
|
+
chat,
|
|
155
|
+
tool_call: tc,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const answer1 = await getState().functions.llm_generate.run(
|
|
160
|
+
"Make the same list in a structured format using the provided tool but for the original 12 member countries of the EU",
|
|
161
|
+
{ chat, appendToChat: true, ...cities_tool },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const cities1 = answer1.getToolCalls()[0].input?.cities;
|
|
165
|
+
|
|
166
|
+
expect(cities1.length).toBe(12);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cities_tool = {
|
|
172
|
+
tools: [
|
|
173
|
+
{
|
|
174
|
+
type: "function",
|
|
175
|
+
function: {
|
|
176
|
+
name: "cities",
|
|
177
|
+
description: "Provide a list of cities by country and city name",
|
|
178
|
+
parameters: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
cities: {
|
|
182
|
+
type: "array",
|
|
183
|
+
items: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
country_name: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "Country name",
|
|
189
|
+
},
|
|
190
|
+
city_name: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "City name",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ["country_name", "city_name"],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
tool_choice: {
|
|
204
|
+
type: "function",
|
|
205
|
+
function: {
|
|
206
|
+
name: "cities",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|