feat: Matrix AI voice agent (LiveKit + LiteLLM)
Bot @ai:agiliton.eu accepts room invites, dispatches LiveKit agent. Agent joins call with STT (Groq Whisper) → LLM (Sonnet) → TTS (ElevenLabs) pipeline, all routed through LiteLLM. CF-1147 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
58
agent.py
Normal file
58
agent.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from livekit.agents import Agent, AgentSession, AgentServer, JobContext, JobProcess, cli
|
||||
from livekit.plugins import openai as lk_openai, elevenlabs, silero
|
||||
|
||||
logger = logging.getLogger("matrix-ai-agent")
|
||||
|
||||
LITELLM_URL = os.environ["LITELLM_BASE_URL"]
|
||||
LITELLM_KEY = os.environ.get("LITELLM_API_KEY", "not-needed")
|
||||
|
||||
SYSTEM_PROMPT = """You are a helpful voice assistant in a Matrix call.
|
||||
Rules:
|
||||
- Keep answers SHORT — 1-3 sentences max
|
||||
- Be direct, no filler words
|
||||
- If the user wants more detail, they will ask
|
||||
- Speak naturally as in a conversation"""
|
||||
|
||||
server = AgentServer()
|
||||
|
||||
|
||||
def prewarm(proc: JobProcess):
|
||||
proc.userdata["vad"] = silero.VAD.load()
|
||||
|
||||
|
||||
server.setup_fnc = prewarm
|
||||
|
||||
|
||||
@server.rtc_session(agent_name="matrix-ai")
|
||||
async def entrypoint(ctx: JobContext):
|
||||
model = os.environ.get("LITELLM_MODEL", "claude-sonnet")
|
||||
voice_id = os.environ.get("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM")
|
||||
|
||||
session = AgentSession(
|
||||
stt=lk_openai.STT(
|
||||
base_url=LITELLM_URL,
|
||||
api_key=LITELLM_KEY,
|
||||
model="whisper",
|
||||
),
|
||||
llm=lk_openai.LLM(
|
||||
base_url=LITELLM_URL,
|
||||
api_key=LITELLM_KEY,
|
||||
model=model,
|
||||
),
|
||||
tts=elevenlabs.TTS(
|
||||
voice=voice_id,
|
||||
model="eleven_multilingual_v2",
|
||||
),
|
||||
vad=ctx.proc.userdata["vad"],
|
||||
)
|
||||
|
||||
agent = Agent(instructions=SYSTEM_PROMPT)
|
||||
await session.start(agent=agent, room=ctx.room)
|
||||
await session.generate_reply(instructions="Greet the user briefly.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli.run_app(server)
|
||||
73
bot.py
Normal file
73
bot.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from nio import AsyncClient, LoginResponse, InviteMemberEvent
|
||||
from livekit import api
|
||||
|
||||
logger = logging.getLogger("matrix-ai-bot")
|
||||
|
||||
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
|
||||
BOT_USER = os.environ["MATRIX_BOT_USER"]
|
||||
BOT_PASS = os.environ["MATRIX_BOT_PASSWORD"]
|
||||
LK_URL = os.environ["LIVEKIT_URL"]
|
||||
LK_KEY = os.environ["LIVEKIT_API_KEY"]
|
||||
LK_SECRET = os.environ["LIVEKIT_API_SECRET"]
|
||||
AGENT_NAME = os.environ.get("AGENT_NAME", "matrix-ai")
|
||||
|
||||
|
||||
class Bot:
|
||||
def __init__(self):
|
||||
self.client = AsyncClient(HOMESERVER, BOT_USER)
|
||||
self.lkapi = None
|
||||
self.dispatched_rooms = set()
|
||||
|
||||
async def start(self):
|
||||
resp = await self.client.login(BOT_PASS, device_name="ai-voice-bot")
|
||||
if not isinstance(resp, LoginResponse):
|
||||
logger.error("Login failed: %s", resp)
|
||||
return
|
||||
logger.info("Logged in as %s", resp.user_id)
|
||||
|
||||
self.lkapi = api.LiveKitAPI(LK_URL, LK_KEY, LK_SECRET)
|
||||
self.client.add_event_callback(self.on_invite, InviteMemberEvent)
|
||||
|
||||
await self.client.sync_forever(timeout=30000)
|
||||
|
||||
async def on_invite(self, room, event: InviteMemberEvent):
|
||||
if event.state_key != BOT_USER:
|
||||
return
|
||||
logger.info("Invited to %s", room.room_id)
|
||||
await self.client.join(room.room_id)
|
||||
|
||||
# LiveKit room name = Matrix room ID (Element Call convention)
|
||||
lk_room_name = room.room_id
|
||||
try:
|
||||
await self.lkapi.agent_dispatch.create_dispatch(
|
||||
api.CreateAgentDispatchRequest(
|
||||
agent_name=AGENT_NAME,
|
||||
room=lk_room_name,
|
||||
)
|
||||
)
|
||||
self.dispatched_rooms.add(room.room_id)
|
||||
logger.info("Agent dispatched to %s", lk_room_name)
|
||||
except Exception:
|
||||
logger.exception("Dispatch failed for %s", room.room_id)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.close()
|
||||
if self.lkapi:
|
||||
await self.lkapi.aclose()
|
||||
|
||||
|
||||
async def main():
|
||||
bot = Bot()
|
||||
try:
|
||||
await bot.start()
|
||||
finally:
|
||||
await bot.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
agent:
|
||||
build: .
|
||||
command: python agent.py start
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
bot:
|
||||
build: .
|
||||
command: python bot.py
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
livekit-agents>=1.4,<2.0
|
||||
livekit-plugins-openai>=1.4,<2.0
|
||||
livekit-plugins-elevenlabs>=1.0,<2.0
|
||||
livekit-plugins-silero>=0.25,<1.0
|
||||
livekit>=0.18,<1.0
|
||||
livekit-api>=0.8,<1.0
|
||||
matrix-nio>=0.25,<1.0
|
||||
Reference in New Issue
Block a user