bloby-bot 0.25.1 → 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.1",
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;
@@ -245,7 +247,7 @@ export class ChannelManager {
245
247
 
246
248
  /** Format a bot reply with the agent's name prefix (for admin & assistant messages, NOT customer) */
247
249
  private formatBotReply(text: string, botName: string): string {
248
- return `🤖 *${botName}:*\n\n${text}`;
250
+ return `🤖 *${botName}:*\n\n\`${text}\``;
249
251
  }
250
252
 
251
253
  private handleStatusChange(status: ChannelStatus) {
@@ -362,19 +364,57 @@ export class ChannelManager {
362
364
  return;
363
365
  }
364
366
 
365
- // Assistant mode — triggered message in someone else's chat
367
+ // Assistant mode — triggered message in someone else's chat → route through admin (shared brain)
366
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
+
367
406
  const message: InboundMessage = {
368
407
  channel,
369
- sender: sender.replace(/@.*/, ''),
408
+ sender: phone,
370
409
  senderName,
371
410
  role: 'assistant',
372
- text: combinedText,
411
+ text: enrichedText,
373
412
  rawSender: sender,
374
413
  attachments: attachments.length > 0 ? attachments : undefined,
375
414
  };
376
- log.info(`[channels] Assistant mode | triggered in chat with ${message.sender} | "${combinedText.slice(0, 60)}"`);
377
- 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);
378
418
  return;
379
419
  }
380
420
 
@@ -415,7 +455,7 @@ export class ChannelManager {
415
455
  return 'customer';
416
456
  }
417
457
 
418
- /** 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 */
419
459
  private async handleAdminMessage(msg: InboundMessage) {
420
460
  const { workerApi, broadcastBloby, getModel } = this.opts;
421
461
  const model = getModel();
@@ -474,8 +514,10 @@ export class ChannelManager {
474
514
  }
475
515
  } catch {}
476
516
 
477
- // Channel context — tells the agent this is a WhatsApp message, respond naturally
478
- 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`;
479
521
 
480
522
  // Convert inbound attachments to agent format
481
523
  const agentAttachments: AgentAttachment[] | undefined = msg.attachments?.map((att) => ({
@@ -485,7 +527,7 @@ export class ChannelManager {
485
527
  data: att.data,
486
528
  }));
487
529
 
488
- // Show "typing..." while the agent processes
530
+ // Show "typing..." in the correct chat
489
531
  this.startTyping(msg.channel, msg.rawSender);
490
532
 
491
533
  // Track text chunks for WhatsApp — lives for the conversation lifetime
@@ -501,24 +543,37 @@ export class ChannelManager {
501
543
  waChunkBuf += eventData.token;
502
544
  }
503
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
+
504
550
  // Agent paused to use a tool — send accumulated text as an intermediate WhatsApp message
505
551
  if (type === 'bot:tool' && waChunkBuf.trim()) {
506
- this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(waChunkBuf.trim(), botName)).catch((err) => {
552
+ this.sendMessage(target.channel, target.rawSender, this.formatBotReply(waChunkBuf.trim(), botName)).catch((err) => {
507
553
  log.warn(`[channels] Failed to send WhatsApp chunk: ${err.message}`);
508
554
  });
509
555
  waChunkBuf = '';
510
556
  }
511
557
 
512
558
  if (type === 'bot:response' && eventData.content) {
513
- // Send remaining text
559
+ // Send remaining text to the correct chat
514
560
  const remaining = waChunkBuf.trim();
515
561
  if (remaining) {
516
- this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(remaining, botName)).catch((err) => {
562
+ this.sendMessage(target.channel, target.rawSender, this.formatBotReply(remaining, botName)).catch((err) => {
517
563
  log.warn(`[channels] Failed to send WhatsApp reply: ${err.message}`);
518
564
  });
519
565
  waChunkBuf = '';
520
566
  }
521
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
+
522
577
  // Save response to DB
523
578
  workerApi(`/api/conversations/${convId}/messages`, 'POST', {
524
579
  role: 'assistant',
@@ -540,6 +595,13 @@ export class ChannelManager {
540
595
  }, { botName, humanName }, recentMessages);
541
596
  }
542
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
+
543
605
  // Push the message into the live conversation
544
606
  const channelContent = channelContext + msg.text;
545
607
  pushMessage(convId, channelContent, agentAttachments);
@@ -702,146 +764,6 @@ export class ChannelManager {
702
764
  log.info(`[channels] Assistant context stored: ${bufferKey} | ${buffer.length} msgs | [${label}]: "${text.slice(0, 60)}"`);
703
765
  }
704
766
 
705
- /** Handle a triggered assistant message — runs one-shot agent with conversation context */
706
- private async handleAssistantMessage(msg: InboundMessage, channelConfig: ChannelConfig) {
707
- const agentKey = `${msg.channel}:${msg.sender}`;
708
-
709
- // Check concurrent limit
710
- if (this.activeAgents.size >= MAX_CONCURRENT_AGENTS && !this.activeAgents.has(agentKey)) {
711
- log.info(`[channels] Max concurrent agents reached — queuing assistant message for ${msg.sender}`);
712
- this.messageQueue.push(msg);
713
- return;
714
- }
715
-
716
- const { workerApi, getModel } = this.opts;
717
- const model = getModel();
718
-
719
- // Extract command after trigger: "Ok.\n\n@bloby: do X" → "do X"
720
- const config = loadConfig();
721
- const botName = config.username || 'bloby';
722
- const triggerRegex = new RegExp(`@${botName}[:\\s]+`, 'i');
723
- const triggerMatch = msg.text.match(triggerRegex);
724
- let cleanText = msg.text;
725
- if (triggerMatch && triggerMatch.index !== undefined) {
726
- cleanText = msg.text.slice(triggerMatch.index + triggerMatch[0].length).trim();
727
- }
728
-
729
- // Load SCRIPT.md from configured skill
730
- const scriptPrompt = this.loadActiveScript(channelConfig);
731
-
732
- // Fetch agent name
733
- let agentBotName = 'Bloby', humanName = 'Human';
734
- try {
735
- const status = await workerApi('/api/onboard/status');
736
- agentBotName = status.agentName || 'Bloby';
737
- humanName = status.userName || 'Human';
738
- } catch {}
739
-
740
- // Get conversation buffer (already populated by storeAssistantContext)
741
- let buffer = this.customerBuffers.get(agentKey);
742
- if (!buffer) {
743
- buffer = [];
744
- this.customerBuffers.set(agentKey, buffer);
745
- }
746
-
747
- log.info(`[channels] Assistant trigger: agentKey=${agentKey} | buffer=${buffer.length} msgs | cleanText="${cleanText.slice(0, 80)}"`);
748
- if (buffer.length > 0) {
749
- log.info(`[channels] Assistant context preview: ${buffer.slice(-5).map((m) => `[${m.role}] ${m.content.slice(0, 50)}`).join(' | ')}`);
750
- }
751
-
752
- // All buffered messages are context for the agent
753
- const recentMessages: RecentMessage[] = buffer.map((m) => ({
754
- role: m.role,
755
- content: m.content,
756
- }));
757
-
758
- // Load per-contact memory from the skill's customer_data directory
759
- let contactMemory = '';
760
- try {
761
- const customerDataDir = this.getSkillCustomerDataDir(channelConfig);
762
- if (customerDataDir) {
763
- const memoryPath = path.join(WORKSPACE_DIR, customerDataDir, `${msg.sender}.md`);
764
- if (fs.existsSync(memoryPath)) {
765
- contactMemory = fs.readFileSync(memoryPath, 'utf-8').trim();
766
- }
767
- }
768
- } catch {}
769
-
770
- // Build enriched script with contact memory
771
- let enrichedScript = scriptPrompt;
772
- if (contactMemory && enrichedScript) {
773
- enrichedScript += `\n\n---\n# Contact History (${msg.sender})\n\n${contactMemory}`;
774
- }
775
-
776
- const channelContext = `[WhatsApp | ${msg.sender} | assistant${msg.senderName ? ` | ${msg.senderName}` : ''}]\n`;
777
-
778
- // Convert inbound attachments to agent format
779
- const agentAttachments: AgentAttachment[] | undefined = msg.attachments?.map((att) => ({
780
- type: 'image' as const,
781
- name: `whatsapp_image.${att.mediaType.split('/')[1] || 'jpg'}`,
782
- mediaType: att.mediaType,
783
- data: att.data,
784
- }));
785
-
786
- // Stable convId per contact
787
- const convId = `channel-${agentKey}`;
788
-
789
- this.activeAgents.set(agentKey, { sender: msg.sender, channel: msg.channel });
790
-
791
- // Show "typing..." while the agent processes
792
- this.startTyping(msg.channel, msg.rawSender);
793
-
794
- // Track text chunks for WhatsApp
795
- let waChunkBuf = '';
796
-
797
- startBlobyAgentQuery(
798
- convId,
799
- channelContext + cleanText,
800
- model,
801
- (type, eventData) => {
802
- // Accumulate text tokens
803
- if (type === 'bot:token' && eventData.token) {
804
- waChunkBuf += eventData.token;
805
- }
806
-
807
- // Agent paused to use a tool — send accumulated text as intermediate message
808
- if (type === 'bot:tool' && waChunkBuf.trim()) {
809
- this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(waChunkBuf.trim(), agentBotName)).catch((err) => {
810
- log.warn(`[channels] Failed to send assistant chunk: ${err.message}`);
811
- });
812
- waChunkBuf = '';
813
- }
814
-
815
- if (type === 'bot:response' && eventData.content) {
816
- // Add response to buffer for continuity across triggers
817
- buffer!.push({ role: 'assistant', content: eventData.content });
818
- if (buffer!.length > MAX_BUFFER_MESSAGES) {
819
- buffer!.splice(0, buffer!.length - MAX_BUFFER_MESSAGES);
820
- }
821
-
822
- // Send remaining text
823
- const remaining = waChunkBuf.trim();
824
- if (remaining) {
825
- this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(remaining, agentBotName)).catch((err) => {
826
- log.warn(`[channels] Failed to send assistant reply: ${err.message}`);
827
- });
828
- waChunkBuf = '';
829
- }
830
- }
831
-
832
- if (type === 'bot:done') {
833
- this.activeAgents.delete(agentKey);
834
- if (eventData.usedFileTools) this.opts.restartBackend();
835
- this.processQueue();
836
- }
837
- },
838
- agentAttachments,
839
- undefined,
840
- { botName: agentBotName, humanName },
841
- recentMessages,
842
- enrichedScript,
843
- );
844
- }
845
767
 
846
768
  /** Transcribe audio via the existing whisper endpoint */
847
769
  private async transcribeAudio(audioBase64: string): Promise<string | null> {