Lots of tweaks; in progress is_bot => bot_instance_id
This commit is contained in:
parent
71555c5230
commit
4b33b40637
@ -179,7 +179,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
|
|||||||
switch (param.type) {
|
switch (param.type) {
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div className="config-parameter">
|
||||||
<label className="config-label">
|
<label className="config-label">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -194,7 +194,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
|
|||||||
|
|
||||||
case "select":
|
case "select":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
<select
|
<select
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
@ -213,7 +213,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
|
|||||||
|
|
||||||
case "range":
|
case "range":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
<div className="range-container">
|
<div className="range-container">
|
||||||
<input
|
<input
|
||||||
@ -233,7 +233,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
|
|||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -251,7 +251,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
|
|||||||
case "string":
|
case "string":
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -275,15 +275,15 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
|
|||||||
<div key={category.name} className="config-category">
|
<div key={category.name} className="config-category">
|
||||||
<h4 className="category-title">{category.name}</h4>
|
<h4 className="category-title">{category.name}</h4>
|
||||||
<div className="category-parameters">
|
<div className="category-parameters">
|
||||||
{category.parameters.map((paramName) => {
|
{category.parameters?.map((paramName) => {
|
||||||
const param = schema.parameters.find((p) => p.name === paramName);
|
const param = schema.parameters?.find((p) => p.name === paramName);
|
||||||
return param ? renderParameter(param) : null;
|
return param ? <div key={paramName}>{renderParameter(param)}</div> : null;
|
||||||
})}
|
}) || null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
return schema.parameters.map(renderParameter);
|
return schema.parameters?.map((param) => <div key={param.name}>{renderParameter(param)}</div>) || null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,7 +78,6 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
setAddingBot(true);
|
setAddingBot(true);
|
||||||
try {
|
try {
|
||||||
const request: BotJoinLobbyRequest = {
|
const request: BotJoinLobbyRequest = {
|
||||||
bot_name: selectedBot,
|
|
||||||
lobby_id: lobbyId,
|
lobby_id: lobbyId,
|
||||||
nick: botNick || `${selectedBot}-bot`,
|
nick: botNick || `${selectedBot}-bot`,
|
||||||
provider_id: providers[selectedBot],
|
provider_id: providers[selectedBot],
|
||||||
|
@ -26,6 +26,7 @@ type User = {
|
|||||||
has_media?: boolean; // Whether this user provides audio/video streams
|
has_media?: boolean; // Whether this user provides audio/video streams
|
||||||
bot_run_id?: string;
|
bot_run_id?: string;
|
||||||
bot_provider_id?: string;
|
bot_provider_id?: string;
|
||||||
|
bot_instance_id?: string; // For bot instances
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserListProps = {
|
type UserListProps = {
|
||||||
@ -48,16 +49,12 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
const apiClient = new ApiClient();
|
const apiClient = new ApiClient();
|
||||||
|
|
||||||
const handleBotLeave = async (user: User) => {
|
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));
|
setLeavingBots((prev) => new Set(prev).add(user.session_id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request: BotLeaveLobbyRequest = {
|
await apiClient.requestBotLeaveLobby(user.bot_instance_id);
|
||||||
session_id: user.session_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
await apiClient.requestBotLeaveLobby(request);
|
|
||||||
console.log(`Bot ${user.name} leave requested successfully`);
|
console.log(`Bot ${user.name} leave requested successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to request bot leave:", error);
|
console.error("Failed to request bot leave:", error);
|
||||||
@ -156,12 +153,12 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
{users?.map((user) => (
|
{users?.map((user) => (
|
||||||
<Box
|
<Box
|
||||||
key={user.session_id}
|
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" : ""}`}
|
className={`UserEntry ${user.local ? "UserSelf" : ""}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<Box style={{ display: "flex-wrap", alignItems: "center" }}>
|
||||||
<div className="Name">{user.name ? user.name : user.session_id}</div>
|
<div className="Name">{user.name ? user.name : user.session_id}</div>
|
||||||
{user.protected && (
|
{user.protected && (
|
||||||
<div
|
<div
|
||||||
@ -176,9 +173,9 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
🤖
|
🤖
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
{user.is_bot && !user.local && (
|
{user.is_bot && (
|
||||||
<div style={{ display: "flex", gap: "4px" }}>
|
<Box style={{ display: "flex-wrap", gap: "4px", border: "3px solid magenta" }}>
|
||||||
{user.bot_run_id && (
|
{user.bot_run_id && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@ -194,14 +191,15 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() => handleBotLeave(user)}
|
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" }}
|
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"}
|
{leavingBots.has(user.session_id) ? "..." : "Leave"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
{user.name && !user.live && <div className="NoNetwork"></div>}
|
{user.name && !user.live && <div className="NoNetwork"></div>}
|
||||||
</div>
|
</div>
|
||||||
{user.name && user.live && peers[user.session_id] && (user.local || user.has_media !== false) ? (
|
{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>
|
<DialogContent>
|
||||||
{selectedBotForConfig && (
|
{selectedBotForConfig && (
|
||||||
<BotConfig
|
<BotConfig
|
||||||
botName={selectedBotForConfig.name || "unknown"}
|
botName={selectedBotForConfig.name?.replace(/-bot$/, "") || "unknown"}
|
||||||
lobbyId={lobbyId}
|
lobbyId={lobbyId}
|
||||||
onConfigUpdate={(config) => {
|
onConfigUpdate={(config) => {
|
||||||
console.log("Bot configuration updated:", config);
|
console.log("Bot configuration updated:", config);
|
||||||
|
@ -46,7 +46,6 @@ export interface BotListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BotJoinLobbyRequest {
|
export interface BotJoinLobbyRequest {
|
||||||
bot_name: string;
|
|
||||||
lobby_id: string;
|
lobby_id: string;
|
||||||
nick?: string;
|
nick?: string;
|
||||||
provider_id?: string;
|
provider_id?: string;
|
||||||
@ -54,9 +53,34 @@ export interface BotJoinLobbyRequest {
|
|||||||
|
|
||||||
export interface BotJoinLobbyResponse {
|
export interface BotJoinLobbyResponse {
|
||||||
status: string;
|
status: string;
|
||||||
|
bot_instance_id: string;
|
||||||
bot_name: string;
|
bot_name: string;
|
||||||
run_id: string;
|
run_id: string;
|
||||||
provider_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 {
|
export interface BotLeaveLobbyRequest {
|
||||||
@ -212,11 +236,13 @@ export class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestBotLeaveLobby(request: BotLeaveLobbyRequest): Promise<BotLeaveLobbyResponse> {
|
async requestBotLeaveLobby(botInstanceId: string): Promise<BotLeaveLobbyResponse> {
|
||||||
return this.request<BotLeaveLobbyResponse>(this.getApiPath("/ai-voicebot/api/bots/leave"), {
|
return this.request<BotLeaveLobbyResponse>(
|
||||||
method: "POST",
|
this.getApiPath(`/ai-voicebot/api/bots/instances/${encodeURIComponent(botInstanceId)}/leave`),
|
||||||
body: request,
|
{
|
||||||
});
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generated endpoints will be added here by update-api-client.js
|
// Auto-generated endpoints will be added here by update-api-client.js
|
||||||
|
@ -68,7 +68,6 @@ export class AdvancedApiEvolutionChecker {
|
|||||||
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
||||||
'GET:/ai-voicebot/api/bots/providers',
|
'GET:/ai-voicebot/api/bots/providers',
|
||||||
'GET:/ai-voicebot/api/bots',
|
'GET:/ai-voicebot/api/bots',
|
||||||
'POST:/ai-voicebot/api/bots/leave',
|
|
||||||
'POST:/ai-voicebot/api/lobby/{session_id}'
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
"""Bot API endpoints"""
|
"""Bot API endpoints"""
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from logger import logger
|
|
||||||
|
|
||||||
# Import shared models with fallback handling
|
# Import shared models - NO FALLBACKS!
|
||||||
try:
|
try:
|
||||||
from ...shared.models import (
|
from shared.models import (
|
||||||
BotProviderRegisterRequest,
|
BotProviderRegisterRequest,
|
||||||
BotProviderRegisterResponse,
|
BotProviderRegisterResponse,
|
||||||
BotProviderListResponse,
|
BotProviderListResponse,
|
||||||
@ -13,58 +12,12 @@ try:
|
|||||||
BotJoinLobbyResponse,
|
BotJoinLobbyResponse,
|
||||||
BotLeaveLobbyRequest,
|
BotLeaveLobbyRequest,
|
||||||
BotLeaveLobbyResponse,
|
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):
|
def create_bot_router(bot_manager, session_manager, lobby_manager):
|
||||||
@ -108,10 +61,13 @@ def create_bot_router(bot_manager, session_manager, lobby_manager):
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.post("/leave", response_model=BotLeaveLobbyResponse)
|
@router.post(
|
||||||
async def request_bot_leave_lobby(request: BotLeaveLobbyRequest) -> BotLeaveLobbyResponse:
|
"/instances/{bot_instance_id}/leave", response_model=BotLeaveLobbyResponse
|
||||||
"""Request a bot to leave from all lobbies and disconnect"""
|
)
|
||||||
|
async def request_bot_leave_lobby(bot_instance_id: str) -> BotLeaveLobbyResponse:
|
||||||
|
"""Request a bot instance to leave from all lobbies and disconnect"""
|
||||||
try:
|
try:
|
||||||
|
request = BotLeaveLobbyRequest(bot_instance_id=bot_instance_id)
|
||||||
return await bot_manager.request_bot_leave(request, session_manager)
|
return await bot_manager.request_bot_leave(request, session_manager)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if "not found" in str(e).lower():
|
if "not found" in str(e).lower():
|
||||||
@ -121,4 +77,15 @@ def create_bot_router(bot_manager, session_manager, lobby_manager):
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
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
|
return router
|
||||||
|
@ -16,46 +16,14 @@ from pathlib import Path
|
|||||||
|
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
# Import shared models with fallback handling
|
# Import shared models
|
||||||
try:
|
import sys
|
||||||
from ...shared.models import (
|
import os
|
||||||
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
|
|
||||||
|
|
||||||
|
sys.path.append(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
)
|
||||||
|
from shared.models import BotConfigSchema, BotLobbyConfig
|
||||||
|
|
||||||
class BotConfigManager:
|
class BotConfigManager:
|
||||||
"""Manages bot configurations for lobbies"""
|
"""Manages bot configurations for lobbies"""
|
||||||
|
@ -9,52 +9,28 @@ from typing import Dict, List, Optional
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
# Import shared models with fallback handling
|
# Import shared models
|
||||||
try:
|
import sys
|
||||||
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
|
|
||||||
|
|
||||||
warnings.warn(
|
sys.path.append(
|
||||||
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
)
|
)
|
||||||
# Rely on environment setup or raise a clear error
|
from shared.models import (
|
||||||
raise ImportError(
|
BotProviderModel,
|
||||||
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
|
BotProviderRegisterRequest,
|
||||||
)
|
BotProviderRegisterResponse,
|
||||||
|
BotProviderListResponse,
|
||||||
|
BotListResponse,
|
||||||
|
BotInfoModel,
|
||||||
|
BotJoinLobbyRequest,
|
||||||
|
BotJoinLobbyResponse,
|
||||||
|
BotLeaveLobbyRequest,
|
||||||
|
BotLeaveLobbyResponse,
|
||||||
|
BotProviderBotsResponse,
|
||||||
|
BotProviderJoinResponse,
|
||||||
|
BotJoinPayload,
|
||||||
|
BotInstanceModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BotProviderConfig:
|
class BotProviderConfig:
|
||||||
@ -94,6 +70,9 @@ class BotManager:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bot_providers: Dict[str, BotProviderModel] = {}
|
self.bot_providers: Dict[str, BotProviderModel] = {}
|
||||||
|
self.bot_instances: Dict[
|
||||||
|
str, BotInstanceModel
|
||||||
|
] = {} # bot_instance_id -> BotInstanceModel
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
# Check if provider authentication is enabled
|
# Check if provider authentication is enabled
|
||||||
@ -311,13 +290,37 @@ class BotManager:
|
|||||||
bot_session.bot_run_id = run_id
|
bot_session.bot_run_id = run_id
|
||||||
bot_session.bot_provider_id = target_provider_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(
|
return BotJoinLobbyResponse(
|
||||||
status="requested",
|
status="requested",
|
||||||
|
bot_instance_id=bot_instance_id,
|
||||||
bot_name=bot_name,
|
bot_name=bot_name,
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
provider_id=target_provider_id,
|
provider_id=target_provider_id,
|
||||||
|
session_id=bot_session_id,
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Invalid response from bot provider: {e}")
|
logger.error(f"Invalid response from bot provider: {e}")
|
||||||
@ -335,60 +338,97 @@ class BotManager:
|
|||||||
async def request_bot_leave(self, request: BotLeaveLobbyRequest, session_manager) -> BotLeaveLobbyResponse:
|
async def request_bot_leave(self, request: BotLeaveLobbyRequest, session_manager) -> BotLeaveLobbyResponse:
|
||||||
"""Request a bot to leave from all lobbies and disconnect"""
|
"""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
|
# 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:
|
if not bot_session:
|
||||||
raise ValueError("Bot session not found")
|
raise ValueError("Bot session not found")
|
||||||
|
|
||||||
if not bot_session.is_bot:
|
if not bot_session.is_bot:
|
||||||
raise ValueError("Session is not a bot")
|
raise ValueError("Session is not a bot")
|
||||||
|
|
||||||
run_id = bot_session.bot_run_id
|
logger.info(
|
||||||
provider_id = bot_session.bot_provider_id
|
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
|
||||||
|
try:
|
||||||
# Try to stop the bot at the provider level if we have the information
|
|
||||||
if provider_id and run_id:
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if provider_id in self.bot_providers:
|
if bot_instance.provider_id in self.bot_providers:
|
||||||
provider = self.bot_providers[provider_id]
|
provider = self.bot_providers[bot_instance.provider_id]
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
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,
|
timeout=5.0,
|
||||||
)
|
)
|
||||||
if response.status_code == 200:
|
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:
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to request bot stop from provider: {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
|
# 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:
|
for lobby in lobbies_to_leave:
|
||||||
try:
|
try:
|
||||||
await bot_session.leave_lobby(lobby)
|
await bot_session.leave_lobby(lobby)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error removing bot from lobby {lobby.name}: {e}")
|
logger.warning(f"Error removing bot from lobby {lobby.name}: {e}")
|
||||||
|
|
||||||
# Close WebSocket connection if it exists
|
# Close WebSocket connection if it exists
|
||||||
if bot_session.ws:
|
if bot_session.ws:
|
||||||
try:
|
try:
|
||||||
await bot_session.ws.close()
|
await bot_session.ws.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error closing bot WebSocket: {e}")
|
logger.warning(f"Error closing bot WebSocket: {e}")
|
||||||
bot_session.ws = None
|
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(
|
return BotLeaveLobbyResponse(
|
||||||
status="disconnected",
|
status="disconnected",
|
||||||
session_id=request.session_id,
|
bot_instance_id=request.bot_instance_id,
|
||||||
run_id=run_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]:
|
def get_provider(self, provider_id: str) -> Optional[BotProviderModel]:
|
||||||
"""Get a specific bot provider by ID"""
|
"""Get a specific bot provider by ID"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -24,17 +24,9 @@ except ImportError:
|
|||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
from shared.models import ChatMessageModel, ParticipantModel
|
from shared.models import ChatMessageModel, ParticipantModel
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback: create minimal models for testing
|
raise ImportError(
|
||||||
from pydantic import BaseModel
|
f"Failed to import shared models: {e}. Ensure shared/models.py is accessible and PYTHONPATH is correctly set."
|
||||||
from typing import Optional
|
)
|
||||||
class ChatMessageModel(BaseModel):
|
|
||||||
id: str
|
|
||||||
author: str
|
|
||||||
message: str
|
|
||||||
timestamp: float
|
|
||||||
class ParticipantModel(BaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
|
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
|
@ -27,24 +27,9 @@ except ImportError:
|
|||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
from shared.models import SessionSaved, LobbySaved, SessionsPayload, NamePasswordRecord
|
from shared.models import SessionSaved, LobbySaved, SessionsPayload, NamePasswordRecord
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback: create minimal models for testing
|
raise ImportError(
|
||||||
from pydantic import BaseModel
|
f"Failed to import shared models: {e}. Ensure shared/models.py is accessible and PYTHONPATH is correctly set."
|
||||||
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
|
|
||||||
|
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
@ -106,6 +91,7 @@ class Session:
|
|||||||
self.has_media = has_media # Whether this session provides audio/video streams
|
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_run_id: Optional[str] = None # Bot run ID for tracking
|
||||||
self.bot_provider_id: Optional[str] = None # Bot provider ID
|
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
|
self.session_lock = threading.RLock() # Instance-level lock
|
||||||
|
|
||||||
def getName(self) -> str:
|
def getName(self) -> str:
|
||||||
@ -208,6 +194,24 @@ class Session:
|
|||||||
session_name=self.name or self.short
|
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:
|
def to_saved(self) -> SessionSaved:
|
||||||
"""Convert session to saved format for persistence"""
|
"""Convert session to saved format for persistence"""
|
||||||
with self.session_lock:
|
with self.session_lock:
|
||||||
@ -228,6 +232,7 @@ class Session:
|
|||||||
has_media=self.has_media,
|
has_media=self.has_media,
|
||||||
bot_run_id=self.bot_run_id,
|
bot_run_id=self.bot_run_id,
|
||||||
bot_provider_id=self.bot_provider_id,
|
bot_provider_id=self.bot_provider_id,
|
||||||
|
bot_instance_id=self.bot_instance_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -321,6 +321,7 @@ class SessionSaved(BaseModel):
|
|||||||
has_media: bool = True # Whether this session provides audio/video streams
|
has_media: bool = True # Whether this session provides audio/video streams
|
||||||
bot_run_id: Optional[str] = None # Bot run ID for tracking
|
bot_run_id: Optional[str] = None # Bot run ID for tracking
|
||||||
bot_provider_id: Optional[str] = None # Bot provider ID
|
bot_provider_id: Optional[str] = None # Bot provider ID
|
||||||
|
bot_instance_id: Optional[str] = None # Bot instance ID for tracking
|
||||||
|
|
||||||
|
|
||||||
class SessionsPayload(BaseModel):
|
class SessionsPayload(BaseModel):
|
||||||
@ -467,7 +468,6 @@ class BotListResponse(BaseModel):
|
|||||||
class BotJoinLobbyRequest(BaseModel):
|
class BotJoinLobbyRequest(BaseModel):
|
||||||
"""Request to make a bot join a lobby"""
|
"""Request to make a bot join a lobby"""
|
||||||
|
|
||||||
bot_name: str
|
|
||||||
lobby_id: str
|
lobby_id: str
|
||||||
nick: str = ""
|
nick: str = ""
|
||||||
provider_id: Optional[str] = None # Optional: specify which provider to use
|
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"""
|
"""Response after requesting a bot to join a lobby"""
|
||||||
|
|
||||||
status: str
|
status: str
|
||||||
bot_name: str
|
bot_instance_id: str # Unique ID for this bot instance
|
||||||
|
bot_name: str # Bot type name
|
||||||
run_id: str
|
run_id: str
|
||||||
provider_id: str
|
provider_id: str
|
||||||
|
session_id: str # Session ID in the lobby
|
||||||
|
|
||||||
|
|
||||||
class BotProviderJoinResponse(BaseModel):
|
class BotProviderJoinResponse(BaseModel):
|
||||||
@ -503,12 +505,27 @@ class BotProviderJoinResponse(BaseModel):
|
|||||||
class BotLeaveLobbyRequest(BaseModel):
|
class BotLeaveLobbyRequest(BaseModel):
|
||||||
"""Request to make a bot leave a lobby"""
|
"""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):
|
class BotLeaveLobbyResponse(BaseModel):
|
||||||
"""Response after requesting a bot to leave a lobby"""
|
"""Response after requesting a bot to leave a lobby"""
|
||||||
|
|
||||||
status: str
|
status: str
|
||||||
|
bot_instance_id: str
|
||||||
session_id: str
|
session_id: str
|
||||||
run_id: Optional[str] = None
|
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
|
||||||
|
@ -191,7 +191,13 @@ async def check_provider_registration(server_url: str, provider_id: str, insecur
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
providers = data.get("providers", {})
|
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:
|
except Exception as e:
|
||||||
logger.debug(f"Provider registration check failed: {e}")
|
logger.debug(f"Provider registration check failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
@ -265,8 +265,8 @@ async def handle_chat_message(
|
|||||||
try:
|
try:
|
||||||
# Initialize bot instance if needed
|
# Initialize bot instance if needed
|
||||||
if _bot_instance is None:
|
if _bot_instance is None:
|
||||||
_bot_instance = EnhancedAIChatbot(chat_message.nick)
|
_bot_instance = EnhancedAIChatbot(chat_message.sender_name)
|
||||||
logger.info(f"Initialized enhanced AI chatbot for session: {chat_message.nick}")
|
logger.info(f"Initialized enhanced AI chatbot for session: {chat_message.sender_name}")
|
||||||
|
|
||||||
# Generate response
|
# Generate response
|
||||||
response = await _bot_instance.generate_response(chat_message.message)
|
response = await _bot_instance.generate_response(chat_message.message)
|
||||||
@ -274,7 +274,7 @@ async def handle_chat_message(
|
|||||||
# Send response
|
# Send response
|
||||||
if response:
|
if response:
|
||||||
await send_message_func(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
|
return response
|
||||||
|
|
||||||
|
@ -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())
|
|
Loading…
x
Reference in New Issue
Block a user