Skip to content

Commit fb60119

Browse files
committed
feat(chatbot): coordination layer between chatbots and Chatwoot
- Create ChatbotChatwootService for coordination logic - Add agent check in BaseChatbotController.emit() before bot processes - Auto-pause bot sessions when human agent responds from Chatwoot - Auto-resolve Chatwoot conversation when Typebot flow completes - Session lifecycle: nullify closed sessions for new conversations - Add debug logging throughout emit() flow - Wire ChatbotChatwootService in server.module and ChatbotController
1 parent 7ed6a67 commit fb60119

8 files changed

Lines changed: 463 additions & 9 deletions

File tree

src/api/integrations/chatbot/base-chatbot.controller.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ import { getConversationMessage } from '@utils/getConversationMessage';
1111
import { BaseChatbotDto } from './base-chatbot.dto';
1212
import { ChatbotController, ChatbotControllerInterface, EmitData } from './chatbot.controller';
1313

14+
// Lazy getter to avoid circular dependency at module load time
15+
// chatbotChatwootService is resolved at runtime when emit() is called
16+
let _chatbotChatwootService: any = null;
17+
function getChatbotChatwootService() {
18+
if (!_chatbotChatwootService) {
19+
try {
20+
// eslint-disable-next-line @typescript-eslint/no-var-requires
21+
const mod = require('@api/server.module');
22+
_chatbotChatwootService = mod.chatbotChatwootService;
23+
} catch {
24+
return null;
25+
}
26+
}
27+
return _chatbotChatwootService;
28+
}
29+
1430
// Common settings interface for all chatbot integrations
1531
export interface ChatbotSettings {
1632
expire: number;
@@ -789,26 +805,36 @@ export abstract class BaseChatbotController<BotType = any, BotData extends BaseC
789805
if (!this.integrationEnabled) return;
790806

791807
try {
808+
this.logger.log(`[${this.integrationName}] emit() called for remoteJid: ${remoteJid}, instanceId: ${instance.instanceId}`);
809+
792810
const settings = await this.settingsRepository.findFirst({
793811
where: {
794812
instanceId: instance.instanceId,
795813
},
796814
});
815+
this.logger.log(`[${this.integrationName}] settings found: ${!!settings}`);
797816

798-
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
817+
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) {
818+
this.logger.log(`[${this.integrationName}] remoteJid IGNORED by ignoreJids`);
819+
return;
820+
}
799821

800-
const session = await this.getSession(remoteJid, instance);
822+
let session = await this.getSession(remoteJid, instance);
823+
this.logger.log(`[${this.integrationName}] session: id=${session?.id}, status=${session?.status}, botId=${session?.botId}`);
801824

802825
const content = getConversationMessage(msg);
826+
this.logger.log(`[${this.integrationName}] content extracted: "${content}"`);
803827

804828
// Get integration type
805829
// const integrationType = this.getIntegrationType();
806830

807831
// Find a bot for this message
808832
let findBot: any = await this.findBotTrigger(this.botRepository, content, instance, session);
833+
this.logger.log(`[${this.integrationName}] findBot: id=${findBot?.id}, triggerType=${findBot?.triggerType}, enabled=${findBot?.enabled}`);
809834

810835
// If no bot is found, try to use fallback
811836
if (!findBot) {
837+
this.logger.log(`[${this.integrationName}] no bot found, trying fallback...`);
812838
const fallback = await this.settingsRepository.findFirst({
813839
where: {
814840
instanceId: instance.instanceId,
@@ -826,16 +852,21 @@ export abstract class BaseChatbotController<BotType = any, BotData extends BaseC
826852
});
827853

828854
findBot = findFallback;
855+
this.logger.log(`[${this.integrationName}] fallback bot found: ${!!findFallback}`);
829856
} else {
857+
this.logger.log(`[${this.integrationName}] no fallback configured, returning`);
830858
return;
831859
}
832860
}
833861

834862
// If we still don't have a bot, return
835863
if (!findBot) {
864+
this.logger.log(`[${this.integrationName}] no bot found after fallback, returning`);
836865
return;
837866
}
838867

868+
this.logger.log(`[${this.integrationName}] processing bot: ${findBot.id}, expire=${findBot.expire}, debounce=${findBot.debounceTime}`);
869+
839870
// Collect settings with fallbacks to default settings
840871
let expire = findBot.expire;
841872
let keywordFinish = findBot.keywordFinish;
@@ -868,6 +899,8 @@ export abstract class BaseChatbotController<BotType = any, BotData extends BaseC
868899
participant: string;
869900
};
870901

902+
this.logger.log(`[${this.integrationName}] key: fromMe=${key.fromMe}, remoteJid=${key.remoteJid}`);
903+
871904
// Handle stopping the bot if message is from me
872905
if (stopBotFromMe && key.fromMe && session) {
873906
await this.prismaRepository.integrationSession.update({
@@ -893,12 +926,38 @@ export abstract class BaseChatbotController<BotType = any, BotData extends BaseC
893926

894927
// Skip if not listening to messages from me
895928
if (!listeningFromMe && key.fromMe) {
929+
this.logger.log(`[${this.integrationName}] skipping: fromMe=true, listeningFromMe=false`);
896930
return;
897931
}
898932

899-
// Skip if session exists but not awaiting user input
933+
// If session is closed, nullify it so processBot treats it as a new conversation
900934
if (session && session.status === 'closed') {
901-
return;
935+
this.logger.log(`[${this.integrationName}] session is closed, nullifying to start new conversation`);
936+
session = null;
937+
}
938+
939+
// Coordination layer: check if Chatwoot has a human agent assigned
940+
// If so, the bot should not process this message
941+
const coordinationService = getChatbotChatwootService();
942+
if (coordinationService?.isEnabled() && msg?.chatwootConversationId) {
943+
const hasHuman = await coordinationService.hasHumanAgentAssigned(
944+
instance.instanceId,
945+
msg.chatwootConversationId,
946+
);
947+
if (hasHuman) {
948+
this.logger.log(
949+
`[${this.integrationName}] Chatwoot conversation ${msg.chatwootConversationId} has human agent, skipping bot`,
950+
);
951+
// If there's an active bot session, pause it
952+
if (session && session.status === 'opened') {
953+
await this.prismaRepository.integrationSession.update({
954+
where: { id: session.id },
955+
data: { status: 'paused' },
956+
});
957+
this.logger.log(`[${this.integrationName}] Paused bot session ${session.id} due to human agent`);
958+
}
959+
return;
960+
}
902961
}
903962

904963
// Merged settings
@@ -917,6 +976,8 @@ export abstract class BaseChatbotController<BotType = any, BotData extends BaseC
917976
timePerChar,
918977
};
919978

979+
this.logger.log(`[${this.integrationName}] proceeding to processBot, debounceTime=${debounceTime}`);
980+
920981
// Process with debounce if needed
921982
if (debounceTime && debounceTime > 0) {
922983
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
@@ -944,7 +1005,8 @@ export abstract class BaseChatbotController<BotType = any, BotData extends BaseC
9441005
);
9451006
}
9461007
} catch (error) {
947-
this.logger.error(error);
1008+
this.logger.error(`[${this.integrationName}] emit() ERROR: ${error?.message || error}`);
1009+
this.logger.error(error?.stack || error);
9481010
}
9491011
}
9501012
}

0 commit comments

Comments
 (0)