Multi-Agent System
This document explains how the multi-agent architecture works in Hexabot + Prophunt.
What is a Multi-Agent System?
A multi-agent system allows multiple specialized AI agents to coexist in the same chatbot platform, each handling specific domains:
- BGV Agent - Background Verification
- Loan Agent - Loan eligibility and processing
- Listing Agent - Property listing management
Each agent has:
- ✅ Isolated conversation context - Separate history per agent
- ✅ Specialized state machine - Custom workflow for its domain
- ✅ Dedicated message builders - Domain-specific responses
- ✅ External service integration - Calls to webserver APIs
Agent Lifecycle
stateDiagram-v2
[*] --> Registered: Agent registered in AgentRegistry
Registered --> Idle: Waiting for activation
Idle --> Activated: Keyword match or explicit selection
Activated --> Processing: User sends message
Processing --> RespondingTo User: Generate responses
RespondingToUser --> AwaitingInput: Waiting for user
AwaitingInput --> Processing: User responds
AwaitingInput --> Deactivated: User switches agent
Processing --> HandlingEvent: External event (payment, file upload)
HandlingEvent --> Processing: Event processed
RespondingToUser --> Completed: Workflow finished
Completed --> [*]
Deactivated --> [*]
Agent Structure
Every agent implements the Agent interface:
interface Agent {
config: AgentConfig;
processMessage(
message: ChannelMessage,
context: ConversationContext
): Promise<MessageEnvelope[]>;
handleEvent(
event: AgentEvent,
context: ConversationContext
): Promise<void>;
getState(context: ConversationContext): string;
transition(
from: string,
to: string,
context: ConversationContext
): Promise<ConversationContext>;
canHandle(message: ChannelMessage): boolean;
}
Agent Config
interface AgentConfig {
id: string; // 'bgv', 'loan', 'listing'
name: string; // 'Background Verification'
description: string; // Human-readable description
capabilities: string[]; // ['pan_verification', 'address_check']
triggerKeywords: string[]; // ['bgv', 'background check', 'verify']
channelPrefix: string; // 'prophunt_bgv'
}
Agent Activation
Method 1: Keyword Matching
User sends: "I want background verification"
// AgentRouter checks each agent
for (const agent of agents) {
if (agent.canHandle(message)) {
return agent; // BGVAgent activated
}
}
// BGVAgent.canHandle()
canHandle(message: ChannelMessage): boolean {
const text = message.text?.toLowerCase() || '';
return this.config.triggerKeywords.some(keyword =>
text.includes(keyword.toLowerCase())
);
}
Method 2: Explicit Selection
Frontend sends: { channel_id: 'prophunt_bgv', ... }
// ChannelManager extracts agent
mapChannelToAgent(channelId: string): string | null {
const parts = channelId.split('_');
if (parts.length >= 2) {
return parts[parts.length - 1]; // 'bgv'
}
return null;
}
// AgentRegistry retrieves agent by ID
const agent = agentRegistry.getAgent('bgv');
Method 3: Context Persistence
User returns to existing conversation:
// ConversationManager loads conversation
const conversation = await conversationManager.getOrCreateConversation(
'prophunt_bgv',
'user123'
);
// Context contains active agent
const agentId = conversation.context.vars.agentId; // 'bgv'
const agent = agentRegistry.getAgent(agentId);
Agent Isolation
Each agent maintains isolated conversation history:
// Frontend connects to specific channel
this.socket.emit('join_channel', {
channel_id: 'prophunt_bgv', // BGV channel
user_id: 'user123'
});
// Later, user switches to Loan Agent
this.socket.emit('leave_channel', {
channel_id: 'prophunt_bgv'
});
this.socket.emit('join_channel', {
channel_id: 'prophunt_loan', // Loan channel
user_id: 'user123'
});
// Separate conversations in database:
// {
// context: { vars: { channelId: 'prophunt_bgv', agentId: 'bgv' } }
// messages: [/* BGV conversation */]
// }
// {
// context: { vars: { channelId: 'prophunt_loan', agentId: 'loan' } }
// messages: [/* Loan conversation */]
// }
State Machines
Each agent uses a state machine to manage workflow:
BGV Agent States
enum BGVState {
INIT = 'INIT',
PAN_UPLOAD_REQUESTED = 'PAN_UPLOAD_REQUESTED',
PAN_UPLOADED = 'PAN_UPLOADED',
AWAITING_PAYMENT = 'AWAITING_PAYMENT',
PAYMENT_COMPLETED = 'PAYMENT_COMPLETED',
VERIFICATION_IN_PROGRESS = 'VERIFICATION_IN_PROGRESS',
REPORT_READY = 'REPORT_READY',
COMPLETED = 'COMPLETED',
ERROR = 'ERROR'
}
State Transitions
INIT
│
├─ User: "Start BGV"
│
▼
PAN_UPLOAD_REQUESTED
│
├─ Event: FILE_UPLOADED
│
▼
PAN_UPLOADED
│
├─ Agent: Generates payment link
│
▼
AWAITING_PAYMENT
│
├─ Event: PAYMENT_COMPLETED
│
▼
PAYMENT_COMPLETED
│
├─ Agent: Calls webserver /start-verification
│
▼
VERIFICATION_IN_PROGRESS
│
├─ Webserver: Calls /agent-api/send-message (verification_completed)
│
▼
REPORT_READY
│
├─ User: Downloads report
│
▼
COMPLETED
Agent Communication
1. Agent → User (via Socket.io)
// In BGVAgent.processMessage()
const responses = [
{
type: MessageType.TEXT,
text: 'Please upload your PAN card'
},
{
type: MessageType.CUSTOM,
custom_payload: {
type: 'OPEN_MODAL',
modal_id: 'file_upload_modal'
}
}
];
// ConversationManager persists and emits
for (const response of responses) {
await conversationManager.sendMessage(
conversationId,
channelId,
response,
'bgv-agent'
);
}
// SocketAdapter emits to frontend
socketAdapter.emitToChannel(channelId, response);
2. Agent → Webserver (HTTP API)
// In BGVService (agent module)
async startVerification(taskId: string): Promise<any> {
const response = await axios.post(
`${this.prophuntApiUrl}/bgv/start-verification`,
{ taskId }
);
return response.data;
}
// Called from BGVAgent
await this.service.startVerification(context.taskId);
3. Webserver → Agent (via AgentApiController)
// Prophunt Webserver
await axios.post('http://hexabot:4000/agent-api/send-message', {
taskId: 'bgv_user123_1234567890',
agentId: 'bgv',
messageType: 'verification_completed',
data: { reportUrl: 'https://...' }
});
// AgentApiController routes to ConversationManager
// Message sent to user via Socket.io
4. User → Agent (via ChatGateway)
// Frontend sends
socket.emit('message', {
channel_id: 'prophunt_bgv',
user_id: 'user123',
text: 'Check my BGV status'
});
// ChatGateway receives and routes
const agent = agentRouter.routeMessage(message, context);
const responses = await agent.processMessage(message, context);
Agent Registry
The AgentRegistry is the central repository for all agents:
@Injectable()
export class AgentRegistry {
private agents: Map<string, Agent> = new Map();
/**
* Register an agent
*/
register(agent: Agent): void {
this.agents.set(agent.config.id, agent);
}
/**
* Get agent by ID
*/
getAgent(agentId: string): Agent | null {
return this.agents.get(agentId) || null;
}
/**
* Get all registered agents
*/
getAllAgents(): Agent[] {
return Array.from(this.agents.values());
}
}
Registration Process
// In AgentsModule
@Module({
providers: [
BGVAgent,
LoanAgent,
ListingAgent
]
})
export class AgentsModule {
constructor(
private readonly agentRegistry: AgentRegistry,
private readonly bgvAgent: BGVAgent,
private readonly loanAgent: LoanAgent,
private readonly listingAgent: ListingAgent
) {
// Register all agents
this.agentRegistry.register(this.bgvAgent);
this.agentRegistry.register(this.loanAgent);
this.agentRegistry.register(this.listingAgent);
}
}
Context Management
Agent context is stored in conversation.context.vars:
{
_id: ObjectId('...'),
sender: 'user123',
active: true,
context: {
vars: {
// Agent-specific fields
channelId: 'prophunt_bgv',
agentId: 'bgv',
taskId: 'bgv_user123_1234567890',
verificationId: 'VER_12345', // Linked externally
state: 'AWAITING_PAYMENT',
// Custom workflow data
panCardUploaded: true,
panNumber: 'ABCDE1234F',
paymentAmount: 499,
paymentStatus: 'pending',
// Metadata
lastMessageAt: Date('2025-12-05T10:30:00Z'),
lastMessageHandler: 'bgv-agent'
},
// Hexabot standard fields
user_location: { lat: 0, lon: 0 },
skip: {},
attempt: 0
}
}
Agent Switching
User can switch between agents mid-conversation:
// User clicks "Switch to Loan Agent" in UI
frontend.leaveChannel('prophunt_bgv');
frontend.join Channel('prophunt_loan');
// ChatGateway handles
@SubscribeMessage('leave_channel')
handleLeaveChannel(client: Socket, payload: { channel_id: string }) {
this.socketAdapter.leaveChannel(client.id, payload.channel_id);
}
@SubscribeMessage('join_channel')
handleJoinChannel(client: Socket, payload: { channel_id, user_id }) {
// Get/create conversation for new agent
const conversation = await this.conversationManager.getOrCreateConversation(
payload.channel_id, // 'prophunt_loan'
payload.user_id
);
// Join Socket.io room
this.socketAdapter.joinChannel(client.id, payload.channel_id);
// Agent context switched
const agentId = conversation.context.vars.agentId; // 'loan'
}
Event-Driven Agent Coordination
Agents listen to events via EventBus:
// BGVAgent subscribes on initialization
constructor(
private readonly eventBus: EventBus,
private readonly service: BGVService
) {
this.eventBus.subscribe('FILE_UPLOADED', this.onFileUploaded.bind(this));
this.eventBus.subscribe('PAYMENT_COMPLETED', this.onPaymentCompleted.bind(this));
}
// Event handler
async onFileUploaded(event: AgentEvent) {
if (event.agentId === 'bgv') {
await this.service.notifyPANUpload(event.taskId, event.data.fileUrl);
}
}
// EventHandler emits events
await this.eventBus.emit('FILE_UPLOADED', {
type: AgentEventType.FILE_UPLOADED,
agentId: 'bgv',
taskId: context.taskId,
data: { fileUrl: 'https://...' },
timestamp: new Date()
});
Best Practices
✅ Keep agents stateless - All state in context.vars
✅ Use state machines - Clear workflow progression
✅ Isolate conversations - One channel per agent
✅ Persist everything - Don't rely on in-memory state
✅ Event-driven communication - Decouple via EventBus
✅ Type-safe messages - Use TypeScript enums and interfaces
Next Steps
- Learn BGV Agent Implementation for a complete example
- Understand Event Bus for event-driven patterns
- Read Agent Router Logic for routing details
- Follow How to Add a New Agent tutorial