Skip to content

Commit c70d9c3

Browse files
committed
feat(typebot): detect [transfer_human] marker for bot-to-human handoff
- Add detectTransferMarker to coordination config (env.config.ts) - Typebot service detects [transfer_human] in bot responses - On detection: pauses bot session + opens Chatwoot conversation - Configurable per-instance via coordinationSettings - ChatbotChatwootService: add getCoordinationConfig helper
1 parent fa87c7b commit c70d9c3

3 files changed

Lines changed: 104 additions & 44 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface CoordinationConfig {
1414
autoPause: boolean;
1515
autoResolve: boolean;
1616
manageEnabled: boolean;
17+
detectTransferMarker: boolean;
1718
}
1819

1920
/**
@@ -49,6 +50,7 @@ export class ChatbotChatwootService {
4950
autoPause: global.AUTO_PAUSE,
5051
autoResolve: global.AUTO_RESOLVE,
5152
manageEnabled: global.MANAGE_ENABLED,
53+
detectTransferMarker: global.DETECT_TRANSFER_MARKER,
5254
};
5355
}
5456

@@ -70,6 +72,7 @@ export class ChatbotChatwootService {
7072
autoPause: override.autoPause ?? defaults.autoPause,
7173
autoResolve: override.autoResolve ?? defaults.autoResolve,
7274
manageEnabled: override.manageEnabled ?? defaults.manageEnabled,
75+
detectTransferMarker: override.detectTransferMarker ?? defaults.detectTransferMarker,
7376
};
7477
} catch (error) {
7578
this.logger.error(`[Coordination] Error reading instance config, using defaults: ${error?.message}`);
@@ -241,7 +244,7 @@ export class ChatbotChatwootService {
241244
},
242245
chatwootConversationId: { not: null },
243246
},
244-
orderBy: { createdAt: 'desc' },
247+
orderBy: { messageTimestamp: 'desc' },
245248
});
246249

247250
if (recentMessage?.chatwootConversationId) {

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

Lines changed: 98 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,24 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
298298
return null;
299299
};
300300

301+
let transferToHumanRequested = false;
302+
303+
// Check if [transfer_human] marker detection is enabled for this instance
304+
let detectMarkerEnabled = true;
305+
try {
306+
// eslint-disable-next-line @typescript-eslint/no-var-requires
307+
const { chatbotChatwootService: markerSvc } = require('@api/server.module');
308+
if (markerSvc) {
309+
const instDb = await this.prismaRepository.instance.findFirst({ where: { name: instance.instanceName } });
310+
if (instDb) {
311+
const cfg = await markerSvc.getCoordinationConfig(instDb.id);
312+
detectMarkerEnabled = cfg.detectTransferMarker;
313+
}
314+
}
315+
} catch {
316+
// Default: enabled
317+
}
318+
301319
for (const message of messages) {
302320
if (message.type === 'text') {
303321
let formattedText = '';
@@ -313,6 +331,14 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
313331

314332
formattedText = formattedText.replace(/\n$/, '');
315333

334+
// Detect [transfer_human] marker from Typebot flow (configurable per instance)
335+
if (detectMarkerEnabled && formattedText.includes('[transfer_human]')) {
336+
transferToHumanRequested = true;
337+
formattedText = formattedText.replace('[transfer_human]', '').trim();
338+
this.logger.log(`[Coordination] Detected [transfer_human] marker in Typebot message for ${session.remoteJid}`);
339+
if (!formattedText) continue; // skip empty message after stripping marker
340+
}
341+
316342
if (formattedText.includes('[list]')) {
317343
await this.processListMessage(instance, formattedText, session.remoteJid);
318344
} else if (formattedText.includes('[buttons]')) {
@@ -408,55 +434,84 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
408434
},
409435
});
410436
} else {
411-
let statusChange = 'closed';
412-
if (!settings?.keepOpen) {
413-
await prismaRepository.integrationSession.deleteMany({
414-
where: {
415-
id: session.id,
416-
},
417-
});
418-
statusChange = 'delete';
437+
// Check if transfer_human was requested via [transfer_human] marker or HTTP Request
438+
const currentSession = await prismaRepository.integrationSession.findUnique({
439+
where: { id: session.id },
440+
});
441+
const sessionPaused = currentSession?.status === 'paused';
442+
443+
if (transferToHumanRequested || sessionPaused) {
444+
this.logger.log(
445+
`[Coordination] Transfer to human requested for ${session.remoteJid} (marker: ${transferToHumanRequested}, paused: ${sessionPaused})`,
446+
);
447+
448+
// Execute transfer_human logic internally
449+
if (transferToHumanRequested && !sessionPaused) {
450+
try {
451+
// eslint-disable-next-line @typescript-eslint/no-var-requires
452+
const { chatbotChatwootService } = require('@api/server.module');
453+
if (chatbotChatwootService) {
454+
const instanceDb = await this.prismaRepository.instance.findFirst({ where: { name: instance.instanceName } });
455+
if (instanceDb) {
456+
const result = await chatbotChatwootService.transferToHuman(instanceDb.id, session.remoteJid);
457+
this.logger.log(`[Coordination] transferToHuman result: ${JSON.stringify(result)}`);
458+
}
459+
}
460+
} catch (err) {
461+
this.logger.error(`[Coordination] Error executing transferToHuman: ${err?.message}`);
462+
}
463+
}
419464
} else {
420-
await prismaRepository.integrationSession.update({
421-
where: {
422-
id: session.id,
423-
},
424-
data: {
425-
status: 'closed',
426-
},
427-
});
428-
}
465+
let statusChange = 'closed';
466+
if (!settings?.keepOpen) {
467+
await prismaRepository.integrationSession.deleteMany({
468+
where: {
469+
id: session.id,
470+
},
471+
});
472+
statusChange = 'delete';
473+
} else {
474+
await prismaRepository.integrationSession.update({
475+
where: {
476+
id: session.id,
477+
},
478+
data: {
479+
status: 'closed',
480+
},
481+
});
482+
}
429483

430-
// Coordination: resolve Chatwoot conversation when bot flow completes
431-
// Respects autoResolve config (global env var + per-instance override)
432-
try {
433-
let shouldAutoResolve = true;
484+
// Coordination: resolve Chatwoot conversation when bot flow completes
485+
// Respects autoResolve config (global env var + per-instance override)
434486
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;
487+
let shouldAutoResolve = true;
488+
try {
489+
// eslint-disable-next-line @typescript-eslint/no-var-requires
490+
const { chatbotChatwootService } = require('@api/server.module');
491+
if (chatbotChatwootService) {
492+
const instanceDb = await this.prismaRepository.instance.findFirst({ where: { name: instance.instanceName } });
493+
if (instanceDb) {
494+
const config = await chatbotChatwootService.getCoordinationConfig(instanceDb.id);
495+
shouldAutoResolve = config.autoResolve;
496+
}
442497
}
498+
} catch {
499+
// If service not available, use default (true)
443500
}
444-
} catch {
445-
// If service not available, use default (true)
446-
}
447-
if (shouldAutoResolve) {
448-
await this.resolveChatwootConversation(session.remoteJid, instance);
501+
if (shouldAutoResolve) {
502+
await this.resolveChatwootConversation(session.remoteJid, instance);
503+
}
504+
} catch (error) {
505+
this.logger.error(`[Coordination] Error checking autoResolve config: ${error?.message}`);
449506
}
450-
} catch (error) {
451-
this.logger.error(`[Coordination] Error checking autoResolve config: ${error?.message}`);
452-
}
453507

454-
const typebotData = {
455-
remoteJid: session.remoteJid,
456-
status: statusChange,
457-
session,
458-
};
459-
instance.sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
508+
const typebotData = {
509+
remoteJid: session.remoteJid,
510+
status: statusChange,
511+
session,
512+
};
513+
instance.sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
514+
}
460515
}
461516
}
462517

@@ -485,7 +540,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
485540
key: { path: ['remoteJid'], equals: remoteJid },
486541
chatwootConversationId: { not: null },
487542
},
488-
orderBy: { createdAt: 'desc' },
543+
orderBy: { messageTimestamp: 'desc' },
489544
});
490545

491546
if (!recentMessage?.chatwootConversationId) {

src/config/env.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ export type ChatbotCoordination = {
340340
AUTO_PAUSE: boolean;
341341
AUTO_RESOLVE: boolean;
342342
MANAGE_ENABLED: boolean;
343+
DETECT_TRANSFER_MARKER: boolean;
343344
};
344345

345346
export type S3 = {
@@ -852,6 +853,7 @@ export class ConfigService {
852853
AUTO_PAUSE: process.env?.CHATBOT_COORDINATION_AUTO_PAUSE !== 'false',
853854
AUTO_RESOLVE: process.env?.CHATBOT_COORDINATION_AUTO_RESOLVE !== 'false',
854855
MANAGE_ENABLED: process.env?.CHATBOT_COORDINATION_MANAGE_ENABLED !== 'false',
856+
DETECT_TRANSFER_MARKER: process.env?.CHATBOT_COORDINATION_DETECT_TRANSFER_MARKER !== 'false',
855857
},
856858
CACHE: {
857859
REDIS: {

0 commit comments

Comments
 (0)