Bots now join on demand
This commit is contained in:
parent
b916db243b
commit
b5614b9d99
@ -6,28 +6,21 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
__pycache__
|
**/__pycache__
|
||||||
|
**/.venv
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyd
|
*.pyd
|
||||||
*.log
|
*.log
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
.DS_Store
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.sublime-workspace
|
|
||||||
*.sublime-project
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
dev-keys
|
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
coverage
|
|
||||||
*.bak
|
*.bak
|
||||||
*.tmp
|
*.tmp
|
||||||
*.local
|
*.local
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
*docker-compose.override.yml
|
|
||||||
|
@ -172,7 +172,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
{Object.entries(bots).map(([botName, botInfo]) => {
|
{Object.entries(bots).map(([botName, botInfo]) => {
|
||||||
const providerId = providers[botName];
|
const providerId = providers[botName];
|
||||||
const providerName = getProviderName(providerId);
|
const providerName = getProviderName(providerId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem key={botName}>
|
<ListItem key={botName}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
@ -182,14 +182,10 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
<Typography variant="body2" component="div">
|
<Typography variant="body2" component="div">
|
||||||
{botInfo.description}
|
{botInfo.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip
|
<Chip label={providerName} size="small" variant="outlined" sx={{ mt: 0.5 }} />
|
||||||
label={providerName}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ mt: 0.5 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
secondaryTypographyProps={{ component: "div" }}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
@ -220,6 +216,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
secondaryTypographyProps={{ component: "div" }}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
@ -239,7 +236,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
Select Bot
|
Select Bot
|
||||||
@ -258,15 +255,8 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => setSelectedBot(botName)}
|
onClick={() => setSelectedBot(botName)}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText primary={botInfo.name} secondary={botInfo.description} />
|
||||||
primary={botInfo.name}
|
<Chip label={getProviderName(providers[botName])} size="small" variant="outlined" />
|
||||||
secondary={botInfo.description}
|
|
||||||
/>
|
|
||||||
<Chip
|
|
||||||
label={getProviderName(providers[botName])}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
@ -64,15 +64,14 @@ services:
|
|||||||
- PRODUCTION=${PRODUCTION:-false}
|
- PRODUCTION=${PRODUCTION:-false}
|
||||||
- VOICEBOT_MODE=provider
|
- VOICEBOT_MODE=provider
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./cache:/root/.cache:rw
|
- ./cache:/root/.cache:rw
|
||||||
- ./shared:/shared:ro
|
- ./shared:/shared:ro
|
||||||
- ./voicebot:/voicebot:rw
|
- ./voicebot:/voicebot:rw
|
||||||
- ./voicebot/.venv:/voicebot/.venv:rw
|
- ./voicebot/.venv:/voicebot/.venv:rw
|
||||||
# network_mode: host
|
# network_mode: host
|
||||||
# networks:
|
networks:
|
||||||
# - ai-voicebot-net
|
- ai-voicebot-net
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
@ -1433,13 +1433,17 @@ async def request_bot_join_lobby(
|
|||||||
# Create a session for the bot
|
# Create a session for the bot
|
||||||
bot_session_id = secrets.token_hex(16)
|
bot_session_id = secrets.token_hex(16)
|
||||||
|
|
||||||
|
# Create the Session object for the bot
|
||||||
|
bot_session = Session(bot_session_id)
|
||||||
|
logger.info(f"Created session for bot: {bot_session.getName()}")
|
||||||
|
|
||||||
# Determine server URL for the bot to connect back to
|
# Determine server URL for the bot to connect back to
|
||||||
# Use the server's public URL or construct from request
|
# Use the server's public URL or construct from request
|
||||||
server_base_url = os.getenv("PUBLIC_SERVER_URL", "http://localhost:8000")
|
server_base_url = os.getenv("PUBLIC_SERVER_URL", "http://localhost:8000")
|
||||||
if server_base_url.endswith("/"):
|
if server_base_url.endswith("/"):
|
||||||
server_base_url = server_base_url[:-1]
|
server_base_url = server_base_url[:-1]
|
||||||
|
|
||||||
bot_nick = request.nick or f"{bot_name}-bot"
|
bot_nick = request.nick or f"{bot_name}-bot-{bot_session_id[:8]}"
|
||||||
|
|
||||||
# Prepare the join request for the bot provider
|
# Prepare the join request for the bot provider
|
||||||
bot_join_payload = BotJoinPayload(
|
bot_join_payload = BotJoinPayload(
|
||||||
@ -1447,7 +1451,7 @@ async def request_bot_join_lobby(
|
|||||||
session_id=bot_session_id,
|
session_id=bot_session_id,
|
||||||
nick=bot_nick,
|
nick=bot_nick,
|
||||||
server_url=f"{server_base_url}{public_url}".rstrip("/"),
|
server_url=f"{server_base_url}{public_url}".rstrip("/"),
|
||||||
insecure=False, # Assume secure by default
|
insecure=True, # Accept self-signed certificates in development
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -11,6 +11,8 @@ import importlib
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
# Add the parent directory to sys.path to allow absolute imports
|
# Add the parent directory to sys.path to allow absolute imports
|
||||||
@ -24,11 +26,47 @@ from voicebot.models import JoinRequest
|
|||||||
from voicebot.webrtc_signaling import WebRTCSignalingClient
|
from voicebot.webrtc_signaling import WebRTCSignalingClient
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="voicebot-bot-orchestrator")
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# Startup
|
||||||
|
logger.info(f"🚀 Voicebot bot orchestrator started successfully at {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
# Log the discovered bots
|
||||||
|
bots = discover_bots()
|
||||||
|
if bots:
|
||||||
|
logger.info(f"📋 Discovered {len(bots)} bots: {list(bots.keys())}")
|
||||||
|
else:
|
||||||
|
logger.info("⚠️ No bots discovered")
|
||||||
|
|
||||||
|
# Check for remote server registration
|
||||||
|
remote_server_url = os.getenv('VOICEBOT_SERVER_URL')
|
||||||
|
if remote_server_url:
|
||||||
|
# Attempt to register with remote server
|
||||||
|
try:
|
||||||
|
host = os.getenv('HOST', '0.0.0.0')
|
||||||
|
port = os.getenv('PORT', '8788')
|
||||||
|
insecure = os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
provider_id = await _perform_server_registration(remote_server_url, host, port, insecure)
|
||||||
|
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {provider_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to register with remote server: {e}")
|
||||||
|
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
|
||||||
|
else:
|
||||||
|
logger.info("ℹ️ No VOICEBOT_SERVER_URL provided - running in local-only mode")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Shutdown
|
||||||
|
logger.info("🛑 Voicebot bot orchestrator shutting down")
|
||||||
|
|
||||||
|
app = FastAPI(title="voicebot-bot-orchestrator", lifespan=lifespan)
|
||||||
|
|
||||||
# Lightweight in-memory registry of running bot clients
|
# Lightweight in-memory registry of running bot clients
|
||||||
registry: Dict[str, WebRTCSignalingClient] = {}
|
registry: Dict[str, WebRTCSignalingClient] = {}
|
||||||
|
|
||||||
|
# Log module import for debugging reloads
|
||||||
|
logger.info("📦 Bot orchestrator module imported/reloaded")
|
||||||
|
|
||||||
|
|
||||||
def discover_bots() -> Dict[str, Dict[str, Any]]:
|
def discover_bots() -> Dict[str, Dict[str, Any]]:
|
||||||
"""Discover bot modules under the voicebot.bots package that expose bot_info.
|
"""Discover bot modules under the voicebot.bots package that expose bot_info.
|
||||||
@ -83,6 +121,8 @@ async def bot_join(bot_name: str, req: JoinRequest):
|
|||||||
|
|
||||||
create_tracks = bot.get("create_tracks")
|
create_tracks = bot.get("create_tracks")
|
||||||
|
|
||||||
|
logger.info(f"🤖 Bot {bot_name} joining lobby {req.lobby_id} with nick: '{req.nick}'")
|
||||||
|
|
||||||
# Start the WebRTCSignalingClient in a background asyncio task and register it
|
# Start the WebRTCSignalingClient in a background asyncio task and register it
|
||||||
client = WebRTCSignalingClient(
|
client = WebRTCSignalingClient(
|
||||||
server_url=req.server_url,
|
server_url=req.server_url,
|
||||||
@ -104,8 +144,14 @@ async def bot_join(bot_name: str, req: JoinRequest):
|
|||||||
finally:
|
finally:
|
||||||
registry.pop(run_id, None)
|
registry.pop(run_id, None)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
def run_client_in_thread():
|
||||||
threading.Thread(target=loop.run_until_complete, args=(run_client(),), daemon=True).start()
|
"""Run the client in a new event loop in a separate thread."""
|
||||||
|
try:
|
||||||
|
asyncio.run(run_client())
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Bot client thread failed for run %s", run_id)
|
||||||
|
|
||||||
|
threading.Thread(target=run_client_in_thread, daemon=True).start()
|
||||||
|
|
||||||
return {"status": "started", "bot": bot_name, "run_id": run_id}
|
return {"status": "started", "bot": bot_name, "run_id": run_id}
|
||||||
|
|
||||||
@ -141,8 +187,46 @@ def start_bot_api(host: str = "0.0.0.0", port: int = 8788):
|
|||||||
uvicorn.run(app, host=host, port=port)
|
uvicorn.run(app, host=host, port=port)
|
||||||
|
|
||||||
|
|
||||||
|
def _construct_voicebot_url(host: str, port: str) -> str:
|
||||||
|
"""Construct the voicebot URL based on host and port"""
|
||||||
|
if host == "0.0.0.0":
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
voicebot_url = f"http://{hostname}:{port}"
|
||||||
|
logger.info(f"🏠 Using hostname-based URL: {voicebot_url}")
|
||||||
|
except Exception:
|
||||||
|
voicebot_url = f"http://localhost:{port}"
|
||||||
|
logger.info(f"🏠 Using localhost URL: {voicebot_url}")
|
||||||
|
else:
|
||||||
|
voicebot_url = f"http://{host}:{port}"
|
||||||
|
logger.info(f"🏠 Using host-based URL: {voicebot_url}")
|
||||||
|
|
||||||
|
return voicebot_url
|
||||||
|
|
||||||
|
|
||||||
|
def _perform_server_registration_sync(server_url: str, host: str, port: str, insecure: bool) -> str:
|
||||||
|
"""Synchronous wrapper for _perform_server_registration"""
|
||||||
|
return asyncio.run(_perform_server_registration(server_url, host, port, insecure))
|
||||||
|
|
||||||
|
|
||||||
|
async def _perform_server_registration(server_url: str, host: str, port: str, insecure: bool) -> str:
|
||||||
|
"""Perform server registration with common logic"""
|
||||||
|
voicebot_url = _construct_voicebot_url(host, port)
|
||||||
|
|
||||||
|
logger.info("⏱️ Waiting 2 seconds before attempting remote registration...")
|
||||||
|
await asyncio.sleep(2) # Give server time to fully start
|
||||||
|
|
||||||
|
provider_id = await register_with_server(server_url, voicebot_url, insecure)
|
||||||
|
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {provider_id}")
|
||||||
|
return provider_id
|
||||||
|
|
||||||
|
|
||||||
async def register_with_server(server_url: str, voicebot_url: str, insecure: bool = False) -> str:
|
async def register_with_server(server_url: str, voicebot_url: str, insecure: bool = False) -> str:
|
||||||
"""Register this voicebot instance as a bot provider with the main server"""
|
"""Register this voicebot instance as a bot provider with the main server"""
|
||||||
|
logger.info(f"🔗 Attempting to register with remote server at {server_url}")
|
||||||
|
logger.info(f"📍 Registration details - Voicebot URL: {voicebot_url}, Insecure: {insecure}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Import httpx locally to avoid dependency issues
|
# Import httpx locally to avoid dependency issues
|
||||||
import httpx
|
import httpx
|
||||||
@ -153,6 +237,8 @@ async def register_with_server(server_url: str, voicebot_url: str, insecure: boo
|
|||||||
"description": "AI voicebot provider with speech recognition and synthetic media capabilities"
|
"description": "AI voicebot provider with speech recognition and synthetic media capabilities"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(f"📤 Sending registration payload: {payload}")
|
||||||
|
|
||||||
# Prepare SSL context if needed
|
# Prepare SSL context if needed
|
||||||
verify = not insecure
|
verify = not insecure
|
||||||
|
|
||||||
@ -166,14 +252,15 @@ async def register_with_server(server_url: str, voicebot_url: str, insecure: boo
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
provider_id = result.get("provider_id")
|
provider_id = result.get("provider_id")
|
||||||
logger.info(f"Successfully registered with server as provider: {provider_id}")
|
logger.info(f"✅ Successfully registered with server as provider: {provider_id}")
|
||||||
|
logger.info(f"🎯 Remote server can now discover bots at: {voicebot_url}")
|
||||||
return provider_id
|
return provider_id
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to register with server: HTTP {response.status_code}: {response.text}")
|
logger.error(f"❌ Failed to register with server: HTTP {response.status_code}: {response.text}")
|
||||||
raise RuntimeError(f"Registration failed: {response.status_code}")
|
raise RuntimeError(f"Registration failed: {response.status_code}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error registering with server: {e}")
|
logger.error(f"💥 Error registering with server: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@ -186,7 +273,6 @@ def start_bot_provider(
|
|||||||
):
|
):
|
||||||
"""Start the bot provider API server and optionally register with main server"""
|
"""Start the bot provider API server and optionally register with main server"""
|
||||||
import time
|
import time
|
||||||
import socket
|
|
||||||
|
|
||||||
# Start the FastAPI server in a background thread
|
# Start the FastAPI server in a background thread
|
||||||
# Add reload functionality for development
|
# Add reload functionality for development
|
||||||
@ -212,23 +298,19 @@ def start_bot_provider(
|
|||||||
|
|
||||||
# If server_url is provided, register with the main server
|
# If server_url is provided, register with the main server
|
||||||
if server_url:
|
if server_url:
|
||||||
|
logger.info(f"🔄 Server URL provided - will attempt registration with: {server_url}")
|
||||||
# Give the server a moment to start
|
# Give the server a moment to start
|
||||||
|
logger.info("⏱️ Waiting 2 seconds for server to fully start...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# Construct the voicebot URL
|
|
||||||
voicebot_url = f"http://{host}:{port}"
|
|
||||||
if host == "0.0.0.0":
|
|
||||||
# Try to get a better hostname
|
|
||||||
try:
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
voicebot_url = f"http://{hostname}:{port}"
|
|
||||||
except Exception:
|
|
||||||
voicebot_url = f"http://localhost:{port}"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.run(register_with_server(server_url, voicebot_url, insecure))
|
provider_id = _perform_server_registration_sync(server_url, host, str(port), insecure)
|
||||||
|
logger.info(f"🎉 Registration completed successfully! Provider ID: {provider_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to register with server: {e}")
|
logger.error(f"❌ Failed to register with server: {e}")
|
||||||
|
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
|
||||||
|
else:
|
||||||
|
logger.info("ℹ️ No remote server URL provided - running in local-only mode")
|
||||||
|
|
||||||
# Keep the main thread alive
|
# Keep the main thread alive
|
||||||
try:
|
try:
|
||||||
|
@ -8,6 +8,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import fcntl
|
import fcntl
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -41,6 +42,9 @@ def create_client_app(args: VoicebotArgs) -> FastAPI:
|
|||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
nonlocal client_task, lock_file
|
nonlocal client_task, lock_file
|
||||||
# Startup
|
# Startup
|
||||||
|
logger.info(f"🚀 Voicebot client app started at {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
logger.info(f"📡 Client config - Server: {args.server_url}, Lobby: {args.lobby}, Session: {args.session_name}")
|
||||||
|
|
||||||
# Use a file lock to prevent multiple instances from starting
|
# Use a file lock to prevent multiple instances from starting
|
||||||
lock_file_path = "/tmp/voicebot_client.lock"
|
lock_file_path = "/tmp/voicebot_client.lock"
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ if [ "$PRODUCTION" != "true" ]; then
|
|||||||
if [ "$MODE" = "provider" ]; then
|
if [ "$MODE" = "provider" ]; then
|
||||||
echo "Running as bot provider with auto-reload..."
|
echo "Running as bot provider with auto-reload..."
|
||||||
export VOICEBOT_MODE=provider
|
export VOICEBOT_MODE=provider
|
||||||
|
export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
|
||||||
|
export VOICEBOT_SERVER_INSECURE="true"
|
||||||
exec uv run uvicorn main:uvicorn_app \
|
exec uv run uvicorn main:uvicorn_app \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 8788 \
|
--port 8788 \
|
||||||
@ -37,11 +39,11 @@ if [ "$PRODUCTION" != "true" ]; then
|
|||||||
else
|
else
|
||||||
echo "Running as client (connecting to lobby)..."
|
echo "Running as client (connecting to lobby)..."
|
||||||
export VOICEBOT_MODE=client
|
export VOICEBOT_MODE=client
|
||||||
export VOICEBOT_SERVER_URL="https://ketrenos.com/ai-voicebot"
|
export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
|
||||||
|
export VOICEBOT_SERVER_INSECURE="true"
|
||||||
export VOICEBOT_LOBBY="default"
|
export VOICEBOT_LOBBY="default"
|
||||||
export VOICEBOT_SESSION_NAME="Python Voicebot"
|
export VOICEBOT_SESSION_NAME="Python Voicebot"
|
||||||
export VOICEBOT_PASSWORD="v01c3b0t"
|
export VOICEBOT_PASSWORD="v01c3b0t"
|
||||||
export VOICEBOT_INSECURE="true"
|
|
||||||
exec uv run uvicorn main:uvicorn_app \
|
exec uv run uvicorn main:uvicorn_app \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 8789 \
|
--port 8789 \
|
||||||
@ -55,6 +57,8 @@ else
|
|||||||
if [ "$MODE" = "provider" ]; then
|
if [ "$MODE" = "provider" ]; then
|
||||||
echo "Running as bot provider..."
|
echo "Running as bot provider..."
|
||||||
export VOICEBOT_MODE=provider
|
export VOICEBOT_MODE=provider
|
||||||
|
export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
|
||||||
|
export VOICEBOT_SERVER_INSECURE="true"
|
||||||
exec uv run uvicorn main:uvicorn_app \
|
exec uv run uvicorn main:uvicorn_app \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 8788 \
|
--port 8788 \
|
||||||
@ -62,11 +66,11 @@ else
|
|||||||
else
|
else
|
||||||
echo "Running as client (connecting to lobby)..."
|
echo "Running as client (connecting to lobby)..."
|
||||||
export VOICEBOT_MODE=client
|
export VOICEBOT_MODE=client
|
||||||
export VOICEBOT_SERVER_URL="https://ai-voicebot.ketrenos.com"
|
export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
|
||||||
|
export VOICEBOT_SERVER_INSECURE="true"
|
||||||
export VOICEBOT_LOBBY="default"
|
export VOICEBOT_LOBBY="default"
|
||||||
export VOICEBOT_SESSION_NAME="Python Voicebot"
|
export VOICEBOT_SESSION_NAME="Python Voicebot"
|
||||||
export VOICEBOT_PASSWORD="v01c3b0t"
|
export VOICEBOT_PASSWORD="v01c3b0t"
|
||||||
export VOICEBOT_INSECURE="false"
|
|
||||||
exec uv run uvicorn main:uvicorn_app \
|
exec uv run uvicorn main:uvicorn_app \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 8789 \
|
--port 8789 \
|
||||||
|
@ -13,11 +13,17 @@ import os
|
|||||||
# Add the parent directory to sys.path to allow absolute imports
|
# Add the parent directory to sys.path to allow absolute imports
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# Import logger for reload debugging
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
from voicebot.models import VoicebotArgs, VoicebotMode
|
from voicebot.models import VoicebotArgs, VoicebotMode
|
||||||
from voicebot.client_main import main_with_args, start_client_with_reload
|
from voicebot.client_main import main_with_args, start_client_with_reload
|
||||||
from voicebot.bot_orchestrator import start_bot_provider
|
from voicebot.bot_orchestrator import start_bot_provider
|
||||||
from voicebot.client_app import get_app
|
from voicebot.client_app import get_app
|
||||||
|
|
||||||
|
# Log module import for debugging reloads
|
||||||
|
logger.info("📦 Main module imported/reloaded")
|
||||||
|
|
||||||
# Create app instance for uvicorn import
|
# Create app instance for uvicorn import
|
||||||
uvicorn_app = get_app()
|
uvicorn_app = get_app()
|
||||||
|
|
||||||
@ -27,7 +33,7 @@ async def main():
|
|||||||
parser = argparse.ArgumentParser(description="Python WebRTC voicebot client")
|
parser = argparse.ArgumentParser(description="Python WebRTC voicebot client")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--server-url",
|
"--server-url",
|
||||||
default="http://localhost:8000/ai-voicebot",
|
default="https://server:8000/ai-voicebot",
|
||||||
help="AI-Voicebot lobby and signaling server base URL (http:// or https://)",
|
help="AI-Voicebot lobby and signaling server base URL (http:// or https://)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -58,13 +58,13 @@ class VoicebotArgs(BaseModel):
|
|||||||
host=os.getenv('VOICEBOT_HOST', '0.0.0.0'),
|
host=os.getenv('VOICEBOT_HOST', '0.0.0.0'),
|
||||||
port=int(os.getenv('VOICEBOT_PORT', '8788')),
|
port=int(os.getenv('VOICEBOT_PORT', '8788')),
|
||||||
reload=os.getenv('VOICEBOT_RELOAD', 'false').lower() == 'true',
|
reload=os.getenv('VOICEBOT_RELOAD', 'false').lower() == 'true',
|
||||||
server_url=os.getenv('VOICEBOT_SERVER_URL', 'http://localhost:8000/ai-voicebot'),
|
server_url=os.getenv('VOICEBOT_SERVER_URL', 'https://server:8000/ai-voicebot'),
|
||||||
lobby=os.getenv('VOICEBOT_LOBBY', 'default'),
|
lobby=os.getenv('VOICEBOT_LOBBY', 'default'),
|
||||||
session_name=os.getenv('VOICEBOT_SESSION_NAME', 'Python Bot'),
|
session_name=os.getenv('VOICEBOT_SESSION_NAME', 'Python Bot'),
|
||||||
session_id=os.getenv('VOICEBOT_SESSION_ID', None),
|
session_id=os.getenv('VOICEBOT_SESSION_ID', None),
|
||||||
password=os.getenv('VOICEBOT_PASSWORD', None),
|
password=os.getenv('VOICEBOT_PASSWORD', None),
|
||||||
private=os.getenv('VOICEBOT_PRIVATE', 'false').lower() == 'true',
|
private=os.getenv('VOICEBOT_PRIVATE', 'false').lower() == 'true',
|
||||||
insecure=os.getenv('VOICEBOT_INSECURE', 'false').lower() == 'true',
|
insecure=os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true',
|
||||||
registration_check_interval=float(os.getenv('VOICEBOT_REGISTRATION_CHECK_INTERVAL', '30.0'))
|
registration_check_interval=float(os.getenv('VOICEBOT_REGISTRATION_CHECK_INTERVAL', '30.0'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,6 +58,17 @@ from voicebot.models import Peer, MessageData
|
|||||||
from voicebot.utils import create_ssl_context, log_network_info
|
from voicebot.utils import create_ssl_context, log_network_info
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_http_to_ws_url(url: str) -> str:
|
||||||
|
"""Convert HTTP/HTTPS URL to WebSocket URL by replacing scheme."""
|
||||||
|
if url.startswith("https://"):
|
||||||
|
return url.replace("https://", "wss://", 1)
|
||||||
|
elif url.startswith("http://"):
|
||||||
|
return url.replace("http://", "ws://", 1)
|
||||||
|
else:
|
||||||
|
# Assume it's already a WebSocket URL
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
class WebSocketProtocol(Protocol):
|
class WebSocketProtocol(Protocol):
|
||||||
def send(self, message: object, text: Optional[bool] = None) -> Awaitable[None]: ...
|
def send(self, message: object, text: Optional[bool] = None) -> Awaitable[None]: ...
|
||||||
def close(self, code: int = 1000, reason: str = "") -> Awaitable[None]: ...
|
def close(self, code: int = 1000, reason: str = "") -> Awaitable[None]: ...
|
||||||
@ -93,7 +104,7 @@ class WebRTCSignalingClient:
|
|||||||
self.websocket: Optional[object] = None
|
self.websocket: Optional[object] = None
|
||||||
|
|
||||||
# Optional password to register or takeover a name
|
# Optional password to register or takeover a name
|
||||||
self.name_password: Optional[str] = None
|
self.name_password: Optional[str] = session_name
|
||||||
|
|
||||||
self.peers: dict[str, Peer] = {}
|
self.peers: dict[str, Peer] = {}
|
||||||
self.peer_connections: dict[str, RTCPeerConnection] = {}
|
self.peer_connections: dict[str, RTCPeerConnection] = {}
|
||||||
@ -120,18 +131,29 @@ class WebRTCSignalingClient:
|
|||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect to the signaling server"""
|
"""Connect to the signaling server"""
|
||||||
ws_url = f"{self.server_url}/ws/lobby/{self.lobby_id}/{self.session_id}"
|
base_ws_url = _convert_http_to_ws_url(self.server_url)
|
||||||
|
ws_url = f"{base_ws_url}/ws/lobby/{self.lobby_id}/{self.session_id}"
|
||||||
logger.info(f"Connecting to signaling server: {ws_url}")
|
logger.info(f"Connecting to signaling server: {ws_url}")
|
||||||
|
|
||||||
# Log network information for debugging
|
# Log network information for debugging
|
||||||
log_network_info()
|
log_network_info()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# If insecure (self-signed certs), create an SSL context for the websocket
|
# Create SSL context based on URL scheme and insecure setting
|
||||||
ws_ssl = create_ssl_context(self.insecure)
|
if ws_url.startswith("wss://"):
|
||||||
|
# For wss://, we need an SSL context
|
||||||
|
if self.insecure:
|
||||||
|
# Accept self-signed certificates
|
||||||
|
ws_ssl = create_ssl_context(insecure=True)
|
||||||
|
else:
|
||||||
|
# Use default SSL context for secure connections
|
||||||
|
ws_ssl = True
|
||||||
|
else:
|
||||||
|
# For ws://, no SSL context needed
|
||||||
|
ws_ssl = None
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Attempting websocket connection to {ws_url} with ssl={bool(ws_ssl)}"
|
f"Attempting websocket connection to {ws_url} with ssl={ws_ssl}"
|
||||||
)
|
)
|
||||||
self.websocket = await websockets.connect(ws_url, ssl=ws_ssl)
|
self.websocket = await websockets.connect(ws_url, ssl=ws_ssl)
|
||||||
logger.info("Connected to signaling server")
|
logger.info("Connected to signaling server")
|
||||||
@ -263,10 +285,21 @@ class WebRTCSignalingClient:
|
|||||||
self.websocket = None
|
self.websocket = None
|
||||||
|
|
||||||
# Reconnect
|
# Reconnect
|
||||||
ws_url = f"{self.server_url}/ws/lobby/{self.lobby_id}/{self.session_id}"
|
base_ws_url = _convert_http_to_ws_url(self.server_url)
|
||||||
|
ws_url = f"{base_ws_url}/ws/lobby/{self.lobby_id}/{self.session_id}"
|
||||||
|
|
||||||
# If insecure (self-signed certs), create an SSL context for the websocket
|
# Create SSL context based on URL scheme and insecure setting
|
||||||
ws_ssl = create_ssl_context(self.insecure)
|
if ws_url.startswith("wss://"):
|
||||||
|
# For wss://, we need an SSL context
|
||||||
|
if self.insecure:
|
||||||
|
# Accept self-signed certificates
|
||||||
|
ws_ssl = create_ssl_context(insecure=True)
|
||||||
|
else:
|
||||||
|
# Use default SSL context for secure connections
|
||||||
|
ws_ssl = True
|
||||||
|
else:
|
||||||
|
# For ws://, no SSL context needed
|
||||||
|
ws_ssl = None
|
||||||
|
|
||||||
logger.info(f"Reconnecting to signaling server: {ws_url}")
|
logger.info(f"Reconnecting to signaling server: {ws_url}")
|
||||||
self.websocket = await websockets.connect(ws_url, ssl=ws_ssl)
|
self.websocket = await websockets.connect(ws_url, ssl=ws_ssl)
|
||||||
@ -377,7 +410,13 @@ class WebRTCSignalingClient:
|
|||||||
async def _process_message(self, message: MessageData):
|
async def _process_message(self, message: MessageData):
|
||||||
"""Process incoming signaling messages"""
|
"""Process incoming signaling messages"""
|
||||||
try:
|
try:
|
||||||
# Validate the base message structure first
|
# Handle error messages specially since they have a different structure
|
||||||
|
if message.get("type") == "error" and "error" in message:
|
||||||
|
error_msg = message.get("error", "Unknown error")
|
||||||
|
logger.error(f"Received error from signaling server: {error_msg}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate the base message structure for non-error messages
|
||||||
validated_message = WebSocketMessageModel.model_validate(message)
|
validated_message = WebSocketMessageModel.model_validate(message)
|
||||||
msg_type = validated_message.type
|
msg_type = validated_message.type
|
||||||
data = validated_message.data
|
data = validated_message.data
|
||||||
@ -448,6 +487,10 @@ class WebRTCSignalingClient:
|
|||||||
logger.error(f"Invalid update payload: {e}", exc_info=True)
|
logger.error(f"Invalid update payload: {e}", exc_info=True)
|
||||||
return
|
return
|
||||||
logger.info(f"Received update message: {validated}")
|
logger.info(f"Received update message: {validated}")
|
||||||
|
elif msg_type == "status_check":
|
||||||
|
# Handle status check messages - these are used to verify connection
|
||||||
|
logger.debug(f"Received status check message: {data}")
|
||||||
|
# No special processing needed for status checks, just acknowledge receipt
|
||||||
else:
|
else:
|
||||||
logger.info(f"Unhandled message type: {msg_type} with data: {data}")
|
logger.info(f"Unhandled message type: {msg_type} with data: {data}")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user