Skip to content

Commit 1cfeafc

Browse files
committed
feat(chatbot): configurable coordination via env vars and per-instance override
- Add ChatbotCoordination type and CHATBOT_COORDINATION config in env.config.ts - CHATBOT_COORDINATION_CHECK_AGENT (default: true) - CHATBOT_COORDINATION_AUTO_PAUSE (default: true) - CHATBOT_COORDINATION_AUTO_RESOLVE (default: true) - CHATBOT_COORDINATION_MANAGE_ENABLED (default: true) - Add coordinationSettings Json field to Chatwoot Prisma model (PostgreSQL + MySQL) - Create database migrations for both providers - Add CoordinationConfig interface and getCoordinationConfig() to ChatbotChatwootService - Per-instance override (Chatwoot.coordinationSettings) takes precedence over env vars - Wrap auto-pause in chatwoot.service.ts with autoPause config check - Wrap auto-resolve in typebot.service.ts with autoResolve config check - Wrap /chatbot/manage endpoint with manageEnabled config check - GET /chatbot/manage/status now returns current coordination config
1 parent 9b38f47 commit 1cfeafc

9 files changed

Lines changed: 145 additions & 22 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE `Chatwoot` ADD COLUMN `coordinationSettings` JSON NULL;

prisma/mysql-schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ model Chatwoot {
233233
organization String? @db.VarChar(100)
234234
logo String? @db.VarChar(500)
235235
ignoreJids Json?
236+
coordinationSettings Json?
236237
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
237238
updatedAt DateTime @updatedAt @db.Timestamp
238239
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Chatwoot" ADD COLUMN "coordinationSettings" JSONB;

prisma/postgresql-schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ model Chatwoot {
232232
organization String? @db.VarChar(100)
233233
logo String? @db.VarChar(500)
234234
ignoreJids Json?
235+
coordinationSettings Json? @db.JsonB
235236
createdAt DateTime? @default(now()) @db.Timestamp
236237
updatedAt DateTime @updatedAt @db.Timestamp
237238
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)

src/api/integrations/chatbot/chatbot-chatwoot.service.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
import { PrismaRepository } from '@api/repository/repository.service';
22
import { WAMonitoringService } from '@api/services/monitor.service';
3-
import { Chatwoot, ConfigService } from '@config/env.config';
3+
import { ChatbotCoordination, Chatwoot, ConfigService } from '@config/env.config';
44
import { Logger } from '@config/logger.config';
55
import axios from 'axios';
66

7+
/**
8+
* Per-instance coordination config. Resolved by merging:
9+
* 1. Global env var defaults (CHATBOT_COORDINATION_*)
10+
* 2. Per-instance override (Chatwoot.coordinationSettings JSON field)
11+
*/
12+
export interface CoordinationConfig {
13+
checkAgent: boolean;
14+
autoPause: boolean;
15+
autoResolve: boolean;
16+
manageEnabled: boolean;
17+
}
18+
719
/**
820
* Coordination service between chatbot integrations and Chatwoot.
921
* Handles: bot pause on human takeover, conversation resolution on bot completion,
1022
* and explicit management actions (transfer_human, resolve_bot, pause_bot, resume_bot).
23+
*
24+
* All behaviors are configurable via env vars (global) and per-instance override.
1125
*/
1226
export class ChatbotChatwootService {
1327
private readonly logger = new Logger('ChatbotChatwootService');
@@ -25,6 +39,44 @@ export class ChatbotChatwootService {
2539
return this.configService.get<Chatwoot>('CHATWOOT').ENABLED;
2640
}
2741

42+
/**
43+
* Get global coordination defaults from env vars
44+
*/
45+
private getGlobalDefaults(): CoordinationConfig {
46+
const global = this.configService.get<ChatbotCoordination>('CHATBOT_COORDINATION');
47+
return {
48+
checkAgent: global.CHECK_AGENT,
49+
autoPause: global.AUTO_PAUSE,
50+
autoResolve: global.AUTO_RESOLVE,
51+
manageEnabled: global.MANAGE_ENABLED,
52+
};
53+
}
54+
55+
/**
56+
* Resolve coordination config for a specific instance.
57+
* Per-instance values (from Chatwoot.coordinationSettings) override global env var defaults.
58+
*/
59+
public async getCoordinationConfig(instanceId: string): Promise<CoordinationConfig> {
60+
const defaults = this.getGlobalDefaults();
61+
62+
try {
63+
const provider = await this.getProvider(instanceId);
64+
const override = provider?.coordinationSettings as Partial<CoordinationConfig> | null;
65+
66+
if (!override) return defaults;
67+
68+
return {
69+
checkAgent: override.checkAgent ?? defaults.checkAgent,
70+
autoPause: override.autoPause ?? defaults.autoPause,
71+
autoResolve: override.autoResolve ?? defaults.autoResolve,
72+
manageEnabled: override.manageEnabled ?? defaults.manageEnabled,
73+
};
74+
} catch (error) {
75+
this.logger.error(`[Coordination] Error reading instance config, using defaults: ${error?.message}`);
76+
return defaults;
77+
}
78+
}
79+
2880
/**
2981
* Get the Chatwoot provider config for an instance
3082
*/
@@ -37,11 +89,15 @@ export class ChatbotChatwootService {
3789
/**
3890
* Check if a conversation in Chatwoot has a human agent assigned.
3991
* Returns true if an agent is assigned (bot should NOT process).
92+
* Respects the checkAgent config flag.
4093
*/
4194
public async hasHumanAgentAssigned(instanceId: string, chatwootConversationId: number): Promise<boolean> {
4295
if (!chatwootConversationId) return false;
4396

4497
try {
98+
const config = await this.getCoordinationConfig(instanceId);
99+
if (!config.checkAgent) return false;
100+
45101
const provider = await this.getProvider(instanceId);
46102
if (!provider?.enabled || !provider.url || !provider.token || !provider.accountId) return false;
47103

@@ -57,7 +113,6 @@ export class ChatbotChatwootService {
57113
const hasAssignee = !!conversation?.meta?.assignee;
58114
const status = conversation?.status;
59115

60-
// Bot should not process if conversation is open with an assigned agent
61116
if (hasAssignee && status === 'open') {
62117
this.logger.log(
63118
`[Coordination] Conversation ${chatwootConversationId} has human agent assigned (status: ${status}), bot should not process`,
@@ -68,7 +123,7 @@ export class ChatbotChatwootService {
68123
return false;
69124
} catch (error) {
70125
this.logger.error(`[Coordination] Error checking agent assignment: ${error?.message}`);
71-
return false; // On error, allow bot to process
126+
return false;
72127
}
73128
}
74129

src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,22 +1452,37 @@ export class ChatwootService {
14521452
}
14531453

14541454
// Coordination: pause active bot sessions when human agent responds from Chatwoot
1455+
// Respects autoPause config (global env var + per-instance override)
14551456
try {
1456-
const remoteJidForSession = chatId.includes('@') ? chatId : `${chatId}@s.whatsapp.net`;
1457-
const pausedSessions = await this.prismaRepository.integrationSession.updateMany({
1458-
where: {
1459-
instanceId: instance.instanceId,
1460-
remoteJid: remoteJidForSession,
1461-
status: 'opened',
1462-
},
1463-
data: {
1464-
status: 'paused',
1465-
},
1466-
});
1467-
if (pausedSessions.count > 0) {
1468-
this.logger.verbose(
1469-
`[Coordination] Paused ${pausedSessions.count} bot session(s) for ${remoteJidForSession} - human agent responded from Chatwoot`,
1470-
);
1457+
let shouldAutoPause = true;
1458+
try {
1459+
// eslint-disable-next-line @typescript-eslint/no-var-requires
1460+
const { chatbotChatwootService } = require('@api/server.module');
1461+
if (chatbotChatwootService) {
1462+
const config = await chatbotChatwootService.getCoordinationConfig(instance.instanceId);
1463+
shouldAutoPause = config.autoPause;
1464+
}
1465+
} catch {
1466+
// If service not available, use default (true)
1467+
}
1468+
1469+
if (shouldAutoPause) {
1470+
const remoteJidForSession = chatId.includes('@') ? chatId : `${chatId}@s.whatsapp.net`;
1471+
const pausedSessions = await this.prismaRepository.integrationSession.updateMany({
1472+
where: {
1473+
instanceId: instance.instanceId,
1474+
remoteJid: remoteJidForSession,
1475+
status: 'opened',
1476+
},
1477+
data: {
1478+
status: 'paused',
1479+
},
1480+
});
1481+
if (pausedSessions.count > 0) {
1482+
this.logger.verbose(
1483+
`[Coordination] Paused ${pausedSessions.count} bot session(s) for ${remoteJidForSession} - human agent responded from Chatwoot`,
1484+
);
1485+
}
14711486
}
14721487
} catch (error) {
14731488
this.logger.error(`[Coordination] Error pausing bot sessions: ${error?.message}`);

src/api/integrations/chatbot/manage/routes/chatbot-manage.router.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { InstanceDto } from '@api/dto/instance.dto';
33
import { ChatbotManageDto } from '@api/integrations/chatbot/manage/dto/chatbot-manage.dto';
44
import { chatbotManageSchema } from '@api/integrations/chatbot/manage/validate/chatbot-manage.schema';
55
import { HttpStatus } from '@api/routes/index.router';
6-
import { chatbotManageController } from '@api/server.module';
6+
import { chatbotChatwootService, chatbotManageController } from '@api/server.module';
77
import { instanceSchema } from '@validate/validate.schema';
88
import { RequestHandler, Router } from 'express';
99

@@ -16,7 +16,13 @@ export class ChatbotManageRouter extends RouterBroker {
1616
request: req,
1717
schema: chatbotManageSchema,
1818
ClassRef: ChatbotManageDto,
19-
execute: (instance, data) => chatbotManageController.manage(instance, data),
19+
execute: async (instance, data) => {
20+
const config = await chatbotChatwootService.getCoordinationConfig(instance.instanceId);
21+
if (!config.manageEnabled) {
22+
return { error: 'Manage endpoint is disabled for this instance', status: 'disabled' };
23+
}
24+
return chatbotManageController.manage(instance, data);
25+
},
2026
});
2127

2228
res.status(HttpStatus.OK).json(response);
@@ -27,7 +33,13 @@ export class ChatbotManageRouter extends RouterBroker {
2733
schema: instanceSchema,
2834
ClassRef: InstanceDto,
2935
execute: async (instance) => {
30-
return { status: 'ok', integration: 'chatbot-chatwoot-coordination', instance: instance.instanceName };
36+
const config = await chatbotChatwootService.getCoordinationConfig(instance.instanceId);
37+
return {
38+
status: 'ok',
39+
integration: 'chatbot-chatwoot-coordination',
40+
instance: instance.instanceName,
41+
coordination: config,
42+
};
3143
},
3244
});
3345

src/api/integrations/chatbot/typebot/services/typebot.service.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,28 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
428428
}
429429

430430
// Coordination: resolve Chatwoot conversation when bot flow completes
431-
await this.resolveChatwootConversation(session.remoteJid, instance);
431+
// Respects autoResolve config (global env var + per-instance override)
432+
try {
433+
let shouldAutoResolve = true;
434+
try {
435+
// eslint-disable-next-line @typescript-eslint/no-var-requires
436+
const { chatbotChatwootService } = require('@api/server.module');
437+
if (chatbotChatwootService) {
438+
const instanceDb = await this.prismaRepository.instance.findFirst({ where: { name: instance.instanceName } });
439+
if (instanceDb) {
440+
const config = await chatbotChatwootService.getCoordinationConfig(instanceDb.id);
441+
shouldAutoResolve = config.autoResolve;
442+
}
443+
}
444+
} catch {
445+
// If service not available, use default (true)
446+
}
447+
if (shouldAutoResolve) {
448+
await this.resolveChatwootConversation(session.remoteJid, instance);
449+
}
450+
} catch (error) {
451+
this.logger.error(`[Coordination] Error checking autoResolve config: ${error?.message}`);
452+
}
432453

433454
const typebotData = {
434455
remoteJid: session.remoteJid,

src/config/env.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,13 @@ export type N8n = { ENABLED: boolean };
335335
export type Evoai = { ENABLED: boolean };
336336
export type Flowise = { ENABLED: boolean };
337337

338+
export type ChatbotCoordination = {
339+
CHECK_AGENT: boolean;
340+
AUTO_PAUSE: boolean;
341+
AUTO_RESOLVE: boolean;
342+
MANAGE_ENABLED: boolean;
343+
};
344+
338345
export type S3 = {
339346
ACCESS_KEY: string;
340347
SECRET_KEY: string;
@@ -418,6 +425,7 @@ export interface Env {
418425
N8N: N8n;
419426
EVOAI: Evoai;
420427
FLOWISE: Flowise;
428+
CHATBOT_COORDINATION: ChatbotCoordination;
421429
CACHE: CacheConf;
422430
S3?: S3;
423431
AUTHENTICATION: Auth;
@@ -839,6 +847,12 @@ export class ConfigService {
839847
FLOWISE: {
840848
ENABLED: process.env?.FLOWISE_ENABLED === 'true',
841849
},
850+
CHATBOT_COORDINATION: {
851+
CHECK_AGENT: process.env?.CHATBOT_COORDINATION_CHECK_AGENT !== 'false',
852+
AUTO_PAUSE: process.env?.CHATBOT_COORDINATION_AUTO_PAUSE !== 'false',
853+
AUTO_RESOLVE: process.env?.CHATBOT_COORDINATION_AUTO_RESOLVE !== 'false',
854+
MANAGE_ENABLED: process.env?.CHATBOT_COORDINATION_MANAGE_ENABLED !== 'false',
855+
},
842856
CACHE: {
843857
REDIS: {
844858
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',

0 commit comments

Comments
 (0)