From c1cd705d490b03aa0b88c20da3b2c4037d471270 Mon Sep 17 00:00:00 2001 From: Bijit Mondal Date: Fri, 13 Feb 2026 14:21:38 +0530 Subject: [PATCH] voice agent works --- example/demo.ts | 146 ++++++++++-- example/ws-server.ts | 150 +++++++++++- output.mp3 | Bin 0 -> 49920 bytes src/VoiceAgent.ts | 555 +++++++++++++++++++++++++++++++++++++++---- src/index.ts | 2 +- 5 files changed, 777 insertions(+), 76 deletions(-) create mode 100644 output.mp3 diff --git a/example/demo.ts b/example/demo.ts index 032fb64..0b36731 100644 --- a/example/demo.ts +++ b/example/demo.ts @@ -3,56 +3,160 @@ import { VoiceAgent } from "../src"; import { tool } from "ai"; import { z } from "zod"; import { openai } from "@ai-sdk/openai"; +import { writeFile } from "fs/promises"; // 1. Define Tools using standard AI SDK const weatherTool = tool({ - description: 'Get the weather in a location', + description: "Get the weather in a location", inputSchema: z.object({ - location: z.string().describe('The location to get the weather for'), + location: z.string().describe("The location to get the weather for"), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, + conditions: ["sunny", "cloudy", "rainy", "partly cloudy"][Math.floor(Math.random() * 4)], }), }); -// 2. Initialize Agent +const timeTool = tool({ + description: "Get the current time", + inputSchema: z.object({}), + execute: async () => ({ + time: new Date().toLocaleTimeString(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }), +}); + +// 2. Initialize Agent with full voice support const agent = new VoiceAgent({ - model: openai('gpt-4o'), - instructions: "You are a helpful voice assistant. Use tools when needed.", + // Chat model for text generation + model: openai("gpt-4o"), + // Transcription model for speech-to-text + transcriptionModel: openai.transcription("whisper-1"), + // Speech model for text-to-speech + speechModel: openai.speech("gpt-4o-mini-tts"), + // System instructions + instructions: `You are a helpful voice assistant. +Keep responses concise and conversational since they will be spoken aloud. +Use tools when needed to provide accurate information.`, + // TTS voice configuration + voice: "alloy", // Options: alloy, echo, fable, onyx, nova, shimmer + speechInstructions: "Speak in a friendly, natural conversational tone.", + outputFormat: "mp3", + // WebSocket endpoint endpoint: process.env.VOICE_WS_ENDPOINT, + // Tools tools: { - getWeather: weatherTool, // Pass the AI SDK tool directly + getWeather: weatherTool, + getTime: timeTool, }, }); // 3. Handle Events -agent.on("connected", () => console.log("Connected to WebSocket")); -// Handle incoming audio from AI (play this to user) -agent.on("audio", (base64Audio: string) => { - // process.stdout.write(Buffer.from(base64Audio, 'base64')); +// Connection events +agent.on("connected", () => console.log("✓ Connected to WebSocket")); +agent.on("disconnected", () => console.log("✗ Disconnected from WebSocket")); + +// Transcription events (when audio is converted to text) +agent.on("transcription", ({ text, language }: { text: string; language?: string }) => { + console.log(`[Transcription] (${language || "unknown"}): ${text}`); }); -// Logs -agent.on("text", (msg: { role: string; text: string }) => console.log(`${msg.role}: ${msg.text}`)); -agent.on("tool_start", ({ name }: { name: string }) => console.log(`[System] Calling ${name}...`)); +// Text events (user input and assistant responses) +agent.on("text", (msg: { role: string; text: string }) => { + const prefix = msg.role === "user" ? "👤 User" : "🤖 Assistant"; + console.log(`${prefix}: ${msg.text}`); +}); -// 4. Start (wrap in async function since we can't use top-level await) +// Streaming text delta events (real-time text chunks) +agent.on("text_delta", ({ text }: { text: string }) => { + process.stdout.write(text); +}); + +// Tool events +agent.on("tool_start", ({ name, input }: { name: string; input?: unknown }) => { + console.log(`\n[Tool] Calling ${name}...`, input ? JSON.stringify(input) : ""); +}); + +agent.on("tool_result", ({ name, result }: { name: string; result: unknown }) => { + console.log(`[Tool] ${name} result:`, JSON.stringify(result)); +}); + +// Speech events +agent.on("speech_start", ({ text }: { text: string }) => { + console.log(`[TTS] Generating speech for: "${text.substring(0, 50)}..."`); +}); + +agent.on("speech_complete", () => { + console.log("[TTS] Speech generation complete"); +}); + +// Audio events (when TTS audio is generated) +agent.on("audio", async (audio: { data: string; format: string; uint8Array: Uint8Array }) => { + console.log(`[Audio] Received ${audio.format} audio (${audio.uint8Array.length} bytes)`); + // Optionally save to file for testing + await writeFile(`output.${audio.format}`, Buffer.from(audio.uint8Array)); +}); + +// Error handling +agent.on("error", (error: Error) => { + console.error("[Error]", error.message); +}); + +// 4. Main execution (async () => { - try { - // For now: text-only sanity check, no voice pipeline required. - await agent.sendText("What is the weather in Berlin?"); + console.log("\n=== Voice Agent Demo ==="); + console.log("Testing text-only mode (no WebSocket required)\n"); - // Optional: connect only when an endpoint is provided. + try { + // Test 1: Simple text query with streaming + console.log("--- Test 1: Weather Query ---"); + const response1 = await agent.sendText("What is the weather in Berlin?"); + console.log("\n"); + + // Test 2: Multi-turn conversation + console.log("--- Test 2: Follow-up Question ---"); + const response2 = await agent.sendText("What about Tokyo?"); + console.log("\n"); + + // Test 3: Time query + console.log("--- Test 3: Time Query ---"); + const response3 = await agent.sendText("What time is it?"); + console.log("\n"); + + // Show conversation history + console.log("--- Conversation History ---"); + const history = agent.getHistory(); + console.log(`Total messages: ${history.length}`); + + // Optional: Connect to WebSocket for real-time voice if (process.env.VOICE_WS_ENDPOINT) { + console.log("\n--- Connecting to WebSocket ---"); await agent.connect(process.env.VOICE_WS_ENDPOINT); - console.log("Agent connected successfully"); + console.log("Agent connected. Listening for audio input..."); + + // Keep the process running to receive WebSocket messages + // In a real app, you would stream microphone audio here } } catch (error) { console.error("Agent run failed:", error); + process.exit(1); } })(); -// 5. Simulate sending audio (in a real app, stream microphone data here) -// agent.sendAudio("Base64EncodedPCM16AudioData..."); \ No newline at end of file +// Example: How to send audio in a real application +// --------------------------------------------- +// import { readFile } from "fs/promises"; +// +// // Option 1: Send base64 encoded audio +// const audioBase64 = (await readFile("recording.mp3")).toString("base64"); +// await agent.sendAudio(audioBase64); +// +// // Option 2: Send raw audio buffer +// const audioBuffer = await readFile("recording.mp3"); +// await agent.sendAudioBuffer(audioBuffer); +// +// // Option 3: Transcribe audio directly +// const transcribedText = await agent.transcribeAudio(audioBuffer); +// console.log("Transcribed:", transcribedText); \ No newline at end of file diff --git a/example/ws-server.ts b/example/ws-server.ts index 1984b0c..aea5bd6 100644 --- a/example/ws-server.ts +++ b/example/ws-server.ts @@ -1,22 +1,73 @@ import "dotenv/config"; -import { WebSocketServer } from "ws"; +import { WebSocketServer, WebSocket } from "ws"; +import { readFile } from "fs/promises"; +import { existsSync } from "fs"; const endpoint = process.env.VOICE_WS_ENDPOINT || "ws://localhost:8080"; const url = new URL(endpoint); const port = Number(url.port || 8080); const host = url.hostname || "localhost"; +// Message types for type safety +interface BaseMessage { + type: string; +} + +interface TextDeltaMessage extends BaseMessage { + type: "text_delta"; + text: string; +} + +interface ToolCallMessage extends BaseMessage { + type: "tool_call"; + toolName: string; + toolCallId: string; + input: unknown; +} + +interface ToolResultMessage extends BaseMessage { + type: "tool_result"; + toolName: string; + toolCallId: string; + result: unknown; +} + +interface AudioMessage extends BaseMessage { + type: "audio"; + data: string; // base64 encoded + format: string; +} + +interface ResponseCompleteMessage extends BaseMessage { + type: "response_complete"; + text: string; + toolCalls: Array<{ toolName: string; toolCallId: string; input: unknown }>; + toolResults: Array<{ toolName: string; toolCallId: string; output: unknown }>; +} + +type AgentMessage = + | TextDeltaMessage + | ToolCallMessage + | ToolResultMessage + | AudioMessage + | ResponseCompleteMessage; + const wss = new WebSocketServer({ port, host }); wss.on("listening", () => { - console.log(`[ws-server] listening on ${endpoint}`); + console.log(`[ws-server] 🚀 listening on ${endpoint}`); + console.log("[ws-server] Waiting for connections...\n"); }); -wss.on("connection", (socket) => { - console.log("[ws-server] client connected"); +wss.on("connection", (socket: WebSocket) => { + console.log("[ws-server] ✓ client connected"); + + let streamingText = ""; + let audioChunks: Buffer[] = []; // Send a sample transcript to test text pipeline end-to-end. setTimeout(() => { + console.log("[ws-server] -> Sending test transcript..."); socket.send( JSON.stringify({ type: "transcript", @@ -25,19 +76,94 @@ wss.on("connection", (socket) => { ); }, 500); - socket.on("message", (data) => { + socket.on("message", async (data) => { try { - const msg = JSON.parse(data.toString()) as { - type?: string; - text?: string; - }; - console.log("[ws-server] <-", msg); + const msg = JSON.parse(data.toString()) as AgentMessage; + + switch (msg.type) { + case "text_delta": + // Real-time streaming text from the agent + streamingText += msg.text; + process.stdout.write(msg.text); + break; + + case "tool_call": + console.log(`\n[ws-server] 🛠️ Tool call: ${msg.toolName}`); + console.log(` Input: ${JSON.stringify(msg.input)}`); + break; + + case "tool_result": + console.log(`[ws-server] 🛠️ Tool result: ${msg.toolName}`); + console.log(` Result: ${JSON.stringify(msg.result)}`); + break; + + case "audio": + // Handle audio response from TTS + const audioBuffer = Buffer.from(msg.data, "base64"); + audioChunks.push(audioBuffer); + console.log( + `[ws-server] 🔊 Received audio: ${audioBuffer.length} bytes (${msg.format})`, + ); + + // Optionally save audio to file for testing + // await writeFile(`output_${Date.now()}.${msg.format}`, audioBuffer); + break; + + case "response_complete": + console.log("\n[ws-server] ✅ Response complete"); + console.log(` Text length: ${msg.text.length}`); + console.log(` Tool calls: ${msg.toolCalls.length}`); + console.log(` Tool results: ${msg.toolResults.length}`); + + // Reset for next response + streamingText = ""; + audioChunks = []; + break; + + default: + console.log("[ws-server] <- Unknown message:", msg); + } } catch { - console.log("[ws-server] <- raw", data.toString()); + console.log("[ws-server] <- raw", data.toString().substring(0, 100)); } }); socket.on("close", () => { - console.log("[ws-server] client disconnected"); + console.log("[ws-server] ✗ client disconnected\n"); + }); + + socket.on("error", (error) => { + console.error("[ws-server] Error:", error.message); }); }); + +// Graceful shutdown +process.on("SIGINT", () => { + console.log("\n[ws-server] Shutting down..."); + wss.close(() => { + console.log("[ws-server] Server closed"); + process.exit(0); + }); +}); + +// Helper function to simulate sending audio to the agent +async function simulateAudioInput(socket: WebSocket, audioPath: string) { + if (!existsSync(audioPath)) { + console.log(`[ws-server] Audio file not found: ${audioPath}`); + return; + } + + const audioBuffer = await readFile(audioPath); + const base64Audio = audioBuffer.toString("base64"); + + console.log(`[ws-server] -> Sending audio: ${audioPath} (${audioBuffer.length} bytes)`); + socket.send( + JSON.stringify({ + type: "audio", + data: base64Audio, + }), + ); +} + +// Export for use as a module +export { wss, simulateAudioInput }; diff --git a/output.mp3 b/output.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f1fe13f8b81095c40dfda3e69056fedca81de428 GIT binary patch literal 49920 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[0]["stopWhen"]>; + stopWhen?: NonNullable[0]["stopWhen"]>; tools?: Record; endpoint?: string; + voice?: string; // Voice for TTS (e.g., 'alloy', 'echo', 'shimmer') + speechInstructions?: string; // Instructions for TTS voice style + outputFormat?: string; // Audio output format (e.g., 'mp3', 'opus', 'wav') } export class VoiceAgent extends EventEmitter { private socket?: WebSocket; private tools: Record = {}; private model: LanguageModel; + private transcriptionModel?: TranscriptionModel; + private speechModel?: SpeechModel; private instructions: string; - private stopWhen: NonNullable[0]["stopWhen"]>; + private stopWhen: NonNullable[0]["stopWhen"]>; private endpoint?: string; private isConnected = false; + private conversationHistory: ModelMessage[] = []; + private voice: string; + private speechInstructions?: string; + private outputFormat: string; + private isProcessing = false; constructor(options: VoiceAgentOptions) { super(); this.model = options.model; + this.transcriptionModel = options.transcriptionModel; + this.speechModel = options.speechModel; this.instructions = options.instructions || "You are a helpful voice assistant."; this.stopWhen = options.stopWhen || stepCountIs(5); this.endpoint = options.endpoint; + this.voice = options.voice || "alloy"; + this.speechInstructions = options.speechInstructions; + this.outputFormat = options.outputFormat || "mp3"; if (options.tools) { this.tools = { ...options.tools }; } @@ -38,16 +65,17 @@ export class VoiceAgent extends EventEmitter { try { const message = JSON.parse(data.toString()); - // Example: Handle transcribed text from the client/STT + // Handle transcribed text from the client/STT if (message.type === "transcript") { await this.processUserInput(message.text); } - // Handle audio data + // Handle raw audio data that needs transcription if (message.type === "audio") { - this.emit("audio", message.data); + await this.processAudioInput(message.data); } } catch (err) { console.error("Failed to process message:", err); + this.emit("error", err); } }); @@ -56,12 +84,81 @@ export class VoiceAgent extends EventEmitter { this.isConnected = false; this.emit("disconnected"); }); + + this.socket.on("error", (error) => { + console.error("WebSocket error:", error); + this.emit("error", error); + }); } public registerTools(tools: Record) { this.tools = { ...this.tools, ...tools }; } + /** + * Transcribe audio data to text using the configured transcription model + */ + public async transcribeAudio(audioData: Buffer | Uint8Array): Promise { + if (!this.transcriptionModel) { + throw new Error("Transcription model not configured"); + } + + const result = await transcribe({ + model: this.transcriptionModel, + audio: audioData, + }); + + this.emit("transcription", { + text: result.text, + language: result.language, + }); + + return result.text; + } + + /** + * Generate speech from text using the configured speech model + */ + public async generateSpeechFromText(text: string): Promise { + if (!this.speechModel) { + throw new Error("Speech model not configured"); + } + + const result = await generateSpeech({ + model: this.speechModel, + text, + voice: this.voice, + instructions: this.speechInstructions, + outputFormat: this.outputFormat, + }); + + return result.audio.uint8Array; + } + + /** + * Process incoming audio data: transcribe and generate response + */ + private async processAudioInput(base64Audio: string): Promise { + if (!this.transcriptionModel) { + this.emit("error", new Error("Transcription model not configured for audio input")); + return; + } + + try { + const audioBuffer = Buffer.from(base64Audio, "base64"); + this.emit("audio_received", { size: audioBuffer.length }); + + const transcribedText = await this.transcribeAudio(audioBuffer); + + if (transcribedText.trim()) { + await this.processUserInput(transcribedText); + } + } catch (error) { + console.error("Failed to process audio input:", error); + this.emit("error", error); + } + } + public async connect(url?: string): Promise { return new Promise((resolve, reject) => { try { @@ -85,52 +182,426 @@ export class VoiceAgent extends EventEmitter { }); } - public async sendText(text: string): Promise { - await this.processUserInput(text); + /** + * Send text input for processing (bypasses transcription) + */ + public async sendText(text: string): Promise { + return this.processUserInput(text); } - public sendAudio(audioData: string): void { - if (this.socket && this.isConnected) { - this.socket.send(JSON.stringify({ + /** + * Send audio data to be transcribed and processed + * @param audioData Base64 encoded audio data + */ + public async sendAudio(audioData: string): Promise { + await this.processAudioInput(audioData); + } + + /** + * Send raw audio buffer to be transcribed and processed + */ + public async sendAudioBuffer(audioBuffer: Buffer | Uint8Array): Promise { + const base64Audio = Buffer.from(audioBuffer).toString("base64"); + await this.processAudioInput(base64Audio); + } + + /** + * Process user input with streaming text generation + * Handles the full pipeline: text -> LLM (streaming) -> TTS -> WebSocket + */ + private async processUserInput(text: string): Promise { + if (this.isProcessing) { + this.emit("warning", "Already processing a request, queuing..."); + } + this.isProcessing = true; + + try { + // Emit text event for incoming user input + this.emit("text", { role: "user", text }); + + // Add user message to conversation history + this.conversationHistory.push({ role: "user", content: text }); + + // Use streamText for streaming responses with tool support + const result = streamText({ + model: this.model, + system: this.instructions, + messages: this.conversationHistory, + tools: this.tools, + stopWhen: this.stopWhen, + onChunk: ({ chunk }) => { + // Emit streaming chunks for real-time updates + // Note: onChunk only receives a subset of stream events + switch (chunk.type) { + case "text-delta": + this.emit("chunk:text_delta", { id: chunk.id, text: chunk.text }); + break; + + case "reasoning-delta": + this.emit("chunk:reasoning_delta", { id: chunk.id, text: chunk.text }); + break; + + case "tool-call": + this.emit("chunk:tool_call", { + toolName: chunk.toolName, + toolCallId: chunk.toolCallId, + input: chunk.input, + }); + break; + + case "tool-result": + this.emit("chunk:tool_result", { + toolName: chunk.toolName, + toolCallId: chunk.toolCallId, + result: chunk.output, + }); + break; + + case "tool-input-start": + this.emit("chunk:tool_input_start", { + id: chunk.id, + toolName: chunk.toolName, + }); + break; + + case "tool-input-delta": + this.emit("chunk:tool_input_delta", { + id: chunk.id, + delta: chunk.delta, + }); + break; + + case "source": + this.emit("chunk:source", chunk); + break; + } + }, + onFinish: async (event) => { + // Process steps for tool results + for (const step of event.steps) { + for (const toolResult of step.toolResults) { + this.emit("tool_result", { + name: toolResult.toolName, + toolCallId: toolResult.toolCallId, + result: toolResult.output, + }); + } + } + }, + onError: ({ error }) => { + console.error("Stream error:", error); + this.emit("error", error); + }, + }); + + // Collect the full response text and reasoning + let fullText = ""; + let fullReasoning = ""; + const allToolCalls: Array<{ + toolName: string; + toolCallId: string; + input: unknown; + }> = []; + const allToolResults: Array<{ + toolName: string; + toolCallId: string; + output: unknown; + }> = []; + const allSources: Array = []; + const allFiles: Array = []; + + // Process the full stream + for await (const part of result.fullStream) { + switch (part.type) { + // Stream lifecycle + case "start": + this.sendWebSocketMessage({ type: "stream_start" }); + break; + + case "finish": + this.emit("text", { role: "assistant", text: fullText }); + this.sendWebSocketMessage({ + type: "stream_finish", + finishReason: part.finishReason, + usage: part.totalUsage, + }); + break; + + case "error": + this.emit("error", part.error); + this.sendWebSocketMessage({ + type: "stream_error", + error: String(part.error), + }); + break; + + case "abort": + this.emit("abort", { reason: part.reason }); + this.sendWebSocketMessage({ + type: "stream_abort", + reason: part.reason, + }); + break; + + // Step lifecycle + case "start-step": + this.sendWebSocketMessage({ + type: "step_start", + warnings: part.warnings, + }); + break; + + case "finish-step": + this.sendWebSocketMessage({ + type: "step_finish", + finishReason: part.finishReason, + usage: part.usage, + }); + break; + + // Text streaming + case "text-start": + this.sendWebSocketMessage({ type: "text_start", id: part.id }); + break; + + case "text-delta": + fullText += part.text; + this.sendWebSocketMessage({ + type: "text_delta", + id: part.id, + text: part.text, + }); + break; + + case "text-end": + this.sendWebSocketMessage({ type: "text_end", id: part.id }); + break; + + // Reasoning streaming (for models that support it) + case "reasoning-start": + this.sendWebSocketMessage({ type: "reasoning_start", id: part.id }); + break; + + case "reasoning-delta": + fullReasoning += part.text; + this.sendWebSocketMessage({ + type: "reasoning_delta", + id: part.id, + text: part.text, + }); + break; + + case "reasoning-end": + this.sendWebSocketMessage({ type: "reasoning_end", id: part.id }); + break; + + // Tool input streaming + case "tool-input-start": + this.sendWebSocketMessage({ + type: "tool_input_start", + id: part.id, + toolName: part.toolName, + }); + break; + + case "tool-input-delta": + this.sendWebSocketMessage({ + type: "tool_input_delta", + id: part.id, + delta: part.delta, + }); + break; + + case "tool-input-end": + this.sendWebSocketMessage({ type: "tool_input_end", id: part.id }); + break; + + // Tool execution + case "tool-call": + allToolCalls.push({ + toolName: part.toolName, + toolCallId: part.toolCallId, + input: part.input, + }); + this.sendWebSocketMessage({ + type: "tool_call", + toolName: part.toolName, + toolCallId: part.toolCallId, + input: part.input, + }); + break; + + case "tool-result": + allToolResults.push({ + toolName: part.toolName, + toolCallId: part.toolCallId, + output: part.output, + }); + this.sendWebSocketMessage({ + type: "tool_result", + toolName: part.toolName, + toolCallId: part.toolCallId, + result: part.output, + }); + break; + + case "tool-error": + this.sendWebSocketMessage({ + type: "tool_error", + toolName: part.toolName, + toolCallId: part.toolCallId, + error: String(part.error), + }); + break; + + // Sources and files + case "source": + allSources.push(part); + this.sendWebSocketMessage({ + type: "source", + source: part, + }); + break; + + case "file": + allFiles.push(part.file); + this.sendWebSocketMessage({ + type: "file", + file: part.file, + }); + break; + } + } + + // Add assistant response to conversation history + if (fullText) { + this.conversationHistory.push({ role: "assistant", content: fullText }); + } + + // Generate speech from the response if speech model is configured + if (this.speechModel && fullText) { + await this.generateAndSendSpeech(fullText); + } + + // Send the complete response + this.sendWebSocketMessage({ + type: "response_complete", + text: fullText, + reasoning: fullReasoning || undefined, + toolCalls: allToolCalls, + toolResults: allToolResults, + sources: allSources.length > 0 ? allSources : undefined, + files: allFiles.length > 0 ? allFiles : undefined, + }); + + return fullText; + } finally { + this.isProcessing = false; + } + } + + /** + * Generate speech and send audio via WebSocket + */ + private async generateAndSendSpeech(text: string): Promise { + if (!this.speechModel) return; + + try { + this.emit("speech_start", { text }); + + const audioData = await this.generateSpeechFromText(text); + const base64Audio = Buffer.from(audioData).toString("base64"); + + // Send audio via WebSocket + this.sendWebSocketMessage({ type: "audio", - data: audioData - })); + data: base64Audio, + format: this.outputFormat, + }); + + // Also emit for local handling + this.emit("audio", { + data: base64Audio, + format: this.outputFormat, + uint8Array: audioData, + }); + + this.emit("speech_complete", { text }); + } catch (error) { + console.error("Failed to generate speech:", error); + this.emit("error", error); } } - private async processUserInput(text: string) { - // Emit text event for incoming user input - this.emit("text", { role: "user", text }); - - const result = await generateText({ - model: this.model, - system: this.instructions, - prompt: text, - tools: this.tools, - stopWhen: this.stopWhen, - }); - - for (const toolCall of result.toolCalls ?? []) { - this.emit("tool_start", { name: toolCall.toolName }); - } - - // Emit text event for assistant response - this.emit("text", { role: "assistant", text: result.text }); - - // Send the response back (either text to be TTSed or tool results) + /** + * Send a message via WebSocket if connected + */ + private sendWebSocketMessage(message: Record): void { if (this.socket && this.isConnected) { - this.socket.send( - JSON.stringify({ - type: "response", - text: result.text, - toolCalls: result.toolCalls, - toolResults: result.toolResults, - }), - ); + this.socket.send(JSON.stringify(message)); } } + /** + * Start listening for voice input + */ startListening() { console.log("Starting voice agent..."); + this.emit("listening"); + } + + /** + * Stop listening for voice input + */ + stopListening() { + console.log("Stopping voice agent..."); + this.emit("stopped"); + } + + /** + * Clear conversation history + */ + clearHistory() { + this.conversationHistory = []; + this.emit("history_cleared"); + } + + /** + * Get current conversation history + */ + getHistory(): ModelMessage[] { + return [...this.conversationHistory]; + } + + /** + * Set conversation history (useful for restoring sessions) + */ + setHistory(history: ModelMessage[]) { + this.conversationHistory = [...history]; + } + + /** + * Disconnect from WebSocket + */ + disconnect() { + if (this.socket) { + this.socket.close(); + this.socket = undefined; + this.isConnected = false; + } + } + + /** + * Check if agent is connected to WebSocket + */ + get connected(): boolean { + return this.isConnected; + } + + /** + * Check if agent is currently processing a request + */ + get processing(): boolean { + return this.isProcessing; } } diff --git a/src/index.ts b/src/index.ts index e4c28c3..03a07b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export { VoiceAgent } from "./VoiceAgent"; +export { VoiceAgent, type VoiceAgentOptions } from "./VoiceAgent";