Skip to content

Commit

Permalink
feat: add rate limiter, create client
Browse files Browse the repository at this point in the history
  • Loading branch information
OrestSonich committed Nov 6, 2024
1 parent d0b97f9 commit 15be08a
Show file tree
Hide file tree
Showing 25 changed files with 3,409 additions and 189 deletions.
7 changes: 5 additions & 2 deletions .env.template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ export DEEPGRAM_API_KEY=<your-deepgram-api-key>
export GROQ_API_KEY=<your-groq-api-key>

# Rate Limiter
export RATE_LIMIT_MAX_REQUESTS=5
export RATE_LIMIT_TIME_WINDOW=60
export RATE_LIMIT_MAX_REQUESTS_RTC=5
export RATE_LIMIT_TIME_WINDOW_RTC=60

export RATE_LIMIT_MAX_REQUESTS_API=5
export RATE_LIMIT_TIME_WINDOW_API=60
164 changes: 0 additions & 164 deletions index.html

This file was deleted.

7 changes: 7 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

# Run Flask API server
python -m src.main &

# Run LiveKit agent
python -c "from src.voice_agent import entrypoint, prewarm; from livekit.agents import cli, WorkerOptions; cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm))" dev
13 changes: 4 additions & 9 deletions src/__main__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
from flask import Flask
from flask_cors import CORS
from src.api.routes import api_bp
from src.voice_agent import entrypoint, prewarm
from livekit.agents import cli, WorkerOptions
import threading
from src.config import API_PORT, API_ORIGINS

app = Flask(__name__)

app.register_blueprint(api_bp, url_prefix='/api')
CORS(app, origins=API_ORIGINS)

def start_livekit_agent():
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm))

if __name__ == "__main__":
agent_thread = threading.Thread(target=start_livekit_agent)
agent_thread.start()

app.run(host="0.0.0.0", port=5000)
app.run(host="0.0.0.0", port=API_PORT)
11 changes: 10 additions & 1 deletion src/api/routes.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
from flask import Blueprint, jsonify
from flask import Blueprint, jsonify, request, abort
from src.config import RATE_LIMIT_MAX_REQUESTS_API, RATE_LIMIT_TIME_WINDOW_API
from src.token_service import create_access_token
import random
from src.rate_limiter import RateLimiter

api_bp = Blueprint('api', __name__)

# Initialize the rate limiter for IPs with desired limits
rate_limiter = RateLimiter(max_requests=RATE_LIMIT_MAX_REQUESTS_API, time_window=RATE_LIMIT_TIME_WINDOW_API) # 1 request per minute

@api_bp.route("/getToken", methods=["GET"])
def get_token():
ip_address = request.remote_addr
if not rate_limiter.is_allowed(ip_address):
abort(429, description="Too many requests from this IP. Please try again later.")

participant_identity = f"user_{random.randint(1, 10000)}"
room_name = "voice_assistant_room"
participant_token = create_access_token(participant_identity, room_name)
Expand Down
8 changes: 6 additions & 2 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
CARTESIA_API_KEY = os.getenv("CARTESIA_API_KEY")
CARTESIA_API_URL = os.getenv("CARTESIA_API_URL", "https://api.cartesia.ai/voices")

RATE_LIMIT_MAX_REQUESTS = int(os.getenv("RATE_LIMIT_MAX_REQUESTS", 5))
RATE_LIMIT_TIME_WINDOW = int(os.getenv("RATE_LIMIT_TIME_WINDOW", 60))
RATE_LIMIT_MAX_REQUESTS_RTC = int(os.getenv("RATE_LIMIT_MAX_REQUESTS_RTC", 5))
RATE_LIMIT_TIME_WINDOW_RTC = int(os.getenv("RATE_LIMIT_TIME_WINDOW_RTC", 60))

RATE_LIMIT_MAX_REQUESTS_API = int(os.getenv("RATE_LIMIT_MAX_REQUESTS_API", 5))
RATE_LIMIT_TIME_WINDOW_API = int(os.getenv("RATE_LIMIT_TIME_WINDOW_API", 60))

LIVEKIT_API_KEY = os.getenv("LIVEKIT_API_KEY")
LIVEKIT_API_SECRET = os.getenv("LIVEKIT_API_SECRET")
LIVEKIT_URL = os.getenv("LIVEKIT_URL")

API_PORT = os.getenv("API_PORT", 5000)
API_ORIGINS = os.getenv("API_ORIGINS")
31 changes: 20 additions & 11 deletions src/voice_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from livekit.agents.log import logger
from livekit.plugins import silero, deepgram, openai, cartesia
from src.rate_limiter import RateLimiter
from src.config import CARTESIA_API_KEY, CARTESIA_API_URL
from typing import List, Dict, Any
from src.config import CARTESIA_API_KEY, CARTESIA_API_URL, RATE_LIMIT_MAX_REQUESTS_RTC, RATE_LIMIT_TIME_WINDOW_RTC
from typing import List, Any

rate_limiter = RateLimiter(max_requests=5, time_window=60)
rate_limiter = RateLimiter(RATE_LIMIT_MAX_REQUESTS_RTC, RATE_LIMIT_TIME_WINDOW_RTC)

def prewarm(proc: JobProcess):
if not CARTESIA_API_KEY:
Expand Down Expand Up @@ -42,7 +42,7 @@ def update_tts_voice(tts, voice_data: dict):
async def entrypoint(ctx: JobContext):
try:
initial_ctx = ChatContext(messages=[
ChatMessage(role="system", content="You are an AI voice agent tasked with generating a prompt for a specific role in a conversation.")
ChatMessage(role="system", content="You are a professional voice assistant designed to help with daily tasks efficiently and accurately. Your main purpose is to respond in a friendly but concise manner, providing relevant information, reminders, and updates as needed. ")
])

cartesia_voices: List[dict[str, Any]] = ctx.proc.userdata.get("cartesia_voices")
Expand All @@ -69,17 +69,14 @@ async def entrypoint(ctx: JobContext):
@ctx.room.on("participant_attributes_changed")
def on_participant_attributes_changed(changed_attributes: dict[str, str], participant: rtc.Participant):
user_id = participant.identity
if not rate_limiter.is_allowed(user_id):
logger.warning(f"Rate limit exceeded for user {user_id}")
asyncio.create_task(agent.say("You have exceeded the rate limit. Please try again later."))
return


if participant.kind != rtc.ParticipantKind.PARTICIPANT_KIND_STANDARD:
return

if "voice" in changed_attributes:
voice_id = participant.attributes.get("voice")
logger.info(f"Participant {participant.identity} requested voice change: {voice_id}")

if not voice_id:
return

Expand All @@ -93,6 +90,7 @@ def on_participant_attributes_changed(changed_attributes: dict[str, str], partic
if not (is_agent_speaking or is_user_speaking):
asyncio.create_task(agent.say("How do I sound now?", allow_interruptions=True))


await ctx.connect()

@agent.on("agent_started_speaking")
Expand All @@ -104,9 +102,18 @@ def agent_started_speaking():
def agent_stopped_speaking():
nonlocal is_agent_speaking
is_agent_speaking = False

@agent.on("user_started_speaking")
def user_started_speaking():
# Check if user exceeds the rate limit
logger.info(f"User {ctx.room.local_participant.identity} started speaking")
user_id = ctx.room.local_participant.identity
if not rate_limiter.is_allowed(user_id):
logger.warning(f"Rate limit exceeded for user {user_id}")

# Silently disconnect the participant
asyncio.create_task(ctx.room.disconnect())
return
nonlocal is_user_speaking
is_user_speaking = True

Expand All @@ -115,12 +122,14 @@ def user_stopped_speaking():
nonlocal is_user_speaking
is_user_speaking = False



voices = [{"id": voice["id"], "name": voice["name"]} for voice in cartesia_voices]
voices.sort(key=lambda x: x["name"])
await ctx.room.local_participant.set_attributes({"voices": json.dumps(voices)})

agent.start(ctx.room)
await agent.say("Thank you for calling Softcery Voice Agent", allow_interruptions=True)
await agent.say("Thank you for calling Softcery Voice Agent. How can I help you today?", allow_interruptions=True)

except Exception as e:
logger.error(f"An error occurred in the entrypoint: {e}")
2 changes: 2 additions & 0 deletions web/.env.template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export VA_BACKEND_URL=<BACKEND_URL>
export LIVEKIT_URL=<LIVEKIT_URL>
24 changes: 24 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
.env.sh
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
28 changes: 28 additions & 0 deletions web/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
Loading

0 comments on commit 15be08a

Please sign in to comment.