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.
@@ -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.headers === headers
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, headers};
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
- return await this._provider.embed({
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
- return await this._provider.embedInBatch({
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "AI Agents framework",
5
5
  "author": "Samuel Loup",
6
6
  "license": "MIT",