bloby-bot 0.25.0 → 0.25.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "releaseNotes": [
5
5
  "1. new stuff",
6
6
  "2. ",
@@ -70,6 +70,8 @@ export class ChannelManager {
70
70
  private customerBuffers = new Map<string, BufferedMessage[]>();
71
71
  /** Debounce buffers per sender (keyed by "channel:sender") */
72
72
  private debounceBuffers = new Map<string, DebounceEntry>();
73
+ /** Dynamic reply target for the admin live conversation (updated before each pushMessage) */
74
+ private waReplyTarget: { channel: ChannelType; rawSender: string; assistantBufferKey?: string } | null = null;
73
75
 
74
76
  constructor(opts: ChannelManagerOpts) {
75
77
  this.opts = opts;
@@ -243,6 +245,11 @@ export class ChannelManager {
243
245
 
244
246
  // ── Internal ──
245
247
 
248
+ /** Format a bot reply with the agent's name prefix (for admin & assistant messages, NOT customer) */
249
+ private formatBotReply(text: string, botName: string): string {
250
+ return `🤖 *${botName}:*\n\n\`${text}\``;
251
+ }
252
+
246
253
  private handleStatusChange(status: ChannelStatus) {
247
254
  for (const listener of this.statusListeners) {
248
255
  listener(status);
@@ -286,9 +293,9 @@ export class ChannelManager {
286
293
  // Store every message for context (both mine and theirs)
287
294
  this.storeAssistantContext(channel, sender, senderName, text, fromMe);
288
295
 
289
- // Only continue if it's me AND the message has the trigger
296
+ // Only continue if it's me AND the message contains the trigger
290
297
  const botName = loadConfig().username || 'bloby';
291
- const triggerPattern = new RegExp(`^@${botName}[:\\s]`, 'i');
298
+ const triggerPattern = new RegExp(`(?:^|\\n)\\s*@${botName}[:\\s]`, 'i');
292
299
  if (!fromMe || !triggerPattern.test(text)) return;
293
300
  // Falls through to debounce → flushDebounce → handleAssistantMessage
294
301
  }
@@ -357,19 +364,57 @@ export class ChannelManager {
357
364
  return;
358
365
  }
359
366
 
360
- // Assistant mode — triggered message in someone else's chat
367
+ // Assistant mode — triggered message in someone else's chat → route through admin (shared brain)
361
368
  if (mode === 'assistant') {
369
+ const phone = sender.replace(/@.*/, '');
370
+ const bufferKey = `${channel}:${phone}`;
371
+ const buffer = this.customerBuffers.get(bufferKey) || [];
372
+
373
+ // Strip trigger prefix
374
+ const cfgBotName = loadConfig().username || 'bloby';
375
+ const triggerRegex = new RegExp(`@${cfgBotName}[:\\s]+`, 'i');
376
+ const triggerMatch = combinedText.match(triggerRegex);
377
+ let cleanText = combinedText;
378
+ if (triggerMatch && triggerMatch.index !== undefined) {
379
+ cleanText = combinedText.slice(triggerMatch.index + triggerMatch[0].length).trim();
380
+ }
381
+
382
+ // Load skill context (SCRIPT.md + contact memory)
383
+ const scriptPrompt = this.loadActiveScript(channelConfig);
384
+ let contactMemory = '';
385
+ try {
386
+ const customerDataDir = this.getSkillCustomerDataDir(channelConfig);
387
+ if (customerDataDir) {
388
+ const memoryPath = path.join(WORKSPACE_DIR, customerDataDir, `${phone}.md`);
389
+ if (fs.existsSync(memoryPath)) {
390
+ contactMemory = fs.readFileSync(memoryPath, 'utf-8').trim();
391
+ }
392
+ }
393
+ } catch {}
394
+
395
+ // Build enriched text: skill instructions + conversation context + command
396
+ let enrichedText = '';
397
+ if (scriptPrompt) enrichedText += `# Assistant Skill Instructions\n${scriptPrompt}\n\n---\n`;
398
+ if (contactMemory) enrichedText += `# Contact Memory (${phone})\n${contactMemory}\n\n---\n`;
399
+ if (buffer.length > 0) {
400
+ enrichedText += `# Recent conversation with ${senderName || phone}\n`;
401
+ enrichedText += buffer.map((m) => m.content).join('\n');
402
+ enrichedText += '\n\n---\n';
403
+ }
404
+ enrichedText += cleanText;
405
+
362
406
  const message: InboundMessage = {
363
407
  channel,
364
- sender: sender.replace(/@.*/, ''),
408
+ sender: phone,
365
409
  senderName,
366
410
  role: 'assistant',
367
- text: combinedText,
411
+ text: enrichedText,
368
412
  rawSender: sender,
369
413
  attachments: attachments.length > 0 ? attachments : undefined,
370
414
  };
371
- log.info(`[channels] Assistant mode | triggered in chat with ${message.sender} | "${combinedText.slice(0, 60)}"`);
372
- await this.handleAssistantMessage(message, channelConfig);
415
+
416
+ log.info(`[channels] Assistant mode | triggered in chat with ${phone} | buffer=${buffer.length} msgs | "${cleanText.slice(0, 60)}"`);
417
+ await this.handleAdminMessage(message);
373
418
  return;
374
419
  }
375
420
 
@@ -410,7 +455,7 @@ export class ChannelManager {
410
455
  return 'customer';
411
456
  }
412
457
 
413
- /** Handle message from an admin — mirrors to chat conversation, uses main system prompt */
458
+ /** Handle message from an admin (or assistant trigger) — mirrors to chat conversation, uses main system prompt */
414
459
  private async handleAdminMessage(msg: InboundMessage) {
415
460
  const { workerApi, broadcastBloby, getModel } = this.opts;
416
461
  const model = getModel();
@@ -469,8 +514,10 @@ export class ChannelManager {
469
514
  }
470
515
  } catch {}
471
516
 
472
- // Channel context — tells the agent this is a WhatsApp message, respond naturally
473
- const channelContext = `[WhatsApp | ${msg.sender} | admin]\n`;
517
+ // Channel context — dynamic role (admin for self-chat, assistant for triggered messages)
518
+ const roleTag = msg.senderName && msg.role === 'assistant'
519
+ ? `${msg.role} | ${msg.senderName}` : msg.role;
520
+ const channelContext = `[WhatsApp | ${msg.sender} | ${roleTag}]\n`;
474
521
 
475
522
  // Convert inbound attachments to agent format
476
523
  const agentAttachments: AgentAttachment[] | undefined = msg.attachments?.map((att) => ({
@@ -480,7 +527,7 @@ export class ChannelManager {
480
527
  data: att.data,
481
528
  }));
482
529
 
483
- // Show "typing..." while the agent processes
530
+ // Show "typing..." in the correct chat
484
531
  this.startTyping(msg.channel, msg.rawSender);
485
532
 
486
533
  // Track text chunks for WhatsApp — lives for the conversation lifetime
@@ -496,24 +543,37 @@ export class ChannelManager {
496
543
  waChunkBuf += eventData.token;
497
544
  }
498
545
 
546
+ // Use dynamic reply target (self-chat or contact's chat depending on latest push)
547
+ const target = this.waReplyTarget;
548
+ if (!target) return;
549
+
499
550
  // Agent paused to use a tool — send accumulated text as an intermediate WhatsApp message
500
551
  if (type === 'bot:tool' && waChunkBuf.trim()) {
501
- this.sendMessage(msg.channel, msg.rawSender, waChunkBuf.trim()).catch((err) => {
552
+ this.sendMessage(target.channel, target.rawSender, this.formatBotReply(waChunkBuf.trim(), botName)).catch((err) => {
502
553
  log.warn(`[channels] Failed to send WhatsApp chunk: ${err.message}`);
503
554
  });
504
555
  waChunkBuf = '';
505
556
  }
506
557
 
507
558
  if (type === 'bot:response' && eventData.content) {
508
- // Send remaining text
559
+ // Send remaining text to the correct chat
509
560
  const remaining = waChunkBuf.trim();
510
561
  if (remaining) {
511
- this.sendMessage(msg.channel, msg.rawSender, remaining).catch((err) => {
562
+ this.sendMessage(target.channel, target.rawSender, this.formatBotReply(remaining, botName)).catch((err) => {
512
563
  log.warn(`[channels] Failed to send WhatsApp reply: ${err.message}`);
513
564
  });
514
565
  waChunkBuf = '';
515
566
  }
516
567
 
568
+ // If this was an assistant response, store in the contact's context buffer
569
+ if (target.assistantBufferKey) {
570
+ const buf = this.customerBuffers.get(target.assistantBufferKey);
571
+ if (buf) {
572
+ buf.push({ role: 'assistant', content: eventData.content });
573
+ if (buf.length > MAX_BUFFER_MESSAGES) buf.splice(0, buf.length - MAX_BUFFER_MESSAGES);
574
+ }
575
+ }
576
+
517
577
  // Save response to DB
518
578
  workerApi(`/api/conversations/${convId}/messages`, 'POST', {
519
579
  role: 'assistant',
@@ -535,6 +595,13 @@ export class ChannelManager {
535
595
  }, { botName, humanName }, recentMessages);
536
596
  }
537
597
 
598
+ // Set reply target BEFORE pushing — callback reads this to know where to send
599
+ this.waReplyTarget = {
600
+ channel: msg.channel,
601
+ rawSender: msg.rawSender,
602
+ assistantBufferKey: msg.role === 'assistant' ? `${msg.channel}:${msg.sender}` : undefined,
603
+ };
604
+
538
605
  // Push the message into the live conversation
539
606
  const channelContent = channelContext + msg.text;
540
607
  pushMessage(convId, channelContent, agentAttachments);
@@ -681,150 +748,22 @@ export class ChannelManager {
681
748
  text: string,
682
749
  fromMe: boolean,
683
750
  ) {
684
- const bufferKey = `${channel}:${sender}`;
751
+ // Normalize key to phone number (strip @lid / @s.whatsapp.net) — must match handleAssistantMessage's agentKey
752
+ const phone = sender.replace(/@.*/, '');
753
+ const bufferKey = `${channel}:${phone}`;
685
754
  let buffer = this.customerBuffers.get(bufferKey);
686
755
  if (!buffer) {
687
756
  buffer = [];
688
757
  this.customerBuffers.set(bufferKey, buffer);
689
758
  }
690
- const label = fromMe ? 'me' : (senderName || sender.replace(/@.*/, ''));
759
+ const label = fromMe ? 'me' : (senderName || phone);
691
760
  buffer.push({ role: 'user', content: `[${label}]: ${text}` });
692
761
  if (buffer.length > MAX_BUFFER_MESSAGES) {
693
762
  buffer.splice(0, buffer.length - MAX_BUFFER_MESSAGES);
694
763
  }
764
+ log.info(`[channels] Assistant context stored: ${bufferKey} | ${buffer.length} msgs | [${label}]: "${text.slice(0, 60)}"`);
695
765
  }
696
766
 
697
- /** Handle a triggered assistant message — runs one-shot agent with conversation context */
698
- private async handleAssistantMessage(msg: InboundMessage, channelConfig: ChannelConfig) {
699
- const agentKey = `${msg.channel}:${msg.sender}`;
700
-
701
- // Check concurrent limit
702
- if (this.activeAgents.size >= MAX_CONCURRENT_AGENTS && !this.activeAgents.has(agentKey)) {
703
- log.info(`[channels] Max concurrent agents reached — queuing assistant message for ${msg.sender}`);
704
- this.messageQueue.push(msg);
705
- return;
706
- }
707
-
708
- const { workerApi, getModel } = this.opts;
709
- const model = getModel();
710
-
711
- // Strip trigger prefix: "@bloby: do X" → "do X"
712
- const config = loadConfig();
713
- const botName = config.username || 'bloby';
714
- const triggerRegex = new RegExp(`^@${botName}[:\\s]+`, 'i');
715
- const cleanText = msg.text.replace(triggerRegex, '').trim();
716
-
717
- // Load SCRIPT.md from configured skill
718
- const scriptPrompt = this.loadActiveScript(channelConfig);
719
-
720
- // Fetch agent name
721
- let agentBotName = 'Bloby', humanName = 'Human';
722
- try {
723
- const status = await workerApi('/api/onboard/status');
724
- agentBotName = status.agentName || 'Bloby';
725
- humanName = status.userName || 'Human';
726
- } catch {}
727
-
728
- // Get conversation buffer (already populated by storeAssistantContext)
729
- let buffer = this.customerBuffers.get(agentKey);
730
- if (!buffer) {
731
- buffer = [];
732
- this.customerBuffers.set(agentKey, buffer);
733
- }
734
-
735
- // All buffered messages are context for the agent
736
- const recentMessages: RecentMessage[] = buffer.map((m) => ({
737
- role: m.role,
738
- content: m.content,
739
- }));
740
-
741
- // Load per-contact memory from the skill's customer_data directory
742
- let contactMemory = '';
743
- try {
744
- const customerDataDir = this.getSkillCustomerDataDir(channelConfig);
745
- if (customerDataDir) {
746
- const memoryPath = path.join(WORKSPACE_DIR, customerDataDir, `${msg.sender}.md`);
747
- if (fs.existsSync(memoryPath)) {
748
- contactMemory = fs.readFileSync(memoryPath, 'utf-8').trim();
749
- }
750
- }
751
- } catch {}
752
-
753
- // Build enriched script with contact memory
754
- let enrichedScript = scriptPrompt;
755
- if (contactMemory && enrichedScript) {
756
- enrichedScript += `\n\n---\n# Contact History (${msg.sender})\n\n${contactMemory}`;
757
- }
758
-
759
- const channelContext = `[WhatsApp | ${msg.sender} | assistant${msg.senderName ? ` | ${msg.senderName}` : ''}]\n`;
760
-
761
- // Convert inbound attachments to agent format
762
- const agentAttachments: AgentAttachment[] | undefined = msg.attachments?.map((att) => ({
763
- type: 'image' as const,
764
- name: `whatsapp_image.${att.mediaType.split('/')[1] || 'jpg'}`,
765
- mediaType: att.mediaType,
766
- data: att.data,
767
- }));
768
-
769
- // Stable convId per contact
770
- const convId = `channel-${agentKey}`;
771
-
772
- this.activeAgents.set(agentKey, { sender: msg.sender, channel: msg.channel });
773
-
774
- // Show "typing..." while the agent processes
775
- this.startTyping(msg.channel, msg.rawSender);
776
-
777
- // Track text chunks for WhatsApp
778
- let waChunkBuf = '';
779
-
780
- startBlobyAgentQuery(
781
- convId,
782
- channelContext + cleanText,
783
- model,
784
- (type, eventData) => {
785
- // Accumulate text tokens
786
- if (type === 'bot:token' && eventData.token) {
787
- waChunkBuf += eventData.token;
788
- }
789
-
790
- // Agent paused to use a tool — send accumulated text as intermediate message
791
- if (type === 'bot:tool' && waChunkBuf.trim()) {
792
- this.sendMessage(msg.channel, msg.rawSender, waChunkBuf.trim()).catch((err) => {
793
- log.warn(`[channels] Failed to send assistant chunk: ${err.message}`);
794
- });
795
- waChunkBuf = '';
796
- }
797
-
798
- if (type === 'bot:response' && eventData.content) {
799
- // Add response to buffer for continuity across triggers
800
- buffer!.push({ role: 'assistant', content: eventData.content });
801
- if (buffer!.length > MAX_BUFFER_MESSAGES) {
802
- buffer!.splice(0, buffer!.length - MAX_BUFFER_MESSAGES);
803
- }
804
-
805
- // Send remaining text
806
- const remaining = waChunkBuf.trim();
807
- if (remaining) {
808
- this.sendMessage(msg.channel, msg.rawSender, remaining).catch((err) => {
809
- log.warn(`[channels] Failed to send assistant reply: ${err.message}`);
810
- });
811
- waChunkBuf = '';
812
- }
813
- }
814
-
815
- if (type === 'bot:done') {
816
- this.activeAgents.delete(agentKey);
817
- if (eventData.usedFileTools) this.opts.restartBackend();
818
- this.processQueue();
819
- }
820
- },
821
- agentAttachments,
822
- undefined,
823
- { botName: agentBotName, humanName },
824
- recentMessages,
825
- enrichedScript,
826
- );
827
- }
828
767
 
829
768
  /** Transcribe audio via the existing whisper endpoint */
830
769
  private async transcribeAudio(audioBase64: string): Promise<string | null> {