How to Add a New Agent
This step-by-step tutorial will guide you through creating a Listing Agent from scratch.
Overview
By the end of this tutorial, you'll have:
- ✅ A fully functional Listing Agent
- ✅ Agent registered in the system
- ✅ Custom state machine for property listings
- ✅ Message templates for listing queries
- ✅ Integration with Prophunt webserver
Time: ~30 minutes
Difficulty: Intermediate
Step 1: Create Agent Folder Structure
cd modules/agents
mkdir listing
cd listing
Create these files:
touch index.ts # Main agent implementation
touch agent.config.ts # Agent metadata & config
touch agent.state-machine.ts # State flow logic
touch agent.message-builder.ts # Response templates
touch agent.service.ts # Webserver API client
touch listing.module.ts # NestJS module
Step 2: Define Agent Configuration
agent.config.ts:
import { AgentConfig } from '../../agent-orchestrator/types';
export const LISTING_AGENT_CONFIG: AgentConfig = {
id: 'listing',
name: 'Property Listing',
description: 'Helps users search and manage property listings',
capabilities: [
'search_properties',
'filter_by_location',
'filter_by_price',
'schedule_viewing',
'save_favorite'
],
triggerKeywords: [
'listing',
'property',
'apartment',
'house',
'flat',
'real estate',
'buy',
'rent'
],
channelPrefix: 'prophunt_listing'
};
Step 3: Create State Machine
agent.state-machine.ts:
import { ConversationContext } from '../../agent-orchestrator/types';
export enum ListingState {
INIT = 'INIT',
COLLECTING_PREFERENCES = 'COLLECTING_PREFERENCES',
SHOWING_RESULTS = 'SHOWING_RESULTS',
SHOWING_DETAILS = 'SHOWING_DETAILS',
SCHEDULING_VIEWING = 'SCHEDULING_VIEWING',
VIEWING_SCHEDULED = 'VIEWING_SCHEDULED',
COMPLETED = 'COMPLETED',
ERROR = 'ERROR'
}
export class ListingStateMachine {
getCurrentState(context: ConversationContext): ListingState {
return (context.state as ListingState) || ListingState.INIT;
}
isValidState(state: string): boolean {
return Object.values(ListingState).includes(state as ListingState);
}
transition(current: ListingState, next: ListingState): boolean {
const validTransitions: Record<ListingState, ListingState[]> = {
[ListingState.INIT]: [ListingState.COLLECTING_PREFERENCES],
[ListingState.COLLECTING_PREFERENCES]: [ListingState.SHOWING_RESULTS],
[ListingState.SHOWING_RESULTS]: [
ListingState.SHOWING_DETAILS,
ListingState.COLLECTING_PREFERENCES // Refine search
],
[ListingState.SHOWING_DETAILS]: [
ListingState.SCHEDULING_VIEWING,
ListingState.SHOWING_RESULTS // Back to list
],
[ListingState.SCHEDULING_VIEWING]: [ListingState.VIEWING_SCHEDULED],
[ListingState.VIEWING_SCHEDULED]: [ListingState.COMPLETED],
[ListingState.COMPLETED]: [],
[ListingState.ERROR]: [ListingState.INIT]
};
return validTransitions[current]?.includes(next) || false;
}
}
Step 4: Build Message Templates
agent.message-builder.ts:
import { Injectable } from '@nestjs/common';
import {
MessageEnvelope,
MessageType,
ButtonType,
QuickReply
} from '../../agent-orchestrator/types';
@Injectable()
export class ListingMessageBuilder {
buildWelcomeMessage(): MessageEnvelope {
return {
type: MessageType.QUICK_REPLIES,
text: '🏠 Welcome to Property Listings! What are you looking for?',
quick_replies: [
{ title: 'Buy', payload: 'SEARCH_BUY' },
{ title: 'Rent', payload: 'SEARCH_RENT' },
{ title: 'Sell', payload: 'SELL_PROPERTY' }
]
};
}
buildPreferenceQuestions(): MessageEnvelope[] {
return [
{
type: MessageType.QUICK_REPLIES,
text: 'Which city are you interested in?',
quick_replies: [
{ title: 'Mumbai', payload: 'CITY_MUMBAI' },
{ title: 'Bangalore', payload: 'CITY_BANGALORE' },
{ title: 'Delhi', payload: 'CITY_DELHI' },
{ title: 'Other', payload: 'CITY_OTHER' }
]
},
{
type: MessageType.TEXT,
text: 'What is your budget range? (e.g., "50L-1Cr" or "20k-30k per month")'
}
];
}
buildSearchResults(properties: any[]): MessageEnvelope {
return {
type: MessageType.BUTTONS,
text: `Found ${properties.length} properties matching your criteria:
${properties.map((p, i) => `${i + 1}. ${p.title} - ${p.location} - ₹${p.price}`).join('\n')}`,
buttons: properties.slice(0, 3).map(p => ({
type: ButtonType.POSTBACK,
title: `View ${p.title.substring(0, 15)}...`,
payload: `VIEW_PROPERTY_${p.id}`
}))
};
}
buildPropertyDetails(property: any): MessageEnvelope {
return {
type: MessageType.BUTTONS,
text: `🏠 ${property.title}
📍 Location: ${property.location}
💰 Price: ₹${property.price}
🛏️ Bedrooms: ${property.bedrooms}
🚿 Bathrooms: ${property.bathrooms}
📐 Area: ${property.area} sq.ft.
${property.description}`,
buttons: [
{
type: ButtonType.POSTBACK,
title: 'Schedule Viewing',
payload: `SCHEDULE_${property.id}`
},
{
type: ButtonType.WEB_URL,
title: 'View on Map',
url: property.mapUrl
},
{
type: ButtonType.POSTBACK,
title: 'Back to Results',
payload: 'BACK_TO_RESULTS'
}
]
};
}
buildViewingScheduled(date: string, time: string): MessageEnvelope {
return {
type: MessageType.TEXT,
text: `✅ Viewing scheduled for ${date} at ${time}. You'll receive a confirmation email shortly.`
};
}
buildErrorMessage(error: string): MessageEnvelope {
return {
type: MessageType.TEXT,
text: `❌ ${error}`
};
}
}
Step 5: Create Service Layer
agent.service.ts:
import { Injectable } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class ListingService {
private readonly apiUrl =
process.env.PROPHUNT_API_URL || 'http://localhost:3000/api';
async searchProperties(filters: {
city: string;
minPrice: number;
maxPrice: number;
type: 'buy' | 'rent';
}): Promise<any[]> {
const response = await axios.post(
`${this.apiUrl}/listings/search`,
filters
);
return response.data.properties;
}
async getPropertyDetails(propertyId: string): Promise<any> {
const response = await axios.get(
`${this.apiUrl}/listings/${propertyId}`
);
return response.data;
}
async scheduleViewing(propertyId: string, userId: string, datetime: string): Promise<any> {
const response = await axios.post(
`${this.apiUrl}/listings/schedule-viewing`,
{ propertyId, userId, datetime }
);
return response.data;
}
async saveFavorite(propertyId: string, userId: string): Promise<void> {
await axios.post(`${this.apiUrl}/listings/favorites`, {
propertyId,
userId
});
}
}
Step 6: Implement Main Agent
index.ts:
import { Injectable } from '@nestjs/common';
import {
Agent,
AgentEvent,
ChannelMessage,
ConversationContext,
MessageEnvelope
} from '../../agent-orchestrator/types';
import { LISTING_AGENT_CONFIG } from './agent.config';
import { ListingStateMachine, ListingState } from './agent.state-machine';
import { ListingService } from './agent.service';
import { ListingMessageBuilder } from './agent.message-builder';
@Injectable()
export class ListingAgent implements Agent {
config = LISTING_AGENT_CONFIG;
private stateMachine = new ListingStateMachine();
constructor(
private readonly service: ListingService,
private readonly messageBuilder: ListingMessageBuilder
) {}
async processMessage(
message: ChannelMessage,
context: ConversationContext
): Promise<MessageEnvelope[]> {
const currentState = this.stateMachine.getCurrentState(context);
const text = message.text?.toLowerCase() || '';
const payload = message.payload as string | undefined;
// Handle commands
if (text.includes('help')) {
return [this.buildHelpMessage()];
}
// State-based processing
switch (currentState) {
case ListingState.INIT:
return this.handleInit();
case ListingState.COLLECTING_PREFERENCES:
return this.handlePreferenceCollection(message, context);
case ListingState.SHOWING_RESULTS:
return this.handleResultsView(payload, context);
case ListingState.SHOWING_DETAILS:
return this.handleDetailsView(payload, context);
case ListingState.SCHEDULING_VIEWING:
return this.handleScheduling(message, context);
default:
return [this.messageBuilder.buildWelcomeMessage()];
}
}
private async handleInit(): Promise<MessageEnvelope[]> {
return [
this.messageBuilder.buildWelcomeMessage(),
...this.messageBuilder.buildPreferenceQuestions()
];
}
private async handlePreferenceCollection(
message: ChannelMessage,
context: ConversationContext
): Promise<MessageEnvelope[]> {
// Parse user preferences
const city = this.extractCity(message.text || '');
const budget = this.extractBudget(message.text || '');
if (!city || !budget) {
return [{
type: 'text' as any,
text: 'Please provide both city and budget.'
}];
}
// Search properties
const properties = await this.service.searchProperties({
city,
minPrice: budget.min,
maxPrice: budget.max,
type: context.vars?.listingType || 'rent'
});
// Store results in context
context.vars = {
...context.vars,
searchResults: properties
};
return [this.messageBuilder.buildSearchResults(properties)];
}
private async handleResultsView(
payload: string | undefined,
context: ConversationContext
): Promise<MessageEnvelope[]> {
if (!payload || !payload.startsWith('VIEW_PROPERTY_')) {
return [this.messageBuilder.buildWelcomeMessage()];
}
const propertyId = payload.replace('VIEW_PROPERTY_', '');
const property = await this.service.getPropertyDetails(propertyId);
return [this.messageBuilder.buildPropertyDetails(property)];
}
private async handleDetailsView(
payload: string | undefined,
context: ConversationContext
): Promise<MessageEnvelope[]> {
if (payload === 'BACK_TO_RESULTS') {
const results = context.vars?.searchResults || [];
return [this.messageBuilder.buildSearchResults(results)];
}
if (payload?.startsWith('SCHEDULE_')) {
return [{
type: 'text' as any,
text: 'Please provide your preferred date and time (e.g., "Tomorrow 2PM")'
}];
}
return [this.messageBuilder.buildWelcomeMessage()];
}
private async handleScheduling(
message: ChannelMessage,
context: ConversationContext
): Promise<MessageEnvelope[]> {
const datetime = message.text || '';
const propertyId = context.vars?.selectedProperty;
await this.service.scheduleViewing(
propertyId,
message.user_id,
datetime
);
return [this.messageBuilder.buildViewingScheduled(datetime, datetime)];
}
async handleEvent(
event: AgentEvent,
context: ConversationContext
): Promise<void> {
// Handle events if needed
}
getState(context: ConversationContext): string {
return this.stateMachine.getCurrentState(context);
}
async transition(
from: string,
to: string,
context: ConversationContext
): Promise<ConversationContext> {
return { ...context, state: to };
}
canHandle(message: ChannelMessage): boolean {
const text = message.text?.toLowerCase() || '';
return this.config.triggerKeywords.some(keyword =>
text.includes(keyword.toLowerCase())
);
}
private extractCity(text: string): string | null {
// Simple extraction - improve with NLP
const cities = ['mumbai', 'bangalore', 'delhi', 'pune'];
return cities.find(city => text.includes(city)) || null;
}
private extractBudget(text: string): { min: number; max: number } | null {
// Simple regex - improve as needed
const match = text.match(/(\d+).*?(\d+)/);
if (match) {
return { min: parseInt(match[1]), max: parseInt(match[2]) };
}
return null;
}
private buildHelpMessage(): MessageEnvelope {
return {
type: 'text' as any,
text: `🏠 Listing Agent Help:
• Search properties by location and budget
• View property details
• Schedule viewings
• Save favorites
Type "search" to start a new search!`
};
}
}
Step 7: Create NestJS Module
listing.module.ts:
import { Module } from '@nestjs/common';
import { ListingAgent } from './index';
import { ListingService } from './agent.service';
import { ListingMessageBuilder } from './agent.message-builder';
@Module({
providers: [
ListingAgent,
ListingService,
ListingMessageBuilder
],
exports: [ListingAgent]
})
export class ListingModule {}
Step 8: Register Agent
Update modules/agents/agents.module.ts:
import { Module } from '@nestjs/common';
import { AgentOrchestratorModule } from '../agent-orchestrator/agent-orchestrator.module';
import { AgentRegistry } from '../agent-orchestrator/agent-registry';
// Import existing agents
import { BGVAgent } from './bgv';
import { LoanAgent } from './loan';
// Import NEW agent
import { ListingAgent } from './listing';
import { ListingModule } from './listing/listing.module';
@Module({
imports: [
AgentOrchestratorModule,
ListingModule // Add module
],
providers: [
BGVAgent,
LoanAgent,
ListingAgent // Add provider
]
})
export class AgentsModule {
constructor(
private readonly agentRegistry: AgentRegistry,
private readonly bgvAgent: BGVAgent,
private readonly loanAgent: LoanAgent,
private readonly listingAgent: ListingAgent // Inject
) {
// Register all agents
this.agentRegistry.register(this.bgvAgent);
this.agentRegistry.register(this.loanAgent);
this.agentRegistry.register(this.listingAgent); // Register!
}
}
Step 9: Test Your Agent
Manual Testing
-
Start Docker:
npm run dev -
Connect frontend:
// In Angular ChatService
this.socket.emit('join_channel', {
channel_id: 'prophunt_listing',
user_id: 'user123'
}); -
Send test message:
this.socket.emit('message', {
channel_id: 'prophunt_listing',
user_id: 'user123',
text: 'I want to rent an apartment'
}); -
Check logs:
docker logs -f api
Unit Testing
Create listing/index.spec.ts:
import { Test } from '@nestjs/testing';
import { ListingAgent } from './index';
import { ListingService } from './agent.service';
import { ListingMessageBuilder } from './agent.message-builder';
describe('ListingAgent', () => {
let agent: ListingAgent;
let service: ListingService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ListingAgent,
{
provide: ListingService,
useValue: {
searchProperties: jest.fn()
}
},
ListingMessageBuilder
]
}).compile();
agent = module.get<ListingAgent>(ListingAgent);
service = module.get<ListingService>(ListingService);
});
it('should activate on keyword "listing"', () => {
const message = {
channel_id: 'prophunt_listing',
user_id: 'test',
text: 'show me listings'
};
expect(agent.canHandle(message)).toBe(true);
});
it('should process initial message', async () => {
const context = { state: 'INIT', vars: {} };
const message = {
channel_id: 'prophunt_listing',
user_id: 'test',
text: 'I want to rent'
};
const responses = await agent.processMessage(message, context);
expect(responses.length).toBeGreaterThan(0);
});
});
Step 10: Frontend Integration
Add to Angular ChatComponent:
// Agent selector
agents = [
{ id: 'bgv', name: 'BGV', icon: '📋' },
{ id: 'loan', name: 'Loan', icon: '💰' },
{ id: 'listing', name: 'Listings', icon: '🏠' } // NEW!
];
selectAgent(agentId: string) {
this.chatService.switchAgent(agentId as 'bgv' | 'loan' | 'listing');
}
Checklist
Before deploying, ensure:
- Agent config is complete
- State machine has all states
- Message builder has all templates
- Service layer handles API calls
- Agent is registered in AgentsModule
- Unit tests pass
- Manual testing successful
- Frontend can select agent
- Webserver APIs are ready
Common Issues
Agent not activating
Problem: Messages not routing to your agent
Solution: Check trigger keywords in agent.config.ts and ensure they're not too generic
State not persisting
Problem: Agent state resets
Solution: Ensure you're updating context.state and calling ConversationManager.updateContext()
Messages not sending
Problem: Responses not appearing in frontend
Solution: Check that ConversationManager.sendMessage() is called for each response
Next Steps
- Add NLP for better intent detection
- Implement caching for frequently accessed data
- Add analytics to track agent usage
- Create admin panel for agent management
Additional Resources
- Agent Interface Reference
- State Machine Patterns
- Testing Agents
- BGV Agent Source (reference implementation)
Congratulations! 🎉 You've created a new agent. Now users can interact with listings through your chatbot!