goblin-agents 0.1.1 → 0.1.4
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/llm/aiAgent.js +56 -26
- package/lib/llm/utils.js +53 -0
- package/package.json +1 -1
package/lib/llm/aiAgent.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
number,
|
|
12
12
|
record,
|
|
13
13
|
} = require('xcraft-core-stones');
|
|
14
|
+
const {getMutex} = require('xcraft-core-utils/lib/locks.js');
|
|
14
15
|
const LLMProvider = require('./providers.js');
|
|
15
16
|
const {OCR_SYSTEM_PROMPT, getPDFImages} = require('./utils.js');
|
|
16
17
|
|
|
@@ -86,6 +87,17 @@ class OptionsShape {
|
|
|
86
87
|
stop = option(array(string));
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function vectorToSqlLiteral(vec) {
|
|
91
|
+
const array = new Float32Array(vec);
|
|
92
|
+
const view = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
93
|
+
let hex = '';
|
|
94
|
+
for (let i = 0; i < view.length; i++) {
|
|
95
|
+
const byte = view[i];
|
|
96
|
+
hex += byte < 16 ? '0' + byte.toString(16) : byte.toString(16);
|
|
97
|
+
}
|
|
98
|
+
return hex;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
class AiAgentShape {
|
|
90
102
|
id = id('aiAgent');
|
|
91
103
|
version = number;
|
|
@@ -238,6 +250,10 @@ class AiAgent extends Elf {
|
|
|
238
250
|
logic = Elf.getLogic(AiAgentLogic);
|
|
239
251
|
state = new AiAgentState();
|
|
240
252
|
|
|
253
|
+
_provider;
|
|
254
|
+
_conf;
|
|
255
|
+
_batchLock = getMutex;
|
|
256
|
+
|
|
241
257
|
/**
|
|
242
258
|
* @param {string} id
|
|
243
259
|
* @param {string} desktopId
|
|
@@ -270,6 +286,24 @@ class AiAgent extends Elf {
|
|
|
270
286
|
this.logic.patch(prev);
|
|
271
287
|
}
|
|
272
288
|
|
|
289
|
+
async _loadProvider() {
|
|
290
|
+
const {state} = this;
|
|
291
|
+
const {provider, host, headers} = state;
|
|
292
|
+
const headersStr = JSON.stringify(headers);
|
|
293
|
+
if (
|
|
294
|
+
this._provider &&
|
|
295
|
+
this._conf.provider === provider &&
|
|
296
|
+
this._conf.host === host &&
|
|
297
|
+
this._conf.headersStr === headersStr
|
|
298
|
+
) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const Instance = LLMProvider.get(this.state.provider);
|
|
303
|
+
this._provider = new Instance(this.state.host, this.state.headers);
|
|
304
|
+
this._conf = {provider, host, headersStr};
|
|
305
|
+
}
|
|
306
|
+
|
|
273
307
|
/**
|
|
274
308
|
* @param {Partial<AiAgentState>} [agentState]
|
|
275
309
|
*/
|
|
@@ -292,14 +326,13 @@ class AiAgent extends Elf {
|
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
async gen(prompt) {
|
|
295
|
-
|
|
296
|
-
const provider = new Instance(this.state.host, this.state.headers);
|
|
329
|
+
await this._loadProvider();
|
|
297
330
|
|
|
298
331
|
let options;
|
|
299
332
|
if (this.state.options) {
|
|
300
333
|
options = this.state.options.toJS();
|
|
301
334
|
}
|
|
302
|
-
return await
|
|
335
|
+
return await this._provider.gen({
|
|
303
336
|
model: this.state.model,
|
|
304
337
|
prompt,
|
|
305
338
|
system: this.state.prompt,
|
|
@@ -323,8 +356,7 @@ class AiAgent extends Elf {
|
|
|
323
356
|
}
|
|
324
357
|
|
|
325
358
|
async readPDFpages(pdfPath) {
|
|
326
|
-
|
|
327
|
-
const provider = new Instance(this.state.host, this.state.headers);
|
|
359
|
+
await this._loadProvider();
|
|
328
360
|
|
|
329
361
|
const images = await getPDFImages(pdfPath);
|
|
330
362
|
const descriptions = [];
|
|
@@ -346,7 +378,7 @@ class AiAgent extends Elf {
|
|
|
346
378
|
],
|
|
347
379
|
},
|
|
348
380
|
];
|
|
349
|
-
const result = await
|
|
381
|
+
const result = await this._provider.chat({
|
|
350
382
|
model: this.state.model,
|
|
351
383
|
messages,
|
|
352
384
|
});
|
|
@@ -359,24 +391,28 @@ class AiAgent extends Elf {
|
|
|
359
391
|
if (!rawText) {
|
|
360
392
|
return null;
|
|
361
393
|
}
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
return await provider.embed({
|
|
394
|
+
await this._loadProvider();
|
|
395
|
+
const vectors = await this._provider.embed({
|
|
365
396
|
model: this.state.model,
|
|
366
397
|
input: rawText,
|
|
367
398
|
});
|
|
399
|
+
return vectorToSqlLiteral(vectors);
|
|
368
400
|
}
|
|
369
401
|
|
|
370
402
|
async embedInBatch(rawTexts = []) {
|
|
371
403
|
if (rawTexts.length === 0) {
|
|
372
404
|
return [];
|
|
373
405
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
406
|
+
|
|
407
|
+
await this._batchLock.lock(this.id);
|
|
408
|
+
this.quest.defer(() => this._batchLock.unlock(this.id));
|
|
409
|
+
|
|
410
|
+
await this._loadProvider();
|
|
411
|
+
const res = await this._provider.embedInBatch({
|
|
377
412
|
model: this.state.model,
|
|
378
413
|
input: rawTexts,
|
|
379
414
|
});
|
|
415
|
+
return res.map((v) => vectorToSqlLiteral(v));
|
|
380
416
|
}
|
|
381
417
|
|
|
382
418
|
async set(contextId, messages) {
|
|
@@ -384,8 +420,7 @@ class AiAgent extends Elf {
|
|
|
384
420
|
}
|
|
385
421
|
|
|
386
422
|
async chat(contextId, question, userDesktopId, sessionId = null) {
|
|
387
|
-
|
|
388
|
-
const provider = new Instance(this.state.host, this.state.headers);
|
|
423
|
+
await this._loadProvider();
|
|
389
424
|
const contextMessages = this.state.messages[contextId] || [];
|
|
390
425
|
const messages = [
|
|
391
426
|
{role: 'system', content: this.state.prompt},
|
|
@@ -406,7 +441,7 @@ class AiAgent extends Elf {
|
|
|
406
441
|
this.log.dbg(`🐈 Chat session started : ${sessionId}`);
|
|
407
442
|
}
|
|
408
443
|
|
|
409
|
-
const result = await
|
|
444
|
+
const result = await this._provider.chat({
|
|
410
445
|
model: this.state.model,
|
|
411
446
|
messages,
|
|
412
447
|
options,
|
|
@@ -474,9 +509,7 @@ class AiAgent extends Elf {
|
|
|
474
509
|
if (this.state.provider !== 'ollama') {
|
|
475
510
|
throw new Error('Streaming API not impl.');
|
|
476
511
|
}
|
|
477
|
-
|
|
478
|
-
const provider = new Instance(this.state.host, this.state.headers);
|
|
479
|
-
const feedId = await this.newQuestFeed();
|
|
512
|
+
await this._loadProvider();
|
|
480
513
|
|
|
481
514
|
const contextMessages = this.state.messages[contextId] || [];
|
|
482
515
|
const messages = [
|
|
@@ -491,7 +524,7 @@ class AiAgent extends Elf {
|
|
|
491
524
|
const pState = this.state._state.toJS();
|
|
492
525
|
options = pState?.options;
|
|
493
526
|
}
|
|
494
|
-
const response = await
|
|
527
|
+
const response = await this._provider.chat({
|
|
495
528
|
model: this.state.model,
|
|
496
529
|
messages,
|
|
497
530
|
options,
|
|
@@ -509,9 +542,7 @@ class AiAgent extends Elf {
|
|
|
509
542
|
}
|
|
510
543
|
|
|
511
544
|
async resumeExchange(resumePrompt, contextId) {
|
|
512
|
-
|
|
513
|
-
const provider = new Instance(this.state.host, this.state.headers);
|
|
514
|
-
const feedId = await this.newQuestFeed();
|
|
545
|
+
await this._loadProvider();
|
|
515
546
|
const contextMessages = this.state.messages[contextId] || [];
|
|
516
547
|
|
|
517
548
|
let options;
|
|
@@ -524,7 +555,7 @@ class AiAgent extends Elf {
|
|
|
524
555
|
.map((m) => `${m.role}: ${m.content}`)
|
|
525
556
|
.join('\n');
|
|
526
557
|
|
|
527
|
-
return await
|
|
558
|
+
return await this._provider.gen({
|
|
528
559
|
model: this.state.model,
|
|
529
560
|
prompt,
|
|
530
561
|
system: resumePrompt,
|
|
@@ -534,8 +565,7 @@ class AiAgent extends Elf {
|
|
|
534
565
|
}
|
|
535
566
|
|
|
536
567
|
async react(reactPrompt, contextId, question) {
|
|
537
|
-
|
|
538
|
-
const provider = new Instance(this.state.host, this.state.headers);
|
|
568
|
+
await this._loadProvider();
|
|
539
569
|
const feedId = await this.newQuestFeed();
|
|
540
570
|
const contextMessages = this.state.messages[contextId] || [];
|
|
541
571
|
const messages = [
|
|
@@ -550,7 +580,7 @@ class AiAgent extends Elf {
|
|
|
550
580
|
options = this.state.options.toJS();
|
|
551
581
|
}
|
|
552
582
|
|
|
553
|
-
const answer = await
|
|
583
|
+
const answer = await this._provider.chat({
|
|
554
584
|
model: this.state.model,
|
|
555
585
|
messages,
|
|
556
586
|
options,
|
package/lib/llm/utils.js
CHANGED
|
@@ -276,7 +276,60 @@ const buildPromptFromArticles = (articles, question, maxTokens) => {
|
|
|
276
276
|
return context.trim();
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Split an array into smaller chunks of a given size.
|
|
281
|
+
*
|
|
282
|
+
* @param {Array} array - The array to be chunked.
|
|
283
|
+
* @param {number} size - The size of each chunk.
|
|
284
|
+
* @returns {Array[]} An array of chunked subarrays.
|
|
285
|
+
*/
|
|
286
|
+
function chunkArray(array, size) {
|
|
287
|
+
const chunks = [];
|
|
288
|
+
for (let startIndex = 0; startIndex < array.length; startIndex += size) {
|
|
289
|
+
chunks.push(array.slice(startIndex, startIndex + size));
|
|
290
|
+
}
|
|
291
|
+
return chunks;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Perform batched embedding of content chunks, associating each embedding
|
|
296
|
+
* with its original contentId and key.
|
|
297
|
+
*
|
|
298
|
+
* @param {object} agent - The AI agent instance with an embedInBatch method.
|
|
299
|
+
* @param {Array<{ contentId: string, key: string, chunk: string }>} allChunks - Array of content to embed.
|
|
300
|
+
* @param {number} [batchSize=500] - Maximum number of chunks per embedding request.
|
|
301
|
+
* @returns {Promise<object>} A map of embeddings structured as:
|
|
302
|
+
* { [contentId]: { [key]: { chunk, embedding } } }
|
|
303
|
+
*/
|
|
304
|
+
async function embedChunksInBatch(agent, allChunks, batchSize = 500) {
|
|
305
|
+
const embeddingsByContent = {};
|
|
306
|
+
|
|
307
|
+
for (let start = 0; start < allChunks.length; start += batchSize) {
|
|
308
|
+
const currentBatch = allChunks.slice(start, start + batchSize);
|
|
309
|
+
const inputTexts = currentBatch.map(({chunk}) => chunk);
|
|
310
|
+
const embeddedVectors = await agent.embedInBatch(inputTexts);
|
|
311
|
+
|
|
312
|
+
for (let i = 0; i < embeddedVectors.length; i++) {
|
|
313
|
+
const {contentId, key, chunk} = currentBatch[i];
|
|
314
|
+
const embedding = embeddedVectors[i];
|
|
315
|
+
|
|
316
|
+
if (!embeddingsByContent[contentId]) {
|
|
317
|
+
embeddingsByContent[contentId] = {};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
embeddingsByContent[contentId][key] = {
|
|
321
|
+
chunk,
|
|
322
|
+
embedding,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return embeddingsByContent;
|
|
328
|
+
}
|
|
329
|
+
|
|
279
330
|
module.exports = {
|
|
331
|
+
chunkArray,
|
|
332
|
+
embedChunksInBatch,
|
|
280
333
|
extractTitlesAndContent,
|
|
281
334
|
html2chunks,
|
|
282
335
|
splitLongText,
|