Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 953fb3aff1 | |||
| 96d10c3df0 | |||
| 616d4472d2 |
236
scripts/cli.ts
236
scripts/cli.ts
|
|
@ -5,174 +5,126 @@ import {$} from "bun";
|
||||||
const SERVICES = {
|
const SERVICES = {
|
||||||
ap: "phone-ap",
|
ap: "phone-ap",
|
||||||
web: "phone-web",
|
web: "phone-web",
|
||||||
};
|
phone: "phone",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const commands = {
|
const COMMANDS = {
|
||||||
status: "Show status of all services",
|
status: "Show service status",
|
||||||
logs: "Show recent logs from all services (last 50 lines)",
|
logs: "Show recent logs (last 50 lines)",
|
||||||
tail: "Tail logs from all services in real-time",
|
tail: "Tail logs in real-time",
|
||||||
restart: "Restart all services",
|
restart: "Restart service (requires sudo)",
|
||||||
stop: "Stop all services",
|
stop: "Stop service (requires sudo)",
|
||||||
start: "Start all services",
|
start: "Start service (requires sudo)",
|
||||||
"ap-status": "Show status of AP service",
|
} as const;
|
||||||
"ap-logs": "Show recent logs from AP service (last 50 lines)",
|
|
||||||
"ap-tail": "Tail logs from AP service in real-time",
|
|
||||||
"ap-restart": "Restart AP service",
|
|
||||||
"ap-stop": "Stop AP service",
|
|
||||||
"ap-start": "Start AP service",
|
|
||||||
"web-status": "Show status of web service",
|
|
||||||
"web-logs": "Show recent logs from web service (last 50 lines)",
|
|
||||||
"web-tail": "Tail logs from web service in real-time",
|
|
||||||
"web-restart": "Restart web service",
|
|
||||||
"web-stop": "Stop web service",
|
|
||||||
"web-start": "Start web service",
|
|
||||||
help: "Show this help message",
|
|
||||||
};
|
|
||||||
|
|
||||||
const command = process.argv[2];
|
const showHelp = () => {
|
||||||
|
|
||||||
if (!command || command === "help") {
|
|
||||||
console.log(`
|
console.log(`
|
||||||
Phone CLI - Service Management Tool
|
Phone CLI - Service Management
|
||||||
|
|
||||||
Usage: bun cli <command>
|
Usage: cli SERVICE COMMAND [-v]
|
||||||
|
|
||||||
All Services:
|
Services:
|
||||||
status Show status of all services
|
ap WiFi AP Monitor (phone-ap.service)
|
||||||
logs Show recent logs from all services (last 50 lines)
|
web Web Server (phone-web.service)
|
||||||
tail Tail logs from all services in real-time
|
phone Phone Application (phone.service)
|
||||||
restart Restart all services
|
|
||||||
stop Stop all services
|
|
||||||
start Start all services
|
|
||||||
|
|
||||||
AP Service (phone-ap):
|
Commands:
|
||||||
ap-status Show AP status
|
status Show service status
|
||||||
ap-logs Show AP logs (last 50 lines)
|
logs Show recent logs (last 50 lines)
|
||||||
ap-tail Tail AP logs in real-time
|
tail Tail logs in real-time
|
||||||
ap-restart Restart AP service
|
restart Restart service (requires sudo)
|
||||||
ap-stop Stop AP service
|
stop Stop service (requires sudo)
|
||||||
ap-start Start AP service
|
start Start service (requires sudo)
|
||||||
|
|
||||||
Web Service (phone-web):
|
Options:
|
||||||
web-status Show web status
|
-v Verbose mode - show actual systemd commands
|
||||||
web-logs Show web logs (last 50 lines)
|
|
||||||
web-tail Tail web logs in real-time
|
|
||||||
web-restart Restart web service
|
|
||||||
web-stop Stop web service
|
|
||||||
web-start Start web service
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bun cli status
|
cli ap status
|
||||||
bun cli ap-logs
|
cli web logs
|
||||||
bun cli web-tail
|
cli phone tail
|
||||||
sudo bun cli ap-restart
|
cli -v ap status
|
||||||
|
sudo cli ap restart
|
||||||
`);
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse arguments
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// Check for help
|
||||||
|
if (args.length === 0 || args[0] === "help") {
|
||||||
|
showHelp();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.keys(commands).includes(command)) {
|
// Extract verbose flag and remaining args
|
||||||
console.error(`❌ Unknown command: ${command}`);
|
const verbose = args.includes("-v");
|
||||||
console.log(`Run 'bun cli.ts help' to see available commands`);
|
const [service, command] = args.filter(arg => arg !== "-v");
|
||||||
|
|
||||||
|
// Validate service
|
||||||
|
if (!service || !(service in SERVICES)) {
|
||||||
|
console.error(`❌ Unknown service: ${service || "(missing)"}`);
|
||||||
|
console.log(`Available services: ${Object.keys(SERVICES).join(", ")}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n🔧 Phone CLI - ${command}\n`);
|
// Validate command
|
||||||
|
if (!command || !(command in COMMANDS)) {
|
||||||
|
console.error(`❌ Unknown command: ${command || "(missing)"}`);
|
||||||
|
console.log(`Available commands: ${Object.keys(COMMANDS).join(", ")}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Parse service-specific commands
|
// Get systemd service name
|
||||||
const match = command.match(/^(ap|web)-(.+)$/);
|
const serviceName = SERVICES[service as keyof typeof SERVICES];
|
||||||
if (match) {
|
|
||||||
const [, prefix, action] = match;
|
|
||||||
const service = SERVICES[prefix as keyof typeof SERVICES];
|
|
||||||
|
|
||||||
switch (action) {
|
// Execute command
|
||||||
case "status":
|
console.log(`\n🔧 Phone CLI - ${service} ${command}\n`);
|
||||||
console.log(`━━━ ${service}.service ━━━`);
|
|
||||||
await $`systemctl status ${service}.service --no-pager -l`.nothrow();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "logs":
|
const logCommand = (cmd: string) => {
|
||||||
console.log(`📋 Recent logs (last 50 lines):\n`);
|
if (verbose) {
|
||||||
await $`journalctl -u ${service}.service -n 50 --no-pager`.nothrow();
|
console.log(`→ ${cmd}\n`);
|
||||||
break;
|
|
||||||
|
|
||||||
case "tail":
|
|
||||||
console.log(`📡 Tailing logs (Ctrl+C to stop)...\n`);
|
|
||||||
await $`journalctl -u ${service}.service -f --no-pager`.nothrow();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "restart":
|
|
||||||
console.log(`🔄 Restarting ${service}.service...\n`);
|
|
||||||
await $`sudo systemctl restart ${service}.service`;
|
|
||||||
console.log(`✓ ${service}.service restarted!`);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "stop":
|
|
||||||
console.log(`🛑 Stopping ${service}.service...\n`);
|
|
||||||
await $`sudo systemctl stop ${service}.service`;
|
|
||||||
console.log(`✓ ${service}.service stopped!`);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "start":
|
|
||||||
console.log(`▶️ Starting ${service}.service...\n`);
|
|
||||||
await $`sudo systemctl start ${service}.service`;
|
|
||||||
console.log(`✓ ${service}.service started!`);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
// All-services commands
|
|
||||||
const allServices = Object.values(SERVICES);
|
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "status":
|
case "status":
|
||||||
for (const service of allServices) {
|
logCommand(`systemctl status ${serviceName}.service --no-pager -l`);
|
||||||
console.log(`━━━ ${service}.service ━━━`);
|
await $`systemctl status ${serviceName}.service --no-pager -l`.nothrow();
|
||||||
await $`systemctl status ${service}.service --no-pager -l`.nothrow();
|
break;
|
||||||
console.log("");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "logs":
|
case "logs":
|
||||||
console.log("📋 Recent logs (last 50 lines):\n");
|
console.log(`📋 Recent logs (last 50 lines):\n`);
|
||||||
const serviceFlags = allServices.map(s => `-u ${s}.service`).join(" ");
|
logCommand(`journalctl -u ${serviceName}.service -n 50 --no-pager`);
|
||||||
await $`journalctl ${serviceFlags} -n 50 --no-pager`.nothrow();
|
await $`journalctl -u ${serviceName}.service -n 50 --no-pager`.nothrow();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "tail":
|
case "tail":
|
||||||
console.log("📡 Tailing logs (Ctrl+C to stop)...\n");
|
console.log(`📡 Tailing logs (Ctrl+C to stop)...\n`);
|
||||||
const tailFlags = allServices.map(s => `-u ${s}.service`).join(" ");
|
logCommand(`journalctl -u ${serviceName}.service -f --no-pager`);
|
||||||
await $`journalctl ${tailFlags} -f --no-pager`.nothrow();
|
await $`journalctl -u ${serviceName}.service -f --no-pager`.nothrow();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "restart":
|
case "restart":
|
||||||
console.log("🔄 Restarting services...\n");
|
console.log(`🔄 Restarting ${serviceName}.service...\n`);
|
||||||
for (const service of allServices) {
|
logCommand(`sudo systemctl restart ${serviceName}.service`);
|
||||||
console.log(`Restarting ${service}.service...`);
|
await $`sudo systemctl restart ${serviceName}.service`;
|
||||||
await $`sudo systemctl restart ${service}.service`;
|
console.log(`✓ ${serviceName}.service restarted!`);
|
||||||
console.log(`✓ ${service}.service restarted`);
|
break;
|
||||||
}
|
|
||||||
console.log("\n✓ All services restarted!");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "stop":
|
case "stop":
|
||||||
console.log("🛑 Stopping services...\n");
|
console.log(`🛑 Stopping ${serviceName}.service...\n`);
|
||||||
for (const service of allServices) {
|
logCommand(`sudo systemctl stop ${serviceName}.service`);
|
||||||
console.log(`Stopping ${service}.service...`);
|
await $`sudo systemctl stop ${serviceName}.service`;
|
||||||
await $`sudo systemctl stop ${service}.service`;
|
console.log(`✓ ${serviceName}.service stopped!`);
|
||||||
console.log(`✓ ${service}.service stopped`);
|
break;
|
||||||
}
|
|
||||||
console.log("\n✓ All services stopped!");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "start":
|
case "start":
|
||||||
console.log("▶️ Starting services...\n");
|
console.log(`▶️ Starting ${serviceName}.service...\n`);
|
||||||
for (const service of allServices) {
|
logCommand(`sudo systemctl start ${serviceName}.service`);
|
||||||
console.log(`Starting ${service}.service...`);
|
await $`sudo systemctl start ${serviceName}.service`;
|
||||||
await $`sudo systemctl start ${service}.service`;
|
console.log(`✓ ${serviceName}.service started!`);
|
||||||
console.log(`✓ ${service}.service started`);
|
break;
|
||||||
}
|
|
||||||
console.log("\n✓ All services started!");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# Agent
|
# Agent
|
||||||
|
|
||||||
A clean, reusable wrapper for ElevenLabs conversational AI WebSocket protocol. Uses Signal-based events and provides simple tool registration.
|
A clean, reusable wrapper for ElevenLabs conversational AI WebSocket protocol. Uses events and provides simple tool registration.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Agent } from './pi/agent'
|
import { Agent } from "./pi/agent"
|
||||||
import Buzz from './pi/buzz'
|
import Buzz from "./pi/buzz"
|
||||||
|
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
agentId: process.env.ELEVEN_AGENT_ID!,
|
agentId: process.env.ELEVEN_AGENT_ID!,
|
||||||
|
|
@ -14,27 +14,24 @@ const agent = new Agent({
|
||||||
tools: {
|
tools: {
|
||||||
search_web: async (args) => {
|
search_web: async (args) => {
|
||||||
return { results: [`Result for ${args.query}`] }
|
return { results: [`Result for ${args.query}`] }
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
const player = await Buzz.player()
|
const player = await Buzz.player()
|
||||||
let playback = player.playStream()
|
let playback = player.playStream()
|
||||||
|
|
||||||
agent.events.connect((event) => {
|
agent.events.on((event) => {
|
||||||
if (event.type === 'audio') {
|
if (event.type === "audio") {
|
||||||
const audioBuffer = Buffer.from(event.audioBase64, 'base64')
|
const audioBuffer = Buffer.from(event.audioBase64, "base64")
|
||||||
if (!playback.isPlaying) playback = player.playStream()
|
if (!playback.isPlaying) playback = player.playStream()
|
||||||
playback.write(audioBuffer)
|
playback.write(audioBuffer)
|
||||||
}
|
} else if (event.type === "interruption") {
|
||||||
else if (event.type === 'interruption') {
|
|
||||||
playback.stop()
|
playback.stop()
|
||||||
}
|
} else if (event.type === "user_transcript") {
|
||||||
else if (event.type === 'user_transcript') {
|
|
||||||
console.log(`User: ${event.transcript}`)
|
console.log(`User: ${event.transcript}`)
|
||||||
}
|
} else if (event.type === "agent_response") {
|
||||||
else if (event.type === 'agent_response') {
|
|
||||||
console.log(`Agent: ${event.response}`)
|
console.log(`Agent: ${event.response}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -68,7 +65,7 @@ for await (const chunk of recording.stream()) {
|
||||||
if (rms > vadThreshold) {
|
if (rms > vadThreshold) {
|
||||||
// Speech detected! Start conversation
|
// Speech detected! Start conversation
|
||||||
agent = new Agent({ agentId, apiKey, tools })
|
agent = new Agent({ agentId, apiKey, tools })
|
||||||
agent.events.connect(eventHandler)
|
agent.events.on(eventHandler)
|
||||||
await agent.start()
|
await agent.start()
|
||||||
|
|
||||||
// Send buffered audio
|
// Send buffered audio
|
||||||
|
|
@ -112,7 +109,7 @@ new Agent({
|
||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
- `agent.events: Signal<AgentEvent>` - Connect to receive all events
|
- `agent.events: Emitter<AgentEvent>` - Connect to receive all events
|
||||||
- `agent.isConnected: boolean` - Current connection state
|
- `agent.isConnected: boolean` - Current connection state
|
||||||
- `agent.conversationId?: string` - Available after connected event
|
- `agent.conversationId?: string` - Available after connected event
|
||||||
|
|
||||||
|
|
@ -121,11 +118,13 @@ new Agent({
|
||||||
All events are emitted through `agent.events`:
|
All events are emitted through `agent.events`:
|
||||||
|
|
||||||
### Connection
|
### Connection
|
||||||
|
|
||||||
- `{ type: 'connected', conversationId, audioFormat }`
|
- `{ type: 'connected', conversationId, audioFormat }`
|
||||||
- `{ type: 'disconnected' }`
|
- `{ type: 'disconnected' }`
|
||||||
- `{ type: 'error', error }`
|
- `{ type: 'error', error }`
|
||||||
|
|
||||||
### Conversation
|
### Conversation
|
||||||
|
|
||||||
- `{ type: 'user_transcript', transcript }`
|
- `{ type: 'user_transcript', transcript }`
|
||||||
- `{ type: 'agent_response', response }`
|
- `{ type: 'agent_response', response }`
|
||||||
- `{ type: 'agent_response_correction', original, corrected }`
|
- `{ type: 'agent_response_correction', original, corrected }`
|
||||||
|
|
@ -134,11 +133,13 @@ All events are emitted through `agent.events`:
|
||||||
- `{ type: 'interruption', eventId }`
|
- `{ type: 'interruption', eventId }`
|
||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
|
|
||||||
- `{ type: 'tool_call', name, args, callId }`
|
- `{ type: 'tool_call', name, args, callId }`
|
||||||
- `{ type: 'tool_result', name, result, callId }`
|
- `{ type: 'tool_result', name, result, callId }`
|
||||||
- `{ type: 'tool_error', name, error, callId }`
|
- `{ type: 'tool_error', name, error, callId }`
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
- `{ type: 'vad_score', score }`
|
- `{ type: 'vad_score', score }`
|
||||||
- `{ type: 'ping', eventId, pingMs }`
|
- `{ type: 'ping', eventId, pingMs }`
|
||||||
|
|
||||||
|
|
@ -146,7 +147,7 @@ All events are emitted through `agent.events`:
|
||||||
|
|
||||||
- **Generic**: Not tied to phone systems, works in any context
|
- **Generic**: Not tied to phone systems, works in any context
|
||||||
- **Flexible audio**: You control when to send audio, Agent just handles WebSocket
|
- **Flexible audio**: You control when to send audio, Agent just handles WebSocket
|
||||||
- **Event-driven**: All communication through Signal events, no throws
|
- **Event-driven**: All communication through events, no throws
|
||||||
- **Simple tools**: Just pass a function map to constructor
|
- **Simple tools**: Just pass a function map to constructor
|
||||||
- **Automatic buffering**: Sends buffered audio when connection opens
|
- **Automatic buffering**: Sends buffered audio when connection opens
|
||||||
- **Automatic chunking**: Handles 8000-byte chunking internally
|
- **Automatic chunking**: Handles 8000-byte chunking internally
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Signal } from "../utils/signal"
|
import { Emitter } from "../utils/emitter"
|
||||||
import type { AgentConfig, AgentEvent } from "./types"
|
import type { AgentConfig, AgentEvent } from "./types"
|
||||||
|
|
||||||
type AgentState = "disconnected" | "connecting" | "connected"
|
type AgentState = "disconnected" | "connecting" | "connected"
|
||||||
|
|
@ -11,7 +11,7 @@ export class Agent {
|
||||||
#chunkBuffer = new Uint8Array(0)
|
#chunkBuffer = new Uint8Array(0)
|
||||||
#chunkSize = 8000
|
#chunkSize = 8000
|
||||||
|
|
||||||
public readonly events = new Signal<AgentEvent>()
|
public readonly events = new Emitter<AgentEvent>()
|
||||||
public conversationId?: string
|
public conversationId?: string
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
|
|
|
||||||
17
src/phone.ts
17
src/phone.ts
|
|
@ -46,7 +46,7 @@ export const runPhone = async (agentId: string, agentKey: string) => {
|
||||||
using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 })
|
using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 })
|
||||||
using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 })
|
using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 })
|
||||||
|
|
||||||
await Buzz.setVolume(0.2)
|
await Buzz.setVolume(0.3)
|
||||||
log(`📞 Phone is ${hook.value ? "off hook" : "on hook"}`)
|
log(`📞 Phone is ${hook.value ? "off hook" : "on hook"}`)
|
||||||
|
|
||||||
playStartRing(ringer)
|
playStartRing(ringer)
|
||||||
|
|
@ -94,7 +94,7 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
||||||
const baresipConfig = join(import.meta.dir, "..", "baresip")
|
const baresipConfig = join(import.meta.dir, "..", "baresip")
|
||||||
const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
|
const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
|
||||||
|
|
||||||
baresip.registrationSuccess.connect(async () => {
|
baresip.registrationSuccess.on(async () => {
|
||||||
log("🐻 server connected")
|
log("🐻 server connected")
|
||||||
if (hook.value === 0) {
|
if (hook.value === 0) {
|
||||||
phoneService.send({ type: "initialized" })
|
phoneService.send({ type: "initialized" })
|
||||||
|
|
@ -103,17 +103,17 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
baresip.callReceived.connect(({ contact }) => {
|
baresip.callReceived.on(({ contact }) => {
|
||||||
log(`🐻 incoming call from ${contact}`)
|
log(`🐻 incoming call from ${contact}`)
|
||||||
phoneService.send({ type: "incoming-call", from: contact })
|
phoneService.send({ type: "incoming-call", from: contact })
|
||||||
})
|
})
|
||||||
|
|
||||||
baresip.callEstablished.connect(({ contact }) => {
|
baresip.callEstablished.on(({ contact }) => {
|
||||||
log(`🐻 call established with ${contact}`)
|
log(`🐻 call established with ${contact}`)
|
||||||
phoneService.send({ type: "answered" })
|
phoneService.send({ type: "answered" })
|
||||||
})
|
})
|
||||||
|
|
||||||
baresip.hungUp.connect(() => {
|
baresip.hungUp.on(() => {
|
||||||
log("🐻 call hung up")
|
log("🐻 call hung up")
|
||||||
phoneService.send({ type: "remote-hang-up" })
|
phoneService.send({ type: "remote-hang-up" })
|
||||||
})
|
})
|
||||||
|
|
@ -123,7 +123,7 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
||||||
phoneService.send({ type: "error", message: error.message })
|
phoneService.send({ type: "error", message: error.message })
|
||||||
})
|
})
|
||||||
|
|
||||||
baresip.error.connect(async ({ message }) => {
|
baresip.error.on(async ({ message }) => {
|
||||||
log.error("🐻 error:", message)
|
log.error("🐻 error:", message)
|
||||||
phoneService.send({ type: "error", message })
|
phoneService.send({ type: "error", message })
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
|
@ -200,7 +200,7 @@ const startListening = (service: Service<typeof phoneMachine>, agent: Agent) =>
|
||||||
|
|
||||||
let preConnectionBuffer: Uint8Array[] = []
|
let preConnectionBuffer: Uint8Array[] = []
|
||||||
|
|
||||||
agent.events.connect(async (event) => {
|
agent.events.on(async (event) => {
|
||||||
if (event.type === "disconnected") abortAgent.abort()
|
if (event.type === "disconnected") abortAgent.abort()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -250,7 +250,7 @@ const handleAgentEvents = (
|
||||||
) => {
|
) => {
|
||||||
const waitingIndicator = new WaitingSounds(player)
|
const waitingIndicator = new WaitingSounds(player)
|
||||||
|
|
||||||
agent.events.connect(async (event) => {
|
agent.events.on(async (event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "connected":
|
case "connected":
|
||||||
log("🤖 Connected to AI agent\n")
|
log("🤖 Connected to AI agent\n")
|
||||||
|
|
@ -272,6 +272,7 @@ const handleAgentEvents = (
|
||||||
|
|
||||||
case "interruption":
|
case "interruption":
|
||||||
log("🤖 User interrupted")
|
log("🤖 User interrupted")
|
||||||
|
await waitingIndicator.stop()
|
||||||
streamPlayback?.stop()
|
streamPlayback?.stop()
|
||||||
streamPlayback = player.playStream() // Reset playback stream
|
streamPlayback = player.playStream() // Reset playback stream
|
||||||
break
|
break
|
||||||
|
|
|
||||||
20
src/sip.ts
20
src/sip.ts
|
|
@ -1,15 +1,15 @@
|
||||||
import log from "./utils/log.ts"
|
import log from "./utils/log.ts"
|
||||||
import { Signal } from "./utils/signal.ts"
|
import { Emitter } from "./utils/emitter.ts"
|
||||||
import { processStdout, processStderr } from "./utils/stdio.ts"
|
import { processStdout, processStderr } from "./utils/stdio.ts"
|
||||||
|
|
||||||
export class Baresip {
|
export class Baresip {
|
||||||
baresipArgs: string[]
|
baresipArgs: string[]
|
||||||
process?: Bun.PipedSubprocess
|
process?: Bun.PipedSubprocess
|
||||||
callEstablished = new Signal<{ contact: string }>()
|
callEstablished = new Emitter<{ contact: string }>()
|
||||||
callReceived = new Signal<{ contact: string }>()
|
callReceived = new Emitter<{ contact: string }>()
|
||||||
hungUp = new Signal()
|
hungUp = new Emitter()
|
||||||
error = new Signal<{ message: string }>()
|
error = new Emitter<{ message: string }>()
|
||||||
registrationSuccess = new Signal()
|
registrationSuccess = new Emitter()
|
||||||
|
|
||||||
constructor(baresipArgs: string[]) {
|
constructor(baresipArgs: string[]) {
|
||||||
this.baresipArgs = baresipArgs
|
this.baresipArgs = baresipArgs
|
||||||
|
|
@ -48,10 +48,10 @@ export class Baresip {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectAll() {
|
disconnectAll() {
|
||||||
this.callEstablished.disconnect()
|
this.callEstablished.removeAllListeners()
|
||||||
this.callReceived.disconnect()
|
this.callReceived.removeAllListeners()
|
||||||
this.hungUp.disconnect()
|
this.hungUp.removeAllListeners()
|
||||||
this.registrationSuccess.disconnect()
|
this.registrationSuccess.removeAllListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
kill() {
|
kill() {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
|
||||||
const waitingIndicator = new WaitingSounds(player)
|
const waitingIndicator = new WaitingSounds(player)
|
||||||
|
|
||||||
// Set up agent event listeners
|
// Set up agent event listeners
|
||||||
agent.events.connect(async (event) => {
|
agent.events.on(async (event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "connected":
|
case "connected":
|
||||||
console.log("✅ Connected to AI agent\n")
|
console.log("✅ Connected to AI agent\n")
|
||||||
|
|
@ -162,7 +162,7 @@ if (!apiKey) {
|
||||||
|
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
console.error(
|
console.error(
|
||||||
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required"
|
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required",
|
||||||
)
|
)
|
||||||
console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai")
|
console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|
|
||||||
42
src/utils/emitter.ts
Normal file
42
src/utils/emitter.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* How to use Emitter:
|
||||||
|
*
|
||||||
|
* Create an emitter:
|
||||||
|
* const chat = new Emitter<{ username: string, message: string }>()
|
||||||
|
*
|
||||||
|
* Listen to events:
|
||||||
|
* const off = chat.on((data) => {
|
||||||
|
* const {username, message} = data;
|
||||||
|
* console.log(`${username} said "${message}"`);
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* Emit an event:
|
||||||
|
* chat.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
|
||||||
|
*
|
||||||
|
* Remove a specific listener:
|
||||||
|
* off(); // The off function is returned when you add a listener
|
||||||
|
*
|
||||||
|
* Remove all listeners:
|
||||||
|
* chat.removeAllListeners()
|
||||||
|
*/
|
||||||
|
export class Emitter<T = void> {
|
||||||
|
private listeners: Array<(data: T) => void> = []
|
||||||
|
|
||||||
|
on(listener: (data: T) => void) {
|
||||||
|
this.listeners.push(listener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners = this.listeners.filter((l) => l !== listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(data: T) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners() {
|
||||||
|
this.listeners = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
let showDebug = true
|
let showDebug = process.env.DEBUG ?? false
|
||||||
let showInfo = true
|
let showInfo = true
|
||||||
|
|
||||||
export function setLogLevel(level: "debug" | "info" | "error") {
|
export function setLogLevel(level: "debug" | "info" | "error") {
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
/**
|
|
||||||
* How to use a Signal:
|
|
||||||
*
|
|
||||||
* Create a signal:
|
|
||||||
* const chatSignal = new Signal<{ username: string, message: string }>()
|
|
||||||
*
|
|
||||||
* Connect to the signal:
|
|
||||||
* const disconnect = chatSignal.connect((data) => {
|
|
||||||
* const {username, message} = data;
|
|
||||||
* console.log(`${username} said "${message}"`);
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* Emit a signal:
|
|
||||||
* chatSignal.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Disconnect a single listener:
|
|
||||||
* disconnect(); // The disconnect function is returned when you connect to a signal
|
|
||||||
*
|
|
||||||
* Disconnect all listeners:
|
|
||||||
* chatSignal.disconnect()
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class Signal<T extends object | void> {
|
|
||||||
private listeners: Array<(data: T) => void> = []
|
|
||||||
|
|
||||||
connect(listenerOrSignal: Signal<T> | ((data: T) => void)) {
|
|
||||||
let listener: (data: T) => void
|
|
||||||
|
|
||||||
// If it is a signal, forward the data to the signal
|
|
||||||
if (listenerOrSignal instanceof Signal) {
|
|
||||||
listener = (data: T) => listenerOrSignal.emit(data)
|
|
||||||
} else {
|
|
||||||
listener = listenerOrSignal
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeners.push(listener)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.listeners = this.listeners.filter((l) => l !== listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(data: T) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
listener(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.listeners = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How to use Emitter:
|
|
||||||
*
|
|
||||||
* Create an emitter:
|
|
||||||
* const chat = new Emitter<{ username: string, message: string }>()
|
|
||||||
*
|
|
||||||
* Listen to events:
|
|
||||||
* const off = chat.on((data) => {
|
|
||||||
* const {username, message} = data;
|
|
||||||
* console.log(`${username} said "${message}"`);
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* Emit an event:
|
|
||||||
* chat.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
|
|
||||||
*
|
|
||||||
* Remove a specific listener:
|
|
||||||
* off(); // The off function is returned when you add a listener
|
|
||||||
*
|
|
||||||
* Remove all listeners:
|
|
||||||
* chat.removeAllListeners()
|
|
||||||
*/
|
|
||||||
export class Emitter<T = void> {
|
|
||||||
private listeners: Array<(data: T) => void> = []
|
|
||||||
|
|
||||||
on(listener: (data: T) => void) {
|
|
||||||
this.listeners.push(listener)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.listeners = this.listeners.filter((l) => l !== listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(data: T) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
listener(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAllListeners() {
|
|
||||||
this.listeners = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,16 +6,22 @@ import { log } from "console"
|
||||||
export class WaitingSounds {
|
export class WaitingSounds {
|
||||||
typingPlayback?: Buzz.Playback
|
typingPlayback?: Buzz.Playback
|
||||||
speakingPlayback?: Buzz.Playback
|
speakingPlayback?: Buzz.Playback
|
||||||
|
playing = false
|
||||||
|
|
||||||
constructor(private player: Buzz.Player) {}
|
constructor(private player: Buzz.Player) {}
|
||||||
|
|
||||||
async start(operatorStream: Buzz.StreamingPlayback) {
|
async start(operatorStream: Buzz.StreamingPlayback) {
|
||||||
if (this.typingPlayback) return // Already playing
|
if (this.playing) return // Already playing
|
||||||
|
|
||||||
|
this.playing = true
|
||||||
this.#startTypingSounds()
|
this.#startTypingSounds()
|
||||||
this.#startSpeakingSounds(operatorStream)
|
this.#startSpeakingSounds(operatorStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPlaying() {
|
||||||
|
return this.playing
|
||||||
|
}
|
||||||
|
|
||||||
async #startTypingSounds() {
|
async #startTypingSounds() {
|
||||||
return new Promise<void>(async (resolve) => {
|
return new Promise<void>(async (resolve) => {
|
||||||
do {
|
do {
|
||||||
|
|
@ -29,7 +35,7 @@ export class WaitingSounds {
|
||||||
const typingSound = getSound(dir)
|
const typingSound = getSound(dir)
|
||||||
this.typingPlayback = await this.player.play(typingSound)
|
this.typingPlayback = await this.player.play(typingSound)
|
||||||
await this.typingPlayback.finished()
|
await this.typingPlayback.finished()
|
||||||
} while (this.typingPlayback)
|
} while (this.isPlaying)
|
||||||
|
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
|
@ -64,20 +70,17 @@ export class WaitingSounds {
|
||||||
this.speakingPlayback = await this.player.play(speakingSound)
|
this.speakingPlayback = await this.player.play(speakingSound)
|
||||||
playedSounds.add(speakingSound)
|
playedSounds.add(speakingSound)
|
||||||
await this.speakingPlayback.finished()
|
await this.speakingPlayback.finished()
|
||||||
} while (this.typingPlayback)
|
} while (this.isPlaying)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
log(`🛑 Stopping waiting sounds. Has typingPlayback: ${!!this.typingPlayback}`)
|
log(`🛑 Stopping waiting sounds. Playing? ${this.playing}`)
|
||||||
if (!this.typingPlayback) return
|
if (!this.playing) return
|
||||||
|
this.playing = false
|
||||||
|
|
||||||
// Quicky undefine this to stop the loops
|
await Promise.all([this.typingPlayback?.stop(), this.speakingPlayback?.finished()])
|
||||||
const typingPlayback = this.typingPlayback
|
|
||||||
this.typingPlayback = undefined
|
|
||||||
|
|
||||||
await Promise.all([typingPlayback.stop(), this.speakingPlayback?.finished()])
|
|
||||||
log("🛑 Waiting sounds stopped")
|
log("🛑 Waiting sounds stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user