-
Notifications
You must be signed in to change notification settings - Fork 8
ElevenLabs voice chatbot example with Galileo Protect #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # ElevenLabs Configuration | ||
| ELEVENLABS_API_KEY=your-elevenlabs-api-key | ||
| ELEVENLABS_AGENT_ID=your-agent-id | ||
|
|
||
| # Galileo Configuration (for local instance) | ||
| GALILEO_API_KEY=your-galileo-api-key | ||
| GALILEO_CONSOLE_URL= | ||
| GALILEO_PROJECT_NAME=elevenlabs-voice-poc | ||
| GALILEO_LOG_STREAM=voice-conversations | ||
|
|
||
| GALILEO_PROTECT_ENABLED=true | ||
| GALILEO_PROTECT_STAGE_ID=your-stage-id |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 3.12.12 |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you link from the top level README please |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||
| # ElevenLabs Voice Chatbot with Galileo Protect | ||||||
|
|
||||||
| A terminal-based voice chatbot that lets you have real-time voice conversations with an ElevenLabs AI agent, with Galileo Protect guardrails for content moderation. | ||||||
|
|
||||||
| ## What This Example Shows | ||||||
|
|
||||||
| - Interactive voice chat in your terminal (speak via microphone, hear responses via speakers) | ||||||
| - Real-time guardrails using Galileo Protect to moderate user input and agent output | ||||||
| - Conversation logging and tracing with Galileo Observe | ||||||
| - Session metrics tracking (latency, turn counts, character counts) | ||||||
|
|
||||||
| ## Quick Start | ||||||
|
|
||||||
| 1. Install system dependencies: | ||||||
| ```bash | ||||||
| brew install portaudio # Required for audio support on macOS | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about Windows or Linux? Most of our enterprise customers are on Windows, so do they need to install anything. This should be tagged as macOS only |
||||||
| ``` | ||||||
|
|
||||||
| 2. Create and activate a virtual environment: | ||||||
| ```bash | ||||||
| python -m venv venv | ||||||
| source venv/bin/activate # On Windows: venv\Scripts\activate | ||||||
| ``` | ||||||
|
|
||||||
| 3. Install Python dependencies: | ||||||
| ```bash | ||||||
| pip install -r requirements.txt | ||||||
| ``` | ||||||
|
|
||||||
| 4. Set up environment variables: | ||||||
| ```bash | ||||||
| cp .env.example .env | ||||||
| # Edit .env with your API keys | ||||||
| ``` | ||||||
|
|
||||||
| 5. Run the voice chat: | ||||||
| ```bash | ||||||
| python conversation.py | ||||||
| ``` | ||||||
|
|
||||||
| For best results use a headset with micrphone. The app will start listening through your microphone. Speak to chat with the AI agent and hear responses through your speakers. Press `Ctrl+C` to end the session. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| ## Configuration | ||||||
|
|
||||||
| Edit `.env` with your credentials. Note for `ELEVENLABS_*` variables you can signup with a free tier of elevenlabs and use their Agents Platform to create a voice agent to obtain the required API key and Agent Id: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Link to make it easier to sign up? |
||||||
|
|
||||||
| | Variable | Description | | ||||||
| |----------|-------------| | ||||||
| | `ELEVENLABS_API_KEY` | Your ElevenLabs API key | | ||||||
| | `ELEVENLABS_AGENT_ID` | Your ElevenLabs Agent ID | | ||||||
| | `GALILEO_API_KEY` | Your Galileo API key | | ||||||
| | `GALILEO_CONSOLE_URL` | Galileo console URL (optional) | | ||||||
| | `GALILEO_PROJECT_NAME` | Project name for logging | | ||||||
| | `GALILEO_PROTECT_STAGE_ID` | Protect stage ID for guardrails (see below) | | ||||||
|
|
||||||
| ### Creating a Galileo Protect Stage | ||||||
|
|
||||||
| To enable guardrails, you need to create a Galileo Protect stage. Run the included script: | ||||||
|
|
||||||
| ```bash | ||||||
| python scripts/create_stage.py | ||||||
| ``` | ||||||
|
|
||||||
| This will create a protect stage with an input toxicity rule and output the stage ID. Copy this ID to your `.env` file as `GALILEO_PROTECT_STAGE_ID`. | ||||||
|
|
||||||
| ## Requirements | ||||||
|
|
||||||
| - Python 3.9+ | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We are updating Galileo to 3.10 |
||||||
| - Microphone and headphones (to avoid audio feedback) | ||||||
|
|
||||||
| ## Learn More | ||||||
|
|
||||||
| - [Galileo Documentation](https://docs.galileo.ai/) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| - [Runtime Protection](https://v2docs.galileo.ai/concepts/protect/overview) | ||||||
| - [ElevenLabs Conversational AI](https://elevenlabs.io/docs/conversational-ai) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Configuration management for the ElevenLabs Voice POC.""" | ||
|
|
||
| from pydantic_settings import BaseSettings | ||
|
|
||
|
|
||
| class Settings(BaseSettings): | ||
| """Application settings loaded from environment variables.""" | ||
|
|
||
| # ElevenLabs Configuration | ||
| elevenlabs_api_key: str | ||
| elevenlabs_agent_id: str | ||
|
|
||
| # WebSocket monitoring endpoint | ||
| elevenlabs_ws_url: str = "wss://api.elevenlabs.io/v1/convai/conversation" | ||
|
|
||
| # Galileo Configuration | ||
| galileo_api_key: str = "" | ||
| galileo_console_url: str = "http://localhost:3000" # Local Galileo instance | ||
| galileo_project_name: str = "elevenlabs-voice-poc" | ||
| galileo_log_stream: str = "voice-conversations" | ||
|
Comment on lines
+17
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should load from the env variables so why hard code here? |
||
|
|
||
| galileo_protect_enabled: bool = True | ||
| galileo_protect_stage_id: str = "" | ||
|
|
||
| class Config: | ||
| env_file = ".env" | ||
| env_file_encoding = "utf-8" | ||
|
|
||
|
|
||
| def get_settings() -> Settings: | ||
| """Get application settings.""" | ||
| return Settings() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| """ElevenLabs Conversation using official SDK - supports voice input/output.""" | ||
|
|
||
| import os | ||
| import uuid | ||
| from pathlib import Path | ||
|
|
||
| from dotenv import load_dotenv | ||
|
|
||
| # Load .env from project root | ||
| env_path = Path(__file__).parent / ".env" | ||
| load_dotenv(env_path) | ||
|
|
||
| from elevenlabs.client import ElevenLabs | ||
| from elevenlabs.conversational_ai.conversation import Conversation | ||
| from elevenlabs.conversational_ai.default_audio_interface import DefaultAudioInterface | ||
|
|
||
| from config import get_settings | ||
| from galileo_handler import get_galileo_handler | ||
|
|
||
|
|
||
| # Initialize Galileo handler | ||
| _galileo = None | ||
| _conversation = None | ||
|
|
||
|
|
||
| def _get_galileo(): | ||
| """Lazy initialization of Galileo handler.""" | ||
| global _galileo | ||
| if _galileo is None: | ||
| _galileo = get_galileo_handler() | ||
| return _galileo | ||
|
|
||
|
|
||
| def on_agent_response(response: str) -> None: | ||
| """Called when agent responds - logs to Galileo.""" | ||
| print(f"\n[AGENT] {response}") | ||
|
|
||
| galileo = _get_galileo() | ||
| result = galileo.log_agent_turn(response) | ||
| if result.get("blocked"): | ||
| print(f"[FAKE_GUARDRAIL] Agent response flagged: {result.get('reason')}") | ||
|
|
||
|
|
||
| def on_user_transcript(transcript: str) -> None: | ||
| """Called when user speech is transcribed - logs to Galileo.""" | ||
| global _conversation | ||
| print(f"\n[USER] {transcript}") | ||
|
|
||
| galileo = _get_galileo() | ||
| result = galileo.log_user_turn(transcript) | ||
| if result.get("blocked"): | ||
| print(f"\n[GALILEO PROTECT] *** INPUT BLOCKED *** {result.get('reason')}") | ||
|
|
||
| # Get the override message from Galileo Protect | ||
| override_message = result.get("override_message") | ||
|
|
||
| if override_message: | ||
| # End current conversation session first | ||
| if _conversation: | ||
| print(f"[GALILEO PROTECT] Ending conversation session...") | ||
| _conversation.end_session() | ||
|
|
||
| # Pause briefly to let audio system settle | ||
| import time | ||
|
|
||
| time.sleep(1.5) | ||
|
|
||
| # Print the override message | ||
| print(f"\n[AGENT] {override_message}") | ||
|
|
||
| # Generate and play the override message audio | ||
| try: | ||
| import tempfile | ||
| import subprocess | ||
| import platform | ||
|
|
||
| settings = get_settings() | ||
|
|
||
| # Get the ElevenLabs client | ||
| client = ElevenLabs(api_key=settings.elevenlabs_api_key) | ||
|
|
||
| # Generate audio using the text_to_speech module | ||
| print(f"[GALILEO PROTECT] Generating audio for override message...") | ||
| audio_generator = client.text_to_speech.convert( | ||
| text=override_message, | ||
| voice_id="cjVigY5qzO86Huf0OWal", # Eric voice ID | ||
| model_id="eleven_turbo_v2", | ||
| output_format="mp3_22050_32", # Lower quality to match conversational audio | ||
| ) | ||
|
|
||
| # Save audio to a temporary file | ||
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f: | ||
| for chunk in audio_generator: | ||
| f.write(chunk) | ||
| temp_file = f.name | ||
|
|
||
| # Play using system's default audio player | ||
| print(f"[GALILEO PROTECT] Playing override message...") | ||
| system = platform.system() | ||
| if system == "Darwin": # macOS | ||
| subprocess.run(["afplay", temp_file], check=True) | ||
| elif system == "Windows": | ||
| os.startfile(temp_file) | ||
| else: # Linux | ||
| subprocess.run(["xdg-open", temp_file], check=True) | ||
|
|
||
| # Clean up | ||
| os.unlink(temp_file) | ||
|
|
||
| print(f"[GALILEO PROTECT] Override message delivered via audio") | ||
|
|
||
| except Exception as e: | ||
| print(f"[GALILEO PROTECT] Failed to generate or play audio: {e}") | ||
| print(f"[GALILEO PROTECT] Message was displayed as text above") | ||
| import traceback | ||
|
|
||
| traceback.print_exc() | ||
|
|
||
| # Log the override message to Galileo as an agent turn | ||
| # This ensures it shows up in the trace | ||
| galileo.log_agent_turn(override_message) | ||
|
|
||
| # End the conversation | ||
| print("[GALILEO PROTECT] Ending conversation session...") | ||
| galileo.end_conversation() | ||
| print("[INFO] Conversation ended - logs sent to Galileo") | ||
| raise SystemExit("Call terminated by guardrail") | ||
| else: | ||
| # No override message, just end immediately | ||
| if _conversation: | ||
| print("[GALILEO PROTECT] Ending session due to guardrail violation...") | ||
| _conversation.end_session() | ||
| galileo.end_conversation() | ||
| raise SystemExit("Call terminated by guardrail") | ||
|
|
||
|
|
||
| def on_mode_change(mode: dict) -> None: | ||
| """Called when conversation mode changes (speaking/listening).""" | ||
| print(f"[MODE] {mode}") | ||
|
|
||
|
|
||
| def run_voice_conversation(use_headphones: bool = True): | ||
| """Run a voice conversation with microphone input and speaker output. | ||
|
|
||
| Args: | ||
| use_headphones: If True, plays audio through speakers. | ||
| Set to False if not using headphones to avoid feedback loop. | ||
| """ | ||
| settings = get_settings() | ||
|
|
||
| # Initialize ElevenLabs client | ||
| client = ElevenLabs(api_key=settings.elevenlabs_api_key) | ||
|
|
||
| # Initialize Galileo and start conversation trace | ||
| galileo = _get_galileo() | ||
| session_id = str(uuid.uuid4()) | ||
| galileo.start_conversation(session_id) | ||
|
|
||
| print("\n" + "=" * 60) | ||
| print("ElevenLabs Voice POC - Voice Mode + Galileo") | ||
| print(f"Session ID: {session_id}") | ||
| if use_headphones: | ||
| print("*** USE HEADPHONES to avoid audio feedback loop ***") | ||
| else: | ||
| print("*** Silent mode - no audio playback ***") | ||
| print("Speak into your microphone to talk to the agent") | ||
| print("Press Ctrl+C to end the session") | ||
| print("=" * 60 + "\n") | ||
|
|
||
| global _conversation | ||
|
|
||
| # Create conversation with audio interface | ||
| conversation = Conversation( | ||
| client=client, | ||
| agent_id=settings.elevenlabs_agent_id, | ||
| requires_auth=True, # We're using API key auth | ||
| # Required: audio interface for mic/speaker | ||
| audio_interface=DefaultAudioInterface(), | ||
| # Callbacks for monitoring - connected to Galileo | ||
| callback_agent_response=on_agent_response, | ||
| callback_user_transcript=on_user_transcript, | ||
| # callback_mode_change=on_mode_change, # Optional | ||
| ) | ||
|
|
||
| _conversation = conversation | ||
|
|
||
| # Start the conversation (blocking) | ||
| print("[INFO] Starting conversation... Speak now!") | ||
| conversation.start_session() | ||
|
|
||
| # Wait for conversation to end | ||
| try: | ||
| conversation.wait_for_session_end() | ||
| except KeyboardInterrupt: | ||
| print("\n[INFO] Ending conversation...") | ||
| conversation.end_session() | ||
|
|
||
| # End Galileo trace and flush logs | ||
| galileo.end_conversation() | ||
| print("[INFO] Conversation ended - logs sent to Galileo") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run_voice_conversation() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this file as it forces a Python version