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.
@@ -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
- const Instance = LLMProvider.get(this.state.provider);
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 provider.gen({
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
- const Instance = LLMProvider.get(this.state.provider);
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 provider.chat({
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
- const Instance = LLMProvider.get(this.state.provider);
363
- const provider = new Instance(this.state.host, this.state.headers);
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
- const Instance = LLMProvider.get(this.state.provider);
375
- const provider = new Instance(this.state.host, this.state.headers);
376
- return await provider.embedInBatch({
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
- const Instance = LLMProvider.get(this.state.provider);
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 provider.chat({
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
- const Instance = LLMProvider.get(this.state.provider);
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 provider.chat({
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
- const Instance = LLMProvider.get(this.state.provider);
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 provider.gen({
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
- const Instance = LLMProvider.get(this.state.provider);
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 provider.chat({
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-agents",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "AI Agents framework",
5
5
  "author": "Samuel Loup",
6
6
  "license": "MIT",