A full-stack solution for AI-powered calling agents with Twilio and OpenAI integration
import express from 'express';
import cors from 'cors';
import http from 'http';
import { Server } from 'socket.io';
import agentRoutes from './routes/agent.routes.js';
import callRoutes from './routes/call.routes.js';
import { initSocket } from './services/socket.service.js';
const app = express();
const server = http.createServer(app);
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize Socket.IO
const io = new Server(server, {
cors: {
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
initSocket(io);
// Routes
app.use('/api/agents', agentRoutes);
app.use('/api/calls', callRoutes);
// Health check
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK' });
});
export { app, server };
import { Agent } from '../models/agent.model.js';
import { generateResponse } from '../services/ai.service.js';
export const createAgent = async (req, res) => {
try {
const { name, prompt, voice } = req.body;
const agent = new Agent({ name, prompt, voice });
await agent.save();
res.status(201).json(agent);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
export const getAgents = async (req, res) => {
try {
const agents = await Agent.find();
res.status(200).json(agents);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
export const getAgent = async (req, res) => {
try {
const agent = await Agent.findById(req.params.id);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
res.status(200).json(agent);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
export const updateAgent = async (req, res) => {
try {
const { name, prompt, voice } = req.body;
const agent = await Agent.findByIdAndUpdate(
req.params.id,
{ name, prompt, voice },
{ new: true }
);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
res.status(200).json(agent);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
export const deleteAgent = async (req, res) => {
try {
const agent = await Agent.findByIdAndDelete(req.params.id);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
res.status(200).json({ message: 'Agent deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
export const testAgent = async (req, res) => {
try {
const agent = await Agent.findById(req.params.id);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
const response = await generateResponse(agent.prompt, req.body.message);
res.status(200).json({ response });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
import twilio from 'twilio';
import { textToSpeech } from './tts.service.js';
import { Agent } from '../models/agent.model.js';
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER;
const client = twilio(accountSid, authToken);
export const initiateCall = async (agentId, phoneNumber) => {
try {
const agent = await Agent.findById(agentId);
if (!agent) {
throw new Error('Agent not found');
}
const call = await client.calls.create({
url: `${process.env.BASE_URL}/api/calls/initiate/${agentId}`,
to: phoneNumber,
from: twilioPhoneNumber,
});
return call;
} catch (error) {
throw error;
}
};
export const handleIncomingCall = async (agentId, callSid) => {
try {
const agent = await Agent.findById(agentId);
if (!agent) {
throw new Error('Agent not found');
}
// Generate initial greeting
const greeting = await generateResponse(agent.prompt, 'Greet the caller');
const greetingAudio = await textToSpeech(greeting, agent.voice);
// Update the call with the greeting
await client.calls(callSid).update({
twiml: `<Response>
<Play>${greetingAudio}</Play>
<Record action="/api/calls/handle-recording/${agentId}" />
</Response>`
});
return { success: true };
} catch (error) {
throw error;
}
};
export const handleRecording = async (agentId, recordingUrl) => {
try {
const agent = await Agent.findById(agentId);
if (!agent) {
throw new Error('Agent not found');
}
// Convert recording to text
const userMessage = await speechToText(recordingUrl);
// Get AI response
const aiResponse = await generateResponse(agent.prompt, userMessage);
// Convert response to speech
const responseAudio = await textToSpeech(aiResponse, agent.voice);
return {
twiml: `<Response>
<Play>${responseAudio}</Play>
<Record action="/api/calls/handle-recording/${agentId}" />
</Response>`
};
} catch (error) {
throw error;
}
};
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { deleteAgent } from '../services/agent.service';
const AgentCard = ({ agent, onDelete }) => {
const navigate = useNavigate();
const handleDelete = async () => {
try {
await deleteAgent(agent._id);
onDelete(agent._id);
} catch (error) {
console.error('Error deleting agent:', error);
}
};
return (
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<h3 className="text-xl font-semibold text-gray-800 mb-2">{agent.name}</h3>
<p className="text-gray-600 mb-4 line-clamp-3">{agent.prompt}</p>
<div className="flex items-center mb-4">
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
{agent.voice}
</span>
</div>
<div className="flex space-x-2">
<button
onClick={() => navigate(`/agents/${agent._id}`)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
Edit
</button>
<button
onClick={() => navigate(`/call/${agent._id}`)}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
>
Call
</button>
<button
onClick={handleDelete}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
>
Delete
</button>
</div>
</div>
);
};
export default AgentCard;
import React, { useState, useEffect } from 'react';
import { initiateCall } from '../services/call.service';
import { useSocket } from '../services/socket.service';
const CallPanel = ({ agentId }) => {
const [phoneNumber, setPhoneNumber] = useState('');
const [callStatus, setCallStatus] = useState('idle');
const [transcript, setTranscript] = useState([]);
const socket = useSocket();
useEffect(() => {
if (!socket) return;
socket.on('call_status', (status) => {
setCallStatus(status);
});
socket.on('transcript_update', (data) => {
setTranscript(prev => [...prev, data]);
});
return () => {
socket.off('call_status');
socket.off('transcript_update');
};
}, [socket]);
const handleCall = async () => {
try {
setCallStatus('dialing');
await initiateCall(agentId, phoneNumber);
} catch (error) {
console.error('Error initiating call:', error);
setCallStatus('error');
}
};
const callStatusColors = {
idle: 'bg-gray-200',
dialing: 'bg-yellow-200',
in_progress: 'bg-blue-200',
completed: 'bg-green-200',
error: 'bg-red-200'
};
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Initiate Call</h2>
<div className="mb-6">
<label className="block text-gray-700 mb-2">Phone Number</label>
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="+1234567890"
/>
</div>
<div className="flex items-center mb-6">
<span className={`px-4 py-2 rounded-lg ${callStatusColors[callStatus]}`}>
Status: {callStatus.replace('_', ' ')}
</span>
</div>
<button
onClick={handleCall}
disabled={!phoneNumber || callStatus !== 'idle'}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-400"
>
Start Call
</button>
{transcript.length > 0 && (
<div className="mt-6 border-t pt-4">
<h3 className="text-lg font-medium text-gray-800 mb-2">Conversation Transcript</h3>
<div className="space-y-2">
{transcript.map((item, index) => (
<div key={index} className={`p-3 rounded-lg ${item.speaker === 'user' ? 'bg-gray-100' : 'bg-blue-50'}`}>
<div className="font-medium">{item.speaker === 'user' ? 'Caller' : 'Agent'}</div>
<div>{item.text}</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default CallPanel;
version: '3.8'
services:
backend:
build: ./backend
ports:
- "4000:4000"
environment:
- NODE_ENV=development
- PORT=4000
- FRONTEND_URL=http://localhost:3000
- TWILIO_ACCOUNT_SID=${TWILIO_ACCOUNT_SID}
- TWILIO_AUTH_TOKEN=${TWILIO_AUTH_TOKEN}
- TWILIO_PHONE_NUMBER=${TWILIO_PHONE_NUMBER}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- BASE_URL=http://localhost:4000
volumes:
- ./backend:/app
- /app/node_modules
depends_on:
- postgres
restart: unless-stopped
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- VITE_API_URL=http://localhost:4000/api
volumes:
- ./frontend:/app
- /app/node_modules
depends_on:
- backend
restart: unless-stopped
postgres:
image: postgres:14
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 4000 CMD ["npm", "run", "dev"]
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["npm", "run", "dev"]
# Backend PORT=4000 FRONTEND_URL=http://localhost:3000 BASE_URL=http://localhost:4000 # Database DB_USER=postgres DB_PASSWORD=secret DB_NAME=ai_agents # Twilio TWILIO_ACCOUNT_SID=your_account_sid TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890 # OpenAI OPENAI_API_KEY=your_openai_key
# AI Calling Agent Platform - User Manual ## Overview This platform allows you to create AI-powered calling agents that can conduct phone conversations using OpenAI for responses and Twilio for telephony. ## Prerequisites - Docker and Docker Compose installed - Twilio account with a phone number - OpenAI API key ## Setup 1. Clone the repository 2. Create a `.env` file based on `.env.example` 3. Fill in all required environment variables 4. Run `docker-compose up --build` ## Running the Application 1. After containers are running: - Frontend: http://localhost:3000 - Backend: http://localhost:4000 2. Use the web interface to: - Create and manage agents - Initiate test calls - Monitor call status and transcripts ## Key Features - **Agent Management**: Create, read, update, and delete AI agents - **Call Initiation**: Start outbound calls to any phone number - **Real-time Monitoring**: View call status and conversation transcripts in real-time - **Customizable Responses**: Each agent has its own prompt template for responses ## API Endpoints - `GET /api/agents` - List all agents - `POST /api/agents` - Create new agent - `GET /api/agents/:id` - Get agent details - `PUT /api/agents/:id` - Update agent - `DELETE /api/agents/:id` - Delete agent - `POST /api/calls/initiate` - Start a new call ## Troubleshooting - Check container logs with `docker-compose logs` - Ensure all environment variables are set correctly - Verify Twilio phone number is properly configured