Lots of tweaks; in progress is_bot => bot_instance_id

This commit is contained in:
James Ketr 2025-09-04 20:45:09 -07:00
parent 71555c5230
commit 4b33b40637
14 changed files with 259 additions and 719 deletions

View File

@ -179,7 +179,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
switch (param.type) {
case "boolean":
return (
<div key={param.name} className="config-parameter">
<div className="config-parameter">
<label className="config-label">
<input
type="checkbox"
@ -194,7 +194,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
case "select":
return (
<div key={param.name} className="config-parameter">
<div className="config-parameter">
<label className="config-label">{param.label}</label>
<select
value={value || ""}
@ -213,7 +213,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
case "range":
return (
<div key={param.name} className="config-parameter">
<div className="config-parameter">
<label className="config-label">{param.label}</label>
<div className="range-container">
<input
@ -233,7 +233,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
case "number":
return (
<div key={param.name} className="config-parameter">
<div className="config-parameter">
<label className="config-label">{param.label}</label>
<input
type="number"
@ -251,7 +251,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
case "string":
default:
return (
<div key={param.name} className="config-parameter">
<div className="config-parameter">
<label className="config-label">{param.label}</label>
<input
type="text"
@ -275,15 +275,15 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
<div key={category.name} className="config-category">
<h4 className="category-title">{category.name}</h4>
<div className="category-parameters">
{category.parameters.map((paramName) => {
const param = schema.parameters.find((p) => p.name === paramName);
return param ? renderParameter(param) : null;
})}
{category.parameters?.map((paramName) => {
const param = schema.parameters?.find((p) => p.name === paramName);
return param ? <div key={paramName}>{renderParameter(param)}</div> : null;
}) || null}
</div>
</div>
));
} else {
return schema.parameters.map(renderParameter);
return schema.parameters?.map((param) => <div key={param.name}>{renderParameter(param)}</div>) || null;
}
};

View File

@ -78,7 +78,6 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
setAddingBot(true);
try {
const request: BotJoinLobbyRequest = {
bot_name: selectedBot,
lobby_id: lobbyId,
nick: botNick || `${selectedBot}-bot`,
provider_id: providers[selectedBot],

View File

@ -26,6 +26,7 @@ type User = {
has_media?: boolean; // Whether this user provides audio/video streams
bot_run_id?: string;
bot_provider_id?: string;
bot_instance_id?: string; // For bot instances
};
type UserListProps = {
@ -48,16 +49,12 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
const apiClient = new ApiClient();
const handleBotLeave = async (user: User) => {
if (!user.is_bot) return;
if (!user.is_bot || !user.bot_instance_id) return;
setLeavingBots((prev) => new Set(prev).add(user.session_id));
try {
const request: BotLeaveLobbyRequest = {
session_id: user.session_id,
};
await apiClient.requestBotLeaveLobby(request);
await apiClient.requestBotLeaveLobby(user.bot_instance_id);
console.log(`Bot ${user.name} leave requested successfully`);
} catch (error) {
console.error("Failed to request bot leave:", error);
@ -156,12 +153,12 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
{users?.map((user) => (
<Box
key={user.session_id}
sx={{ display: "flex", flexDirection: "column", alignItems: "center", border: "3px solid magenta" }}
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
className={`UserEntry ${user.local ? "UserSelf" : ""}`}
>
<div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "center" }}>
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
<Box style={{ display: "flex-wrap", alignItems: "center" }}>
<div className="Name">{user.name ? user.name : user.session_id}</div>
{user.protected && (
<div
@ -176,9 +173,9 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
🤖
</div>
)}
</div>
{user.is_bot && !user.local && (
<div style={{ display: "flex", gap: "4px" }}>
</Box>
{user.is_bot && (
<Box style={{ display: "flex-wrap", gap: "4px", border: "3px solid magenta" }}>
{user.bot_run_id && (
<IconButton
size="small"
@ -194,14 +191,15 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
variant="outlined"
color="secondary"
onClick={() => handleBotLeave(user)}
disabled={leavingBots.has(user.session_id)}
disabled={leavingBots.has(user.session_id) || !user.bot_instance_id}
style={{ fontSize: "0.7em", minWidth: "50px", height: "24px" }}
title={!user.bot_instance_id ? "Bot instance ID not available" : "Remove bot from lobby"}
>
{leavingBots.has(user.session_id) ? "..." : "Leave"}
</Button>
</div>
</Box>
)}
</div>
</Box>
{user.name && !user.live && <div className="NoNetwork"></div>}
</div>
{user.name && user.live && peers[user.session_id] && (user.local || user.has_media !== false) ? (
@ -240,7 +238,7 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
<DialogContent>
{selectedBotForConfig && (
<BotConfig
botName={selectedBotForConfig.name || "unknown"}
botName={selectedBotForConfig.name?.replace(/-bot$/, "") || "unknown"}
lobbyId={lobbyId}
onConfigUpdate={(config) => {
console.log("Bot configuration updated:", config);

View File

@ -46,7 +46,6 @@ export interface BotListResponse {
}
export interface BotJoinLobbyRequest {
bot_name: string;
lobby_id: string;
nick?: string;
provider_id?: string;
@ -54,9 +53,34 @@ export interface BotJoinLobbyRequest {
export interface BotJoinLobbyResponse {
status: string;
bot_instance_id: string;
bot_name: string;
run_id: string;
provider_id: string;
session_id: string;
}
export interface BotInstanceModel {
bot_instance_id: string;
bot_name: string;
nick: string;
lobby_id: string;
session_id: string;
provider_id: string;
run_id: string;
has_media: boolean;
created_at: number;
}
export interface BotLeaveLobbyRequest {
bot_instance_id: string;
}
export interface BotLeaveLobbyResponse {
status: string;
bot_instance_id: string;
session_id: string;
run_id?: string;
}
export interface BotLeaveLobbyRequest {
@ -212,11 +236,13 @@ export class ApiClient {
);
}
async requestBotLeaveLobby(request: BotLeaveLobbyRequest): Promise<BotLeaveLobbyResponse> {
return this.request<BotLeaveLobbyResponse>(this.getApiPath("/ai-voicebot/api/bots/leave"), {
method: "POST",
body: request,
});
async requestBotLeaveLobby(botInstanceId: string): Promise<BotLeaveLobbyResponse> {
return this.request<BotLeaveLobbyResponse>(
this.getApiPath(`/ai-voicebot/api/bots/instances/${encodeURIComponent(botInstanceId)}/leave`),
{
method: "POST",
}
);
}
// Auto-generated endpoints will be added here by update-api-client.js

View File

@ -68,7 +68,6 @@ export class AdvancedApiEvolutionChecker {
'POST:/ai-voicebot/api/lobby/{sessionId}',
'GET:/ai-voicebot/api/bots/providers',
'GET:/ai-voicebot/api/bots',
'POST:/ai-voicebot/api/bots/leave',
'POST:/ai-voicebot/api/lobby/{session_id}'
]);
}

View File

@ -1,10 +1,9 @@
"""Bot API endpoints"""
from fastapi import APIRouter, HTTPException
from logger import logger
# Import shared models with fallback handling
# Import shared models - NO FALLBACKS!
try:
from ...shared.models import (
from shared.models import (
BotProviderRegisterRequest,
BotProviderRegisterResponse,
BotProviderListResponse,
@ -13,58 +12,12 @@ try:
BotJoinLobbyResponse,
BotLeaveLobbyRequest,
BotLeaveLobbyResponse,
BotInstanceModel,
)
except ImportError as e:
raise ImportError(
f"Failed to import shared models: {e}. Ensure shared/models.py is accessible and PYTHONPATH is correctly set."
)
except ImportError:
try:
from shared.models import (
BotProviderRegisterRequest,
BotProviderRegisterResponse,
BotProviderListResponse,
BotListResponse,
BotJoinLobbyRequest,
BotJoinLobbyResponse,
BotLeaveLobbyRequest,
BotLeaveLobbyResponse,
)
except ImportError:
# Create dummy models for standalone testing
from pydantic import BaseModel
from typing import List, Dict, Optional
class BotProviderRegisterRequest(BaseModel):
base_url: str
name: str
description: str
provider_key: str
class BotProviderRegisterResponse(BaseModel):
provider_id: str
class BotProviderListResponse(BaseModel):
providers: List[dict]
class BotListResponse(BaseModel):
bots: List[dict]
providers: Dict[str, str]
class BotJoinLobbyRequest(BaseModel):
lobby_id: str
provider_id: Optional[str] = None
nick: Optional[str] = None
class BotJoinLobbyResponse(BaseModel):
status: str
bot_name: str
run_id: str
provider_id: str
class BotLeaveLobbyRequest(BaseModel):
session_id: str
class BotLeaveLobbyResponse(BaseModel):
status: str
session_id: str
run_id: Optional[str] = None
def create_bot_router(bot_manager, session_manager, lobby_manager):
@ -107,11 +60,14 @@ def create_bot_router(bot_manager, session_manager, lobby_manager):
raise HTTPException(status_code=502, detail=str(e))
else:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/leave", response_model=BotLeaveLobbyResponse)
async def request_bot_leave_lobby(request: BotLeaveLobbyRequest) -> BotLeaveLobbyResponse:
"""Request a bot to leave from all lobbies and disconnect"""
@router.post(
"/instances/{bot_instance_id}/leave", response_model=BotLeaveLobbyResponse
)
async def request_bot_leave_lobby(bot_instance_id: str) -> BotLeaveLobbyResponse:
"""Request a bot instance to leave from all lobbies and disconnect"""
try:
request = BotLeaveLobbyRequest(bot_instance_id=bot_instance_id)
return await bot_manager.request_bot_leave(request, session_manager)
except ValueError as e:
if "not found" in str(e).lower():
@ -120,5 +76,16 @@ def create_bot_router(bot_manager, session_manager, lobby_manager):
raise HTTPException(status_code=400, detail=str(e))
else:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/instances/{bot_instance_id}", response_model=dict)
async def get_bot_instance(bot_instance_id: str) -> dict:
"""Get information about a specific bot instance"""
try:
return await bot_manager.get_bot_instance(bot_instance_id)
except ValueError as e:
if "not found" in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
else:
raise HTTPException(status_code=500, detail=str(e))
return router

View File

@ -16,46 +16,14 @@ from pathlib import Path
from logger import logger
# Import shared models with fallback handling
try:
from ...shared.models import (
BotConfigSchema,
BotConfigParameter,
BotLobbyConfig,
BotConfigUpdateRequest,
BotConfigUpdateResponse,
BotConfigListResponse,
BotInfoModel
)
except ImportError:
try:
from shared.models import (
BotConfigSchema,
BotConfigParameter,
BotLobbyConfig,
BotConfigUpdateRequest,
BotConfigUpdateResponse,
BotConfigListResponse,
BotInfoModel
)
except ImportError:
# Create dummy models for standalone testing
from pydantic import BaseModel
class BotConfigSchema(BaseModel):
bot_name: str
version: str = "1.0"
parameters: List[Dict[str, Any]]
class BotLobbyConfig(BaseModel):
bot_name: str
lobby_id: str
provider_id: str
config_values: Dict[str, Any]
created_at: float
updated_at: float
created_by: str
# Import shared models
import sys
import os
sys.path.append(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
from shared.models import BotConfigSchema, BotLobbyConfig
class BotConfigManager:
"""Manages bot configurations for lobbies"""

View File

@ -9,52 +9,28 @@ from typing import Dict, List, Optional
from pydantic import ValidationError
from logger import logger
# Import shared models with fallback handling
try:
from ...shared.models import (
BotProviderModel,
BotProviderRegisterRequest,
BotProviderRegisterResponse,
BotProviderListResponse,
BotListResponse,
BotInfoModel,
BotJoinLobbyRequest,
BotJoinLobbyResponse,
BotLeaveLobbyRequest,
BotLeaveLobbyResponse,
BotProviderBotsResponse,
BotProviderJoinResponse,
BotJoinPayload,
)
except ImportError:
try:
# Try direct import (when PYTHONPATH is set)
from shared.models import (
BotProviderModel,
BotProviderRegisterRequest,
BotProviderRegisterResponse,
BotProviderListResponse,
BotListResponse,
BotInfoModel,
BotJoinLobbyRequest,
BotJoinLobbyResponse,
BotLeaveLobbyRequest,
BotLeaveLobbyResponse,
BotProviderBotsResponse,
BotProviderJoinResponse,
BotJoinPayload,
)
except ImportError:
# Log a warning for debugging (optional)
import warnings
# Import shared models
import sys
warnings.warn(
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
)
# Rely on environment setup or raise a clear error
raise ImportError(
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
)
sys.path.append(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
from shared.models import (
BotProviderModel,
BotProviderRegisterRequest,
BotProviderRegisterResponse,
BotProviderListResponse,
BotListResponse,
BotInfoModel,
BotJoinLobbyRequest,
BotJoinLobbyResponse,
BotLeaveLobbyRequest,
BotLeaveLobbyResponse,
BotProviderBotsResponse,
BotProviderJoinResponse,
BotJoinPayload,
BotInstanceModel,
)
class BotProviderConfig:
@ -94,6 +70,9 @@ class BotManager:
def __init__(self):
self.bot_providers: Dict[str, BotProviderModel] = {}
self.bot_instances: Dict[
str, BotInstanceModel
] = {} # bot_instance_id -> BotInstanceModel
self.lock = threading.RLock()
# Check if provider authentication is enabled
@ -311,13 +290,37 @@ class BotManager:
bot_session.bot_run_id = run_id
bot_session.bot_provider_id = target_provider_id
logger.info(f"Bot {bot_name} requested to join lobby {request.lobby_id}")
# Create a unique bot instance ID and track the bot instance
bot_instance_id = str(uuid.uuid4())
bot_instance = BotInstanceModel(
bot_instance_id=bot_instance_id,
bot_name=bot_name,
nick=bot_nick,
lobby_id=request.lobby_id,
session_id=bot_session_id,
provider_id=target_provider_id,
run_id=run_id,
has_media=bot_has_media,
created_at=time.time(),
)
# Set the bot_instance_id on the session as well
bot_session.bot_instance_id = bot_instance_id
with self.lock:
self.bot_instances[bot_instance_id] = bot_instance
logger.info(
f"Bot {bot_name} requested to join lobby {request.lobby_id} with instance ID {bot_instance_id}"
)
return BotJoinLobbyResponse(
status="requested",
bot_instance_id=bot_instance_id,
bot_name=bot_name,
run_id=run_id,
provider_id=target_provider_id,
session_id=bot_session_id,
)
except ValidationError as e:
logger.error(f"Invalid response from bot provider: {e}")
@ -334,61 +337,98 @@ class BotManager:
async def request_bot_leave(self, request: BotLeaveLobbyRequest, session_manager) -> BotLeaveLobbyResponse:
"""Request a bot to leave from all lobbies and disconnect"""
# Find the bot instance
with self.lock:
if request.bot_instance_id not in self.bot_instances:
raise ValueError("Bot instance not found")
bot_instance = self.bot_instances[request.bot_instance_id]
# Find the bot session
bot_session = session_manager.get_session(request.session_id)
bot_session = session_manager.get_session(bot_instance.session_id)
if not bot_session:
raise ValueError("Bot session not found")
if not bot_session.is_bot:
raise ValueError("Session is not a bot")
run_id = bot_session.bot_run_id
provider_id = bot_session.bot_provider_id
logger.info(
f"Requesting bot instance {bot_instance.bot_instance_id} to leave all lobbies"
)
logger.info(f"Requesting bot {bot_session.getName()} to leave all lobbies")
# Try to stop the bot at the provider level if we have the information
if provider_id and run_id:
# Try to stop the bot at the provider level
try:
with self.lock:
if provider_id in self.bot_providers:
provider = self.bot_providers[provider_id]
if bot_instance.provider_id in self.bot_providers:
provider = self.bot_providers[bot_instance.provider_id]
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{provider.base_url}/bots/runs/{run_id}/stop",
f"{provider.base_url}/bots/runs/{bot_instance.run_id}/stop",
timeout=5.0,
)
if response.status_code == 200:
logger.info(f"Successfully requested bot provider to stop run {run_id}")
logger.info(
f"Successfully requested bot provider to stop run {bot_instance.run_id}"
)
else:
logger.warning(f"Bot provider returned error when stopping: HTTP {response.status_code}")
logger.warning(
f"Bot provider returned error when stopping: HTTP {response.status_code}"
)
except Exception as e:
logger.warning(f"Failed to request bot stop from provider: {e}")
except Exception as e:
logger.warning(f"Error communicating with bot provider: {e}")
# Force disconnect the bot session from all lobbies
lobbies_to_leave = bot_session.lobbies[:]
try:
lobbies_to_leave = bot_session.lobbies[:]
for lobby in lobbies_to_leave:
try:
await bot_session.leave_lobby(lobby)
except Exception as e:
logger.warning(f"Error removing bot from lobby {lobby.name}: {e}")
for lobby in lobbies_to_leave:
try:
await bot_session.leave_lobby(lobby)
except Exception as e:
logger.warning(f"Error removing bot from lobby {lobby.name}: {e}")
# Close WebSocket connection if it exists
if bot_session.ws:
try:
await bot_session.ws.close()
except Exception as e:
logger.warning(f"Error closing bot WebSocket: {e}")
bot_session.ws = None
# Close WebSocket connection if it exists
if bot_session.ws:
try:
await bot_session.ws.close()
except Exception as e:
logger.warning(f"Error closing bot WebSocket: {e}")
bot_session.ws = None
except Exception as e:
logger.warning(f"Error disconnecting bot session: {e}")
# Remove bot instance from tracking
with self.lock:
if request.bot_instance_id in self.bot_instances:
del self.bot_instances[request.bot_instance_id]
return BotLeaveLobbyResponse(
status="disconnected",
session_id=request.session_id,
run_id=run_id,
bot_instance_id=request.bot_instance_id,
session_id=bot_instance.session_id,
run_id=bot_instance.run_id,
)
async def get_bot_instance(self, bot_instance_id: str) -> dict:
"""Get information about a specific bot instance"""
with self.lock:
if bot_instance_id not in self.bot_instances:
raise ValueError("Bot instance not found")
bot_instance = self.bot_instances[bot_instance_id]
return bot_instance.model_dump()
def get_bot_instance_id_by_session_id(self, session_id: str) -> Optional[str]:
"""Get bot_instance_id by session_id"""
with self.lock:
for bot_instance_id, bot_instance in self.bot_instances.items():
if bot_instance.session_id == session_id:
return bot_instance_id
return None
def get_provider(self, provider_id: str) -> Optional[BotProviderModel]:
"""Get a specific bot provider by ID"""
with self.lock:

View File

@ -24,17 +24,9 @@ except ImportError:
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from shared.models import ChatMessageModel, ParticipantModel
except ImportError:
# Fallback: create minimal models for testing
from pydantic import BaseModel
from typing import Optional
class ChatMessageModel(BaseModel):
id: str
author: str
message: str
timestamp: float
class ParticipantModel(BaseModel):
id: str
name: str
raise ImportError(
f"Failed to import shared models: {e}. Ensure shared/models.py is accessible and PYTHONPATH is correctly set."
)
from logger import logger

View File

@ -27,24 +27,9 @@ except ImportError:
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from shared.models import SessionSaved, LobbySaved, SessionsPayload, NamePasswordRecord
except ImportError:
# Fallback: create minimal models for testing
from pydantic import BaseModel
class SessionSaved(BaseModel):
id: str
name: str = ""
protected: bool = False
is_bot: bool = False
has_media: bool = True
bot_run_id: Optional[str] = None
lobbies: List[str] = []
class LobbySaved(BaseModel):
name: str
private: bool = False
class SessionsPayload(BaseModel):
sessions: List[SessionSaved]
class NamePasswordRecord(BaseModel):
name: str
password: str
raise ImportError(
f"Failed to import shared models: {e}. Ensure shared/models.py is accessible and PYTHONPATH is correctly set."
)
from logger import logger
@ -106,6 +91,7 @@ class Session:
self.has_media = has_media # Whether this session provides audio/video streams
self.bot_run_id: Optional[str] = None # Bot run ID for tracking
self.bot_provider_id: Optional[str] = None # Bot provider ID
self.bot_instance_id: Optional[str] = None # Bot instance ID for tracking
self.session_lock = threading.RLock() # Instance-level lock
def getName(self) -> str:
@ -208,6 +194,24 @@ class Session:
session_name=self.name or self.short
))
def model_dump(self) -> Dict[str, Any]:
"""Convert session to dictionary format for API responses"""
with self.session_lock:
data: Dict[str, Any] = {
"id": self.id,
"name": self.name or "",
"is_bot": self.is_bot,
"has_media": self.has_media,
"created_at": self.created_at,
"last_used": self.last_used,
}
# Include bot_instance_id if this is a bot session and it has one
if self.is_bot and self.bot_instance_id:
data["bot_instance_id"] = self.bot_instance_id
return data
def to_saved(self) -> SessionSaved:
"""Convert session to saved format for persistence"""
with self.session_lock:
@ -228,6 +232,7 @@ class Session:
has_media=self.has_media,
bot_run_id=self.bot_run_id,
bot_provider_id=self.bot_provider_id,
bot_instance_id=self.bot_instance_id,
)

View File

@ -321,6 +321,7 @@ class SessionSaved(BaseModel):
has_media: bool = True # Whether this session provides audio/video streams
bot_run_id: Optional[str] = None # Bot run ID for tracking
bot_provider_id: Optional[str] = None # Bot provider ID
bot_instance_id: Optional[str] = None # Bot instance ID for tracking
class SessionsPayload(BaseModel):
@ -467,7 +468,6 @@ class BotListResponse(BaseModel):
class BotJoinLobbyRequest(BaseModel):
"""Request to make a bot join a lobby"""
bot_name: str
lobby_id: str
nick: str = ""
provider_id: Optional[str] = None # Optional: specify which provider to use
@ -487,9 +487,11 @@ class BotJoinLobbyResponse(BaseModel):
"""Response after requesting a bot to join a lobby"""
status: str
bot_name: str
bot_instance_id: str # Unique ID for this bot instance
bot_name: str # Bot type name
run_id: str
provider_id: str
session_id: str # Session ID in the lobby
class BotProviderJoinResponse(BaseModel):
@ -503,12 +505,27 @@ class BotProviderJoinResponse(BaseModel):
class BotLeaveLobbyRequest(BaseModel):
"""Request to make a bot leave a lobby"""
session_id: str # The session ID of the bot to remove
bot_instance_id: str # The unique bot instance ID (not session_id)
class BotLeaveLobbyResponse(BaseModel):
"""Response after requesting a bot to leave a lobby"""
status: str
bot_instance_id: str
session_id: str
run_id: Optional[str] = None
class BotInstanceModel(BaseModel):
"""Model representing a bot instance in a lobby"""
bot_instance_id: str
bot_name: str # Bot type name
nick: str
lobby_id: str
session_id: str
provider_id: str
run_id: str
has_media: bool
created_at: float # timestamp

View File

@ -191,7 +191,13 @@ async def check_provider_registration(server_url: str, provider_id: str, insecur
if response.status_code == 200:
data = response.json()
providers = data.get("providers", {})
return provider_id in [p.get("provider_id") for p in providers.values()]
# providers is Dict[bot_name, provider_id], so check if our provider_id is in the values
is_registered = provider_id in providers.values()
logger.debug(f"Registration check: provider_id={provider_id}, providers={providers}, is_registered={is_registered}")
return is_registered
else:
logger.warning(f"Registration check failed: HTTP {response.status_code}")
return False
except Exception as e:
logger.debug(f"Provider registration check failed: {e}")
return False

View File

@ -265,8 +265,8 @@ async def handle_chat_message(
try:
# Initialize bot instance if needed
if _bot_instance is None:
_bot_instance = EnhancedAIChatbot(chat_message.nick)
logger.info(f"Initialized enhanced AI chatbot for session: {chat_message.nick}")
_bot_instance = EnhancedAIChatbot(chat_message.sender_name)
logger.info(f"Initialized enhanced AI chatbot for session: {chat_message.sender_name}")
# Generate response
response = await _bot_instance.generate_response(chat_message.message)
@ -274,7 +274,7 @@ async def handle_chat_message(
# Send response
if response:
await send_message_func(response)
logger.info(f"AI Chatbot responded to {chat_message.nick}: {response[:100]}...")
logger.info(f"AI Chatbot responded to {chat_message.sender_name}: {response[:100]}...")
return response

View File

@ -1,477 +0,0 @@
"""Step 5B Integration: Enhanced Bot Orchestrator with Advanced Bot Management.
This module demonstrates how the new advanced bot management features integrate
with the existing bot orchestrator to provide:
1. AI Provider-powered bots with multiple backend support
2. Personality-driven bot behavior and responses
3. Conversation context and memory management
4. Dynamic bot configuration and health monitoring
This integration enhances the existing bot discovery and management system
without breaking compatibility with existing bot implementations.
"""
import os
import json
import time
import asyncio
from typing import Dict, Optional, Any
from pathlib import Path
# Import existing bot orchestrator functionality
from bot_orchestrator import discover_bots
# Import advanced bot management modules
try:
from voicebot.ai_providers import ai_provider_manager, AIProviderType
from voicebot.personality_system import personality_manager
from voicebot.conversation_context import context_manager
AI_FEATURES_AVAILABLE = True
except ImportError as e:
print(f"Warning: Advanced AI features not available: {e}")
AI_FEATURES_AVAILABLE = False
from logger import logger
class EnhancedBotOrchestrator:
"""Enhanced bot orchestrator with Step 5B advanced management features."""
def __init__(self):
self.enhanced_bots = {} # Enhanced bots with AI features
self.bot_configurations = {} # Bot-specific configurations
self.health_stats = {} # Health monitoring data
# Load configurations
self._load_bot_configurations()
# Initialize AI systems if available
if AI_FEATURES_AVAILABLE:
self._initialize_ai_systems()
def _load_bot_configurations(self):
"""Load bot configurations from JSON file."""
config_path = Path(__file__).parent / "enhanced_bot_configs.json"
default_configs = {
"ai_chatbot": {
"personality": "helpful_assistant",
"ai_provider": "openai",
"streaming": True,
"memory_enabled": True,
"advanced_features": True
},
"technical_expert": {
"personality": "technical_expert",
"ai_provider": "anthropic",
"streaming": False,
"memory_enabled": True,
"advanced_features": True
},
"creative_companion": {
"personality": "creative_companion",
"ai_provider": "local",
"streaming": True,
"memory_enabled": True,
"advanced_features": True
}
}
try:
if config_path.exists():
with open(config_path, 'r') as f:
self.bot_configurations = json.load(f)
else:
self.bot_configurations = default_configs
self._save_bot_configurations()
except Exception as e:
logger.error(f"Failed to load bot configurations: {e}")
self.bot_configurations = default_configs
def _save_bot_configurations(self):
"""Save bot configurations to JSON file."""
config_path = Path(__file__).parent / "enhanced_bot_configs.json"
try:
with open(config_path, 'w') as f:
json.dump(self.bot_configurations, f, indent=2)
except Exception as e:
logger.error(f"Failed to save bot configurations: {e}")
def _initialize_ai_systems(self):
"""Initialize AI provider and personality systems."""
try:
# Ensure default personality templates are loaded
personality_manager.ensure_default_templates()
# Register available AI providers based on environment
providers_to_init = []
if os.getenv("OPENAI_API_KEY"):
providers_to_init.append(AIProviderType.OPENAI)
if os.getenv("ANTHROPIC_API_KEY"):
providers_to_init.append(AIProviderType.ANTHROPIC)
# Local provider is always available
providers_to_init.append(AIProviderType.LOCAL)
for provider_type in providers_to_init:
try:
provider = ai_provider_manager.create_provider(provider_type)
ai_provider_manager.register_provider(f"system_{provider_type.value}", provider)
logger.info(f"Initialized AI provider: {provider_type.value}")
except Exception as e:
logger.warning(f"Failed to initialize provider {provider_type.value}: {e}")
logger.info("AI systems initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize AI systems: {e}")
async def discover_enhanced_bots(self) -> Dict[str, Dict[str, Any]]:
"""Discover bots with enhanced information about AI capabilities."""
# Start with standard bot discovery
standard_bots = discover_bots() # Returns List[BotInfoModel]
enhanced_bot_info = {}
# Convert BotInfoModel list to dict and enhance with AI capabilities
for bot_info in standard_bots:
bot_name = bot_info.name
bot_info_dict = {
"name": bot_name,
"description": bot_info.description,
"has_media": bot_info.has_media,
"standard_info": {
"name": bot_name,
"description": bot_info.description,
"has_media": bot_info.has_media
},
"enhanced_features": False,
"ai_capabilities": {},
"health_status": "unknown"
}
# Check if bot supports enhanced features
if bot_name in self.bot_configurations:
config = self.bot_configurations[bot_name]
if config.get("advanced_features", False):
bot_info_dict["enhanced_features"] = True
bot_info_dict["ai_capabilities"] = {
"personality": config.get("personality", "default"),
"ai_provider": config.get("ai_provider", "local"),
"streaming": config.get("streaming", False),
"memory_enabled": config.get("memory_enabled", False)
}
# Check bot health if it supports it (would need to import the bot module)
try:
bot_module_path = f"voicebot.bots.{bot_name}"
bot_module = __import__(bot_module_path, fromlist=[bot_name])
if hasattr(bot_module, 'get_bot_status'):
status = await bot_module.get_bot_status()
bot_info_dict["health_status"] = "healthy"
bot_info_dict["detailed_status"] = status
except Exception as e:
bot_info_dict["health_status"] = f"import_error: {e}"
enhanced_bot_info[bot_name] = bot_info_dict
return enhanced_bot_info
async def create_enhanced_bot_instance(self, bot_name: str, session_name: str) -> Optional[Any]:
"""Create an enhanced bot instance with AI features configured."""
if not AI_FEATURES_AVAILABLE:
logger.warning(f"Cannot create enhanced bot {bot_name} - AI features not available")
return None
if bot_name not in self.bot_configurations:
logger.warning(f"No configuration found for enhanced bot: {bot_name}")
return None
config = self.bot_configurations[bot_name]
try:
# Set environment variables for the bot based on configuration
os.environ[f"{bot_name.upper()}_PERSONALITY"] = config.get("personality", "helpful_assistant")
os.environ[f"{bot_name.upper()}_PROVIDER"] = config.get("ai_provider", "local")
os.environ[f"{bot_name.upper()}_STREAMING"] = str(config.get("streaming", False)).lower()
os.environ[f"{bot_name.upper()}_MEMORY"] = str(config.get("memory_enabled", False)).lower()
# Import and create the bot
bot_module_path = f"voicebot.bots.{bot_name}"
bot_module = __import__(bot_module_path, fromlist=[bot_name])
# If the bot has a specific initialization function, use it
if hasattr(bot_module, 'create_enhanced_instance'):
bot_instance = await bot_module.create_enhanced_instance(session_name, config)
else:
# Create standard bot instance
bot_instance = bot_module
self.enhanced_bots[f"{bot_name}_{session_name}"] = {
"instance": bot_instance,
"config": config,
"session": session_name,
"created_at": time.time()
}
logger.info(f"Created enhanced bot instance: {bot_name} for session {session_name}")
return bot_instance
except Exception as e:
logger.error(f"Failed to create enhanced bot instance {bot_name}: {e}")
return None
async def monitor_bot_health(self) -> Dict[str, Any]:
"""Monitor health of all enhanced bots and AI systems."""
health_report = {
"timestamp": time.time(),
"ai_systems_available": AI_FEATURES_AVAILABLE,
"enhanced_bots": {},
"ai_providers": {},
"personality_system": {},
"conversation_contexts": {}
}
if not AI_FEATURES_AVAILABLE:
health_report["status"] = "limited - AI features disabled"
return health_report
try:
# Check AI providers
for provider_id, provider in ai_provider_manager.list_providers().items():
try:
provider_instance = ai_provider_manager.get_provider(provider_id)
if provider_instance:
is_healthy = await provider_instance.health_check()
health_report["ai_providers"][provider_id] = {
"status": "healthy" if is_healthy else "unhealthy",
"type": provider.value if hasattr(provider, 'value') else str(provider)
}
except Exception as e:
health_report["ai_providers"][provider_id] = {
"status": f"error: {e}",
"type": "unknown"
}
# Check personality system
try:
templates = personality_manager.list_templates()
health_report["personality_system"] = {
"status": "healthy",
"available_templates": len(templates),
"template_ids": [t.id for t in templates]
}
except Exception as e:
health_report["personality_system"] = {
"status": f"error: {e}"
}
# Check conversation context system
try:
context_stats = context_manager.get_statistics()
health_report["conversation_contexts"] = {
"status": "healthy",
"statistics": context_stats
}
except Exception as e:
health_report["conversation_contexts"] = {
"status": f"error: {e}"
}
# Check enhanced bot instances
for bot_key, bot_data in self.enhanced_bots.items():
try:
bot_instance = bot_data["instance"]
if hasattr(bot_instance, 'health_check'):
bot_health = await bot_instance.health_check()
health_report["enhanced_bots"][bot_key] = {
"status": "healthy",
"details": bot_health,
"uptime": time.time() - bot_data["created_at"]
}
else:
health_report["enhanced_bots"][bot_key] = {
"status": "unknown - no health check available",
"uptime": time.time() - bot_data["created_at"]
}
except Exception as e:
health_report["enhanced_bots"][bot_key] = {
"status": f"error: {e}",
"uptime": time.time() - bot_data.get("created_at", time.time())
}
health_report["status"] = "operational"
except Exception as e:
health_report["status"] = f"system_error: {e}"
# Store health stats for trending
self.health_stats[int(time.time())] = health_report
# Keep only last 24 hours of health stats
cutoff_time = time.time() - (24 * 60 * 60)
self.health_stats = {
timestamp: stats for timestamp, stats in self.health_stats.items()
if timestamp > cutoff_time
}
return health_report
async def configure_bot_runtime(self, bot_name: str, new_config: Dict[str, Any]) -> bool:
"""Dynamically reconfigure a bot at runtime."""
if bot_name not in self.bot_configurations:
logger.error(f"Bot {bot_name} not found in configurations")
return False
try:
# Update configuration
old_config = self.bot_configurations[bot_name].copy()
self.bot_configurations[bot_name].update(new_config)
# Save updated configuration
self._save_bot_configurations()
# If there are active instances, try to update them
updated_instances = []
for bot_key, bot_data in self.enhanced_bots.items():
if bot_key.startswith(f"{bot_name}_"):
bot_instance = bot_data["instance"]
# Try to update personality if changed
if "personality" in new_config and hasattr(bot_instance, 'switch_personality'):
success = await bot_instance.switch_personality(new_config["personality"])
if success:
updated_instances.append(f"{bot_key} personality")
# Try to update AI provider if changed
if "ai_provider" in new_config and hasattr(bot_instance, 'switch_ai_provider'):
success = await bot_instance.switch_ai_provider(new_config["ai_provider"])
if success:
updated_instances.append(f"{bot_key} provider")
# Update bot data configuration
bot_data["config"] = self.bot_configurations[bot_name]
logger.info(f"Bot {bot_name} configuration updated. Active instances updated: {updated_instances}")
return True
except Exception as e:
# Rollback configuration on error
self.bot_configurations[bot_name] = old_config
logger.error(f"Failed to configure bot {bot_name}: {e}")
return False
def get_bot_analytics(self) -> Dict[str, Any]:
"""Get analytics and usage statistics for enhanced bots."""
analytics = {
"enhanced_bots_count": len(self.enhanced_bots),
"configurations_count": len(self.bot_configurations),
"health_history_points": len(self.health_stats),
"bot_breakdown": {},
"feature_usage": {
"ai_providers": {},
"personalities": {},
"memory_enabled": 0,
"streaming_enabled": 0
}
}
# Analyze bot configurations
for bot_name, config in self.bot_configurations.items():
analytics["bot_breakdown"][bot_name] = {
"enhanced_features": config.get("advanced_features", False),
"ai_provider": config.get("ai_provider", "none"),
"personality": config.get("personality", "none"),
"active_instances": sum(1 for key in self.enhanced_bots.keys() if key.startswith(f"{bot_name}_"))
}
# Count feature usage
provider = config.get("ai_provider", "none")
analytics["feature_usage"]["ai_providers"][provider] = analytics["feature_usage"]["ai_providers"].get(provider, 0) + 1
personality = config.get("personality", "none")
analytics["feature_usage"]["personalities"][personality] = analytics["feature_usage"]["personalities"].get(personality, 0) + 1
if config.get("memory_enabled", False):
analytics["feature_usage"]["memory_enabled"] += 1
if config.get("streaming", False):
analytics["feature_usage"]["streaming_enabled"] += 1
# Add conversation context statistics if available
if AI_FEATURES_AVAILABLE:
try:
context_stats = context_manager.get_statistics()
analytics["conversation_statistics"] = context_stats
except Exception as e:
analytics["conversation_statistics"] = {"error": str(e)}
return analytics
# Global enhanced orchestrator instance
enhanced_orchestrator = EnhancedBotOrchestrator()
async def demo_step_5b_integration():
"""Demonstrate Step 5B integration capabilities."""
print("=== Step 5B Advanced Bot Management Demo ===\n")
# 1. Discover enhanced bots
print("1. Discovering bots with enhanced capabilities...")
enhanced_bots = await enhanced_orchestrator.discover_enhanced_bots()
for bot_name, info in enhanced_bots.items():
print(f" Bot: {bot_name}")
print(f" Enhanced: {info['enhanced_features']}")
if info['enhanced_features']:
print(f" AI Capabilities: {info['ai_capabilities']}")
print(f" Health: {info['health_status']}")
print()
# 2. Create enhanced bot instance
print("2. Creating enhanced AI chatbot instance...")
bot_instance = await enhanced_orchestrator.create_enhanced_bot_instance("ai_chatbot", "demo_session")
if bot_instance:
print(" ✓ Enhanced AI chatbot created successfully")
else:
print(" ✗ Failed to create enhanced bot")
print()
# 3. Monitor system health
print("3. Monitoring system health...")
health_report = await enhanced_orchestrator.monitor_bot_health()
print(f" System Status: {health_report['status']}")
print(f" AI Features Available: {health_report['ai_systems_available']}")
if health_report['ai_systems_available']:
print(f" AI Providers: {len(health_report['ai_providers'])} registered")
print(f" Personality Templates: {health_report['personality_system'].get('available_templates', 0)}")
print(f" Enhanced Bot Instances: {len(health_report['enhanced_bots'])}")
print()
# 4. Runtime configuration
print("4. Demonstrating runtime configuration...")
config_success = await enhanced_orchestrator.configure_bot_runtime("ai_chatbot", {
"personality": "technical_expert",
"streaming": False
})
print(f" Configuration Update: {'✓ Success' if config_success else '✗ Failed'}")
print()
# 5. Analytics
print("5. Bot analytics and usage statistics...")
analytics = enhanced_orchestrator.get_bot_analytics()
print(f" Enhanced Bots: {analytics['enhanced_bots_count']}")
print(f" Configurations: {analytics['configurations_count']}")
print(" Feature Usage:")
for feature, usage in analytics['feature_usage'].items():
print(f" {feature}: {usage}")
print()
print("=== Step 5B Integration Demo Complete ===")
if __name__ == "__main__":
# Run the demo
asyncio.run(demo_step_5b_integration())