Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,15 @@ ngrok http 8081

Make note of the `Forwarding` URL. (e.g. `https://54c5-35-170-32-42.ngrok-free.app`)

cloudflared tunnel --url http://localhost:8081


### Websocket URL

Your server should now be accessible at the `Forwarding` URL when run, so set the `PUBLIC_URL` in `websocket-server/.env`. See `websocket-server/.env.example` for reference.

# Additional Notes

This repo isn't polished, and the security practices leave some to be desired. Please only use this as reference, and make sure to audit your app with security and engineering before deploying!


1 change: 1 addition & 0 deletions webapp/components/checklist-and-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default function ChecklistAndConfig({

// 2. Fetch numbers
res = await fetch("/api/twilio/numbers");
console.log("Fetched phone numbers:", res);
if (!res.ok) throw new Error("Failed to fetch phone numbers");
const numbersData = await res.json();
if (Array.isArray(numbersData) && numbersData.length > 0) {
Expand Down
61 changes: 61 additions & 0 deletions websocket-server/src/configs/envs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import dotenv from "dotenv";

const NODE_ENV = process.env.NODE_ENV || "development";
dotenv.config({ path: `.env.${NODE_ENV}` });
dotenv.config();

const requiredEnvs: {
OPENAI_API_KEY: string;
} = {
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
};


for (const [key, value] of Object.entries(requiredEnvs)) {
if (!value) {
console.error(`❌ Missing required environment variable: ${key}`);
process.exit(1);
}
}

export const envs: {
OPENAI_API_KEY: string;
OPENAI_MODEL: string;
PORT: string | number;
HOST: string;
PUBLIC_URL: string;
IS_PRODUCTION: boolean;
IS_DEVELOPMENT: boolean;
WS_HEARTBEAT_INTERVAL: string | number;
MAX_RECONNECT_ATTEMPTS: string | number;
TWILIO_ACCOUNT_SID: string;
TWILIO_AUTH_TOKEN: string;
CALENDLY_ACCESS_TOKEN: string;
} = {

OPENAI_API_KEY: requiredEnvs.OPENAI_API_KEY,
OPENAI_MODEL: process.env.OPENAI_MODEL || "gpt-4o-realtime-preview-2024-12-17",


PORT: process.env.PORT || 8081,
HOST: process.env.HOST || "localhost",
PUBLIC_URL: process.env.PUBLIC_URL || `http://${process.env.HOST || "localhost"}:${process.env.PORT || 8081}`,


IS_PRODUCTION: NODE_ENV === "production",
IS_DEVELOPMENT: NODE_ENV === "development",


WS_HEARTBEAT_INTERVAL: process.env.WS_HEARTBEAT_INTERVAL || 30000,
MAX_RECONNECT_ATTEMPTS: process.env.MAX_RECONNECT_ATTEMPTS || 3,



TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID || "",
TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN || "",


CALENDLY_ACCESS_TOKEN: process.env.CALENDLY_ACCESS_TOKEN || "",

};

1 change: 1 addition & 0 deletions websocket-server/src/configs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./envs";
68 changes: 68 additions & 0 deletions websocket-server/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { WebSocket } from "ws";

export interface Session {
twilioConn?: WebSocket;
frontendConn?: WebSocket;
modelConn?: WebSocket;
config?: any;
streamSid?: string;
}

export interface FunctionCallItem {
name: string;
arguments: string;
call_id?: string;
}

export interface FunctionSchema {
name: string;
type: "function";
description?: string;
parameters: {
type: string;
properties: Record<string, { type: string; description?: string }>;
required: string[];
};
}

export interface FunctionHandler {
schema: FunctionSchema;
handler: (args: any) => Promise<string>;
}



export enum ConnectionType {
TWILIO = 'twilio',
FRONTEND = 'frontend',
MODEL = 'model'
}

export enum SessionEvent {
CONNECTION_ESTABLISHED = 'connection_established',
CONNECTION_CLOSED = 'connection_closed',
FUNCTION_CALL_STARTED = 'function_call_started',
FUNCTION_CALL_COMPLETED = 'function_call_completed',
FUNCTION_CALL_ERROR = 'function_call_error',
SESSION_RESET = 'session_reset',
ERROR = 'error'
}

export interface SessionData {
twilioConn?: WebSocket;
frontendConn?: WebSocket;
modelConn?: WebSocket;
streamSid?: string;
saved_config?: any;
lastAssistantItem?: string;
responseStartTimestamp?: number;
latestMediaTimestamp?: number;
openAIApiKey?: string;
}

export interface ConnectionStats {
totalConnections: number;
activeConnections: number;
connectionDuration: number;
lastActivity: Date;
}
38 changes: 14 additions & 24 deletions websocket-server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
import express from "express";
import { WebSocketServer, WebSocket } from "ws";
import { IncomingMessage } from "http";
import dotenv from "dotenv";
import http from "http";
import { readFileSync } from "fs";
import { join } from "path";
import cors from "cors";
import {
handleCallConnection,
handleFrontendConnection,
} from "./sessionManager";
import functions from "./functionHandlers";

dotenv.config();

const PORT = parseInt(process.env.PORT || "8081", 10);
const PUBLIC_URL = process.env.PUBLIC_URL || "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
import { WebSocketSessionManager } from "./services";
import { envs } from "./configs";
import functionHandlers from "./services/function-handlers";

if (!OPENAI_API_KEY) {
console.error("OPENAI_API_KEY environment variable is required");
process.exit(1);
}
const sessionManager = new WebSocketSessionManager();

const app = express();
app.use(cors());
Expand All @@ -34,21 +24,20 @@ const twimlPath = join(__dirname, "twiml.xml");
const twimlTemplate = readFileSync(twimlPath, "utf-8");

app.get("/public-url", (req, res) => {
res.json({ publicUrl: PUBLIC_URL });
res.json({ publicUrl: envs.PUBLIC_URL });
});


app.all("/twiml", (req, res) => {
const wsUrl = new URL(PUBLIC_URL);
const wsUrl = new URL(envs.PUBLIC_URL);
wsUrl.protocol = "wss:";
wsUrl.pathname = `/call`;

const twimlContent = twimlTemplate.replace("{{WS_URL}}", wsUrl.toString());
res.type("text/xml").send(twimlContent);
});

// New endpoint to list available tools (schemas)
app.get("/tools", (req, res) => {
res.json(functions.map((f) => f.schema));
res.json(functionHandlers.map((f) => f.schema));
});

let currentCall: WebSocket | null = null;
Expand All @@ -68,16 +57,17 @@ wss.on("connection", (ws: WebSocket, req: IncomingMessage) => {
if (type === "call") {
if (currentCall) currentCall.close();
currentCall = ws;
handleCallConnection(currentCall, OPENAI_API_KEY);
sessionManager.handleCallConnection(currentCall, envs.OPENAI_API_KEY);
} else if (type === "logs") {
if (currentLogs) currentLogs.close();
currentLogs = ws;
handleFrontendConnection(currentLogs);
sessionManager.handleFrontendConnection(currentLogs);
} else {
ws.close();
}
});

server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

server.listen(envs.PORT, () => {
console.log(`Server running on http://localhost:${envs.PORT}`);
});
92 changes: 92 additions & 0 deletions websocket-server/src/services/function-handlers/calendly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { envs } from "../../configs";
import { FunctionHandler } from "../../interfaces";

export const scheduleCalendlyMeeting: FunctionHandler = {
schema: {
name: "schedule_calendly_meeting",
type: "function",
description: "Schedule a meeting using Calendly",
parameters: {
type: "object",
properties: {
eventType: {
type: "string",
description: "Calendly event type URI (e.g., 'https://api.calendly.com/event_types/AAAAAAAAAAAAAAAA')"
},
startTime: {
type: "string",
description: "Start time in ISO format (e.g., '2024-01-15T10:00:00Z')"
},
inviteeEmail: {
type: "string",
description: "Email of the person being invited"
},
inviteeName: {
type: "string",
description: "Name of the person being invited"
},
timezone: {
type: "string",
description: "Timezone (e.g., 'America/Sao_Paulo')"
},
},
required: ["eventType", "startTime", "inviteeEmail", "inviteeName"]
}
},
handler: async (args: {
eventType: string;
startTime: string;
inviteeEmail: string;
inviteeName: string;
timezone?: string;
questions?: Array<{ question: string; answer: string }>;
}) => {
try {
const response = await fetch('https://api.calendly.com/scheduled_events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${envs.CALENDLY_ACCESS_TOKEN}`
},
body: JSON.stringify({
event_type: args.eventType,
start_time: args.startTime,
invitee: {
email: args.inviteeEmail,
name: args.inviteeName,
timezone: args.timezone || 'UTC'
},
questions_and_responses: args.questions?.map(q => ({
question: q.question,
response: q.answer
})) || []
})
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(`Calendly API Error: ${errorData.message || response.statusText}`);
}
const data = await response.json();
return JSON.stringify({
success: true,
eventId: data.resource.uri,
meetingLink: data.resource.location?.join_url || data.resource.location?.location,
startTime: data.resource.start_time,
endTime: data.resource.end_time,
status: data.resource.status,
invitee: {
name: args.inviteeName,
email: args.inviteeEmail
}
});
} catch (error) {
console.error('Error creating Calendly meeting:', error);
return JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
});
}
}
};

11 changes: 11 additions & 0 deletions websocket-server/src/services/function-handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FunctionHandler } from "../../interfaces";
import { getWeatherFromCoords } from "./weather";
import { scheduleCalendlyMeeting } from "./calendly";

const functionHandlers: FunctionHandler[] = [];

functionHandlers.push(getWeatherFromCoords);
functionHandlers.push(scheduleCalendlyMeeting);


export default functionHandlers;
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { FunctionHandler } from "./types";
import { FunctionHandler } from "../../interfaces";

const functions: FunctionHandler[] = [];

functions.push({
export const getWeatherFromCoords: FunctionHandler = {
schema: {
name: "get_weather_from_coords",
type: "function",
Expand All @@ -28,6 +27,6 @@ functions.push({
const currentTemp = data.current?.temperature_2m;
return JSON.stringify({ temp: currentTemp });
},
});
};


export default functions;
1 change: 1 addition & 0 deletions websocket-server/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./session-manager";
Loading