Skip to content

Commit b14e8ac

Browse files
initial commit of the call agent
1 parent da89726 commit b14e8ac

File tree

4 files changed

+1364
-0
lines changed

4 files changed

+1364
-0
lines changed

agent.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
const express = require("express");
2+
const { WebSocketServer } = require("ws");
3+
const Twilio = require("twilio");
4+
const { createServer } = require("http");
5+
6+
const app = express();
7+
app.use(express.json());
8+
9+
const server = createServer(app);
10+
11+
const websocket = new WebSocketServer({ noServer: true });
12+
13+
// Endpoint to initiate an outbound call
14+
app.get("/outbound-call", (req, res) => {
15+
const twilioClient = new Twilio(
16+
process.env.TWILIO_ACC, // Twilio Account SID from environment variables
17+
process.env.TWILIO_KEY // Twilio Auth Token from environment variables
18+
);
19+
20+
const twimlResponse = `<?xml version="1.0" encoding="UTF-8"?>
21+
<Response>
22+
<Connect>
23+
<Stream url="wss://${process.env.SERVER_URL}/outbound-stream" />
24+
</Connect>
25+
</Response>`;
26+
27+
// Create the call using Twilio API and initiate the WebSocket stream
28+
twilioClient.calls
29+
.create({
30+
from: process.env.FROM_NUMBER,
31+
to: req.query.toNumber,
32+
twiml: twimlResponse, // The TwiML response containing WebSocket stream information
33+
})
34+
.then((call) => {
35+
res.send({
36+
success: true,
37+
message: "Call initiated",
38+
callSid: call.sid,
39+
});
40+
})
41+
.catch((error) => {
42+
res.status(500).send({
43+
success: false,
44+
message: "Failed to initiate call",
45+
error: error.message,
46+
});
47+
});
48+
});
49+
50+
server.on("upgrade", (request, socket, head) => {
51+
const pathname = new URL(request.url, `http://${request.headers.host}`)
52+
.pathname;
53+
54+
console.log("RECEIVED UPGRADE ON", pathname);
55+
56+
if (pathname === "/outbound-stream") {
57+
websocket.handleUpgrade(request, socket, head, (ws) => {
58+
websocket.emit("connection", ws, request);
59+
});
60+
61+
// Handle WebSocket connection from Twilio
62+
websocket.on("connection", async (ws) => {
63+
console.log("Stream connected from Twilio");
64+
65+
// Set up the Eleven Labs WebSocket
66+
await setupElevenLabs();
67+
68+
// Handle WebSocket errors
69+
ws.on("error", (err) => console.log("Error on Twilio WebSocket:", err));
70+
71+
// Handle incoming messages from Twilio
72+
ws.on("message", (message) => {
73+
try {
74+
const msg = JSON.parse(message);
75+
switch (msg.event) {
76+
case "start":
77+
streamSid = msg.start.streamSid;
78+
console.log(`Started StreamSid ${streamSid}`);
79+
break;
80+
81+
case "media":
82+
if (elevenWs?.readyState === WebSocket.OPEN) {
83+
console.log(
84+
`Received media from Twilio: StreamSid ${streamSid}`
85+
);
86+
const audioMessage = {
87+
user_audio_chunk: Buffer.from(
88+
msg.media.payload,
89+
"base64"
90+
).toString("base64"),
91+
};
92+
elevenWs.send(JSON.stringify(audioMessage));
93+
}
94+
break;
95+
96+
case "stop":
97+
if (elevenWs?.readyState === WebSocket.OPEN) elevenWs.close();
98+
console.log("Stream ended");
99+
break;
100+
101+
default:
102+
console.log(`Unhandled event: ${msg.event}`);
103+
}
104+
} catch (error) {
105+
console.error("Error processing Twilio message:", error, streamSid);
106+
}
107+
});
108+
109+
// Close connection
110+
ws.on("close", () => {
111+
console.log("Connection closed by Twilio");
112+
if (elevenWs?.readyState === WebSocket.OPEN) elevenWs.close();
113+
});
114+
115+
// Set up Eleven Labs WebSocket
116+
async function setupElevenLabs() {
117+
try {
118+
const { data } = await axios.get(
119+
`https://api.elevenlabs.io/v1/convai/conversation/get_signed_url?agent_id=${process.env.AGENT_ID}`,
120+
{ headers: { "xi-api-key": process.env.API_KEY } }
121+
);
122+
elevenWs = new WebSocket(data.signed_url);
123+
124+
elevenWs.on("open", () => console.log("Connected to Eleven Labs"));
125+
elevenWs.on("message", (data) =>
126+
handleElevenLabsMessages(JSON.parse(data))
127+
);
128+
elevenWs.on("error", (error) =>
129+
console.error("Error with Eleven Labs WebSocket:", error)
130+
);
131+
elevenWs.on("close", () =>
132+
console.log("Disconnected from Eleven Labs")
133+
);
134+
} catch (error) {
135+
console.error("Error setting up Eleven Labs WebSocket:", error);
136+
}
137+
}
138+
139+
// Handle Eleven Labs WebSocket messages
140+
function handleElevenLabsMessages(message) {
141+
switch (message.type) {
142+
case "audio":
143+
if (streamSid) {
144+
const audioData = {
145+
event: "media",
146+
streamSid,
147+
media: {
148+
payload:
149+
message.audio.chunk || message.audio_event.audio_base_64,
150+
},
151+
};
152+
ws.send(JSON.stringify(audioData));
153+
}
154+
break;
155+
case "interruption":
156+
ws.send(JSON.stringify({ event: "clear", streamSid }));
157+
break;
158+
case "ping":
159+
if (message.ping_event?.event_id) {
160+
elevenWs.send(
161+
JSON.stringify({
162+
type: "pong",
163+
event_id: message.ping_event.event_id,
164+
})
165+
);
166+
}
167+
break;
168+
default:
169+
console.log(
170+
`Unhandled message type from Eleven Labs: ${message.type}`
171+
);
172+
}
173+
}
174+
});
175+
}
176+
});
177+
178+
// Start the server on port 8080
179+
server.listen(8080, () => {
180+
console.log(`Server is running on port 8080`);
181+
});

0 commit comments

Comments
 (0)