goblin-agents 0.1.2 → 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 +24 -4
- 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;
|
|
@@ -240,6 +252,7 @@ class AiAgent extends Elf {
|
|
|
240
252
|
|
|
241
253
|
_provider;
|
|
242
254
|
_conf;
|
|
255
|
+
_batchLock = getMutex;
|
|
243
256
|
|
|
244
257
|
/**
|
|
245
258
|
* @param {string} id
|
|
@@ -276,18 +289,19 @@ class AiAgent extends Elf {
|
|
|
276
289
|
async _loadProvider() {
|
|
277
290
|
const {state} = this;
|
|
278
291
|
const {provider, host, headers} = state;
|
|
292
|
+
const headersStr = JSON.stringify(headers);
|
|
279
293
|
if (
|
|
280
294
|
this._provider &&
|
|
281
295
|
this._conf.provider === provider &&
|
|
282
296
|
this._conf.host === host &&
|
|
283
|
-
this._conf.
|
|
297
|
+
this._conf.headersStr === headersStr
|
|
284
298
|
) {
|
|
285
299
|
return;
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
const Instance = LLMProvider.get(this.state.provider);
|
|
289
303
|
this._provider = new Instance(this.state.host, this.state.headers);
|
|
290
|
-
this._conf = {provider, host,
|
|
304
|
+
this._conf = {provider, host, headersStr};
|
|
291
305
|
}
|
|
292
306
|
|
|
293
307
|
/**
|
|
@@ -378,21 +392,27 @@ class AiAgent extends Elf {
|
|
|
378
392
|
return null;
|
|
379
393
|
}
|
|
380
394
|
await this._loadProvider();
|
|
381
|
-
|
|
395
|
+
const vectors = await this._provider.embed({
|
|
382
396
|
model: this.state.model,
|
|
383
397
|
input: rawText,
|
|
384
398
|
});
|
|
399
|
+
return vectorToSqlLiteral(vectors);
|
|
385
400
|
}
|
|
386
401
|
|
|
387
402
|
async embedInBatch(rawTexts = []) {
|
|
388
403
|
if (rawTexts.length === 0) {
|
|
389
404
|
return [];
|
|
390
405
|
}
|
|
406
|
+
|
|
407
|
+
await this._batchLock.lock(this.id);
|
|
408
|
+
this.quest.defer(() => this._batchLock.unlock(this.id));
|
|
409
|
+
|
|
391
410
|
await this._loadProvider();
|
|
392
|
-
|
|
411
|
+
const res = await this._provider.embedInBatch({
|
|
393
412
|
model: this.state.model,
|
|
394
413
|
input: rawTexts,
|
|
395
414
|
});
|
|
415
|
+
return res.map((v) => vectorToSqlLiteral(v));
|
|
396
416
|
}
|
|
397
417
|
|
|
398
418
|
async set(contextId, messages) {
|
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,
|