Improving bot configability
This commit is contained in:
parent
15641aa542
commit
095cca785d
242
client/src/BotConfig.css
Normal file
242
client/src/BotConfig.css
Normal file
@ -0,0 +1,242 @@
|
||||
/* Bot Configuration Component Styles */
|
||||
|
||||
.bot-config-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bot-config-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bot-config-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #212529;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.bot-config-header p {
|
||||
margin: 4px 0;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.config-meta {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bot-config-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-category {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
margin: 0 0 16px 0;
|
||||
padding: 8px 12px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
color: #495057;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-parameters {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.config-parameter {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.config-label input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.config-description {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.config-input,
|
||||
.config-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.config-input:focus,
|
||||
.config-select:focus {
|
||||
outline: none;
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.range-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.config-range {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #dee2e6;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.config-range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.config-range::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.range-value {
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
background: #f8f9fa;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bot-config-actions {
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.config-save-button {
|
||||
padding: 12px 24px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.config-save-button:hover:not(:disabled) {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.config-save-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bot-config-loading,
|
||||
.bot-config-error,
|
||||
.bot-config-unavailable {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.bot-config-loading {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.bot-config-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.bot-config-unavailable {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.bot-config-container {
|
||||
margin: 0 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-parameters {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.config-parameter {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.range-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.range-value {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for smooth transitions */
|
||||
.config-parameter {
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.config-parameter:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Custom checkbox styling */
|
||||
.config-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #007bff;
|
||||
}
|
314
client/src/BotConfig.tsx
Normal file
314
client/src/BotConfig.tsx
Normal file
@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Bot Configuration Component
|
||||
*
|
||||
* This component provides a UI for configuring bot settings per lobby.
|
||||
* It fetches the configuration schema from the bot and renders appropriate
|
||||
* form controls for each configuration parameter.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './BotConfig.css';
|
||||
|
||||
interface ConfigParameter {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'select' | 'range';
|
||||
label: string;
|
||||
description: string;
|
||||
default_value?: any;
|
||||
required?: boolean;
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
step?: number;
|
||||
max_length?: number;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
interface ConfigSchema {
|
||||
bot_name: string;
|
||||
version: string;
|
||||
parameters: ConfigParameter[];
|
||||
categories?: Array<{ name: string; parameters: string[] }>;
|
||||
}
|
||||
|
||||
interface BotConfig {
|
||||
bot_name: string;
|
||||
lobby_id: string;
|
||||
provider_id: string;
|
||||
config_values: { [key: string]: any };
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
interface BotConfigProps {
|
||||
botName: string;
|
||||
lobbyId: string;
|
||||
onConfigUpdate?: (config: BotConfig) => void;
|
||||
}
|
||||
|
||||
const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
botName,
|
||||
lobbyId,
|
||||
onConfigUpdate
|
||||
}) => {
|
||||
const [schema, setSchema] = useState<ConfigSchema | null>(null);
|
||||
const [currentConfig, setCurrentConfig] = useState<BotConfig | null>(null);
|
||||
const [configValues, setConfigValues] = useState<{ [key: string]: any }>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Fetch configuration schema
|
||||
useEffect(() => {
|
||||
const fetchSchema = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/bots/config/schema/${botName}`);
|
||||
|
||||
if (response.ok) {
|
||||
const schemaData = await response.json();
|
||||
setSchema(schemaData);
|
||||
|
||||
// Initialize config values with defaults
|
||||
const defaultValues: { [key: string]: any } = {};
|
||||
schemaData.parameters.forEach((param: ConfigParameter) => {
|
||||
if (param.default_value !== undefined) {
|
||||
defaultValues[param.name] = param.default_value;
|
||||
}
|
||||
});
|
||||
setConfigValues(defaultValues);
|
||||
} else if (response.status === 404) {
|
||||
setError(`Bot "${botName}" does not support configuration`);
|
||||
} else {
|
||||
setError('Failed to fetch configuration schema');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Network error while fetching configuration schema');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSchema();
|
||||
}, [botName]);
|
||||
|
||||
// Fetch current configuration
|
||||
useEffect(() => {
|
||||
const fetchCurrentConfig = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/bots/config/lobby/${lobbyId}/bot/${botName}`);
|
||||
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
setCurrentConfig(config);
|
||||
setConfigValues({ ...configValues, ...config.config_values });
|
||||
}
|
||||
// If 404, no existing config - that's fine
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch current config:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (schema) {
|
||||
fetchCurrentConfig();
|
||||
}
|
||||
}, [botName, lobbyId, schema]);
|
||||
|
||||
const handleValueChange = (paramName: string, value: any) => {
|
||||
setConfigValues(prev => ({
|
||||
...prev,
|
||||
[paramName]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/api/bots/config/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bot_name: botName,
|
||||
lobby_id: lobbyId,
|
||||
config_values: configValues
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setCurrentConfig(result.updated_config);
|
||||
if (onConfigUpdate) {
|
||||
onConfigUpdate(result.updated_config);
|
||||
}
|
||||
} else {
|
||||
setError(result.message || 'Failed to save configuration');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Network error while saving configuration');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderParameter = (param: ConfigParameter) => {
|
||||
const value = configValues[param.name];
|
||||
|
||||
switch (param.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value || false}
|
||||
onChange={(e) => handleValueChange(param.name, e.target.checked)}
|
||||
/>
|
||||
{param.label}
|
||||
</label>
|
||||
<p className="config-description">{param.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
||||
className="config-select"
|
||||
>
|
||||
{param.options?.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="config-description">{param.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'range':
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
<div className="range-container">
|
||||
<input
|
||||
type="range"
|
||||
min={param.min_value || 0}
|
||||
max={param.max_value || 100}
|
||||
step={param.step || 1}
|
||||
value={value || param.default_value || 0}
|
||||
onChange={(e) => handleValueChange(param.name, Number(e.target.value))}
|
||||
className="config-range"
|
||||
/>
|
||||
<span className="range-value">{value || param.default_value || 0}</span>
|
||||
</div>
|
||||
<p className="config-description">{param.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
<input
|
||||
type="number"
|
||||
min={param.min_value}
|
||||
max={param.max_value}
|
||||
step={param.step || 1}
|
||||
value={value || ''}
|
||||
onChange={(e) => handleValueChange(param.name, Number(e.target.value))}
|
||||
className="config-input"
|
||||
/>
|
||||
<p className="config-description">{param.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'string':
|
||||
default:
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={param.max_length}
|
||||
pattern={param.pattern}
|
||||
value={value || ''}
|
||||
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
||||
className="config-input"
|
||||
/>
|
||||
<p className="config-description">{param.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderParametersByCategory = () => {
|
||||
if (!schema) return null;
|
||||
|
||||
if (schema.categories && schema.categories.length > 0) {
|
||||
return schema.categories.map(category => (
|
||||
<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;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
} else {
|
||||
return schema.parameters.map(renderParameter);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="bot-config-loading">Loading configuration...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="bot-config-error">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!schema) {
|
||||
return <div className="bot-config-unavailable">Configuration not available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bot-config-container">
|
||||
<div className="bot-config-header">
|
||||
<h3>Configure {botName}</h3>
|
||||
<p>Lobby: {lobbyId}</p>
|
||||
{currentConfig && (
|
||||
<p className="config-meta">
|
||||
Last updated: {new Date(currentConfig.updated_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bot-config-form">
|
||||
{renderParametersByCategory()}
|
||||
</div>
|
||||
|
||||
<div className="bot-config-actions">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="config-save-button"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Configuration'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BotConfigComponent;
|
@ -25,8 +25,11 @@ import {
|
||||
Add as AddIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { botsApi, BotInfoModel, BotProviderModel, BotJoinLobbyRequest } from "./api-client";
|
||||
import BotConfig from "./BotConfig";
|
||||
import BotConfigComponent from "./BotConfig";
|
||||
|
||||
interface BotManagerProps {
|
||||
lobbyId: string;
|
||||
@ -44,6 +47,9 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
||||
const [selectedBot, setSelectedBot] = useState<string>("");
|
||||
const [botNick, setBotNick] = useState("");
|
||||
const [addingBot, setAddingBot] = useState(false);
|
||||
// New state for bot configuration
|
||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||
const [configBotName, setConfigBotName] = useState<string>("");
|
||||
|
||||
const loadBots = async () => {
|
||||
setLoading(true);
|
||||
@ -113,6 +119,29 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
||||
return provider ? provider.name : "Unknown Provider";
|
||||
};
|
||||
|
||||
// Configuration handlers
|
||||
const handleOpenConfigDialog = (botName: string) => {
|
||||
setConfigBotName(botName);
|
||||
setConfigDialogOpen(true);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleCloseConfigDialog = () => {
|
||||
setConfigDialogOpen(false);
|
||||
setConfigBotName("");
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleConfigUpdate = (config: any) => {
|
||||
// Optional: show success message or refresh bot info
|
||||
console.log("Bot configuration updated:", config);
|
||||
setConfigDialogOpen(false);
|
||||
};
|
||||
|
||||
const isBotConfigurable = (bot: BotInfoModel): boolean => {
|
||||
return Boolean(bot.configurable) || Boolean(bot.features && bot.features.includes("per_lobby_config"));
|
||||
};
|
||||
|
||||
const botCount = bots.length;
|
||||
const providerCount = botProviders.length;
|
||||
|
||||
@ -184,6 +213,15 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
||||
}
|
||||
secondaryTypographyProps={{ component: "div" }}
|
||||
/>
|
||||
{isBotConfigurable(botInfo) && (
|
||||
<IconButton
|
||||
onClick={() => handleOpenConfigDialog(botInfo.name)}
|
||||
size="small"
|
||||
title="Configure Bot"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
@ -283,6 +321,19 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Bot Configuration Dialog */}
|
||||
<Dialog open={configDialogOpen} onClose={handleCloseConfigDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Configure {configBotName}</DialogTitle>
|
||||
<DialogContent>
|
||||
{configDialogOpen && configBotName && (
|
||||
<BotConfig botName={configBotName} lobbyId={lobbyId} onConfigUpdate={handleConfigUpdate} />
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseConfigDialog}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
@ -23,6 +23,8 @@ export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
||||
export interface BotInfoModel {
|
||||
name: string;
|
||||
description: string;
|
||||
configurable?: boolean;
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
export interface BotProviderModel {
|
||||
|
358
server/api/bot_config.py
Normal file
358
server/api/bot_config.py
Normal file
@ -0,0 +1,358 @@
|
||||
"""
|
||||
Bot Configuration API
|
||||
|
||||
This module provides REST API endpoints for managing bot configurations
|
||||
including schema discovery, configuration CRUD operations, and real-time updates.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, WebSocket
|
||||
|
||||
from logger import logger
|
||||
from core.bot_config_manager import BotConfigManager
|
||||
|
||||
# Import WebSocket handler base class
|
||||
try:
|
||||
from websocket.message_handlers import MessageHandler
|
||||
except ImportError:
|
||||
from ..websocket.message_handlers import MessageHandler
|
||||
|
||||
# Import shared models with fallback handling
|
||||
try:
|
||||
from ...shared.models import (
|
||||
BotConfigSchema,
|
||||
BotLobbyConfig,
|
||||
BotConfigUpdateRequest,
|
||||
BotConfigUpdateResponse,
|
||||
BotConfigListResponse
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
from shared.models import (
|
||||
BotConfigSchema,
|
||||
BotLobbyConfig,
|
||||
BotConfigUpdateRequest,
|
||||
BotConfigUpdateResponse,
|
||||
BotConfigListResponse
|
||||
)
|
||||
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
|
||||
|
||||
class BotConfigUpdateRequest(BaseModel):
|
||||
bot_name: str
|
||||
lobby_id: str
|
||||
config_values: Dict[str, Any]
|
||||
|
||||
class BotConfigUpdateResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
updated_config: Optional[BotLobbyConfig] = None
|
||||
|
||||
class BotConfigListResponse(BaseModel):
|
||||
lobby_id: str
|
||||
configs: List[BotLobbyConfig]
|
||||
|
||||
|
||||
def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> APIRouter:
|
||||
"""Create FastAPI router for bot configuration endpoints"""
|
||||
|
||||
router = APIRouter(prefix="/api/bots/config", tags=["Bot Configuration"])
|
||||
|
||||
@router.get("/schema/{bot_name}")
|
||||
async def get_bot_config_schema(bot_name: str) -> BotConfigSchema:
|
||||
"""Get configuration schema for a specific bot"""
|
||||
try:
|
||||
# Check if we have cached schema
|
||||
schema = config_manager.get_bot_config_schema(bot_name)
|
||||
|
||||
if not schema:
|
||||
# Try to discover schema from bot provider
|
||||
providers = bot_manager.get_providers()
|
||||
for provider_id, provider in providers.items():
|
||||
try:
|
||||
# Check if this provider has the bot
|
||||
provider_bots = await bot_manager.get_provider_bots(provider_id)
|
||||
bot_names = [bot.name for bot in provider_bots.bots]
|
||||
|
||||
if bot_name in bot_names:
|
||||
schema = await config_manager.discover_bot_config_schema(
|
||||
bot_name, provider.base_url
|
||||
)
|
||||
if schema:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check provider {provider_id} for bot {bot_name}: {e}")
|
||||
continue
|
||||
|
||||
if not schema:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No configuration schema found for bot '{bot_name}'"
|
||||
)
|
||||
|
||||
return schema
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get bot config schema for {bot_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/lobby/{lobby_id}")
|
||||
async def get_lobby_bot_configs(lobby_id: str) -> BotConfigListResponse:
|
||||
"""Get all bot configurations for a lobby"""
|
||||
try:
|
||||
configs = config_manager.get_lobby_configs(lobby_id)
|
||||
return BotConfigListResponse(lobby_id=lobby_id, configs=configs)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get lobby configs for {lobby_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/lobby/{lobby_id}/bot/{bot_name}")
|
||||
async def get_lobby_bot_config(lobby_id: str, bot_name: str) -> BotLobbyConfig:
|
||||
"""Get specific bot configuration for a lobby"""
|
||||
try:
|
||||
config = config_manager.get_lobby_bot_config(lobby_id, bot_name)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get config for bot {bot_name} in lobby {lobby_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.post("/update")
|
||||
async def update_bot_config(
|
||||
request: BotConfigUpdateRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
session_id: str = "unknown" # TODO: Get from auth/session context
|
||||
) -> BotConfigUpdateResponse:
|
||||
"""Update bot configuration for a lobby"""
|
||||
try:
|
||||
# Find the provider for this bot
|
||||
provider_id = None
|
||||
provider_url = None
|
||||
|
||||
providers = bot_manager.get_providers()
|
||||
for pid, provider in providers.items():
|
||||
try:
|
||||
provider_bots = await bot_manager.get_provider_bots(pid)
|
||||
bot_names = [bot.name for bot in provider_bots.bots]
|
||||
|
||||
if request.bot_name in bot_names:
|
||||
provider_id = pid
|
||||
provider_url = provider.base_url
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not provider_id:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Bot '{request.bot_name}' not found in any provider"
|
||||
)
|
||||
|
||||
# Update configuration
|
||||
config = config_manager.set_bot_config(
|
||||
lobby_id=request.lobby_id,
|
||||
bot_name=request.bot_name,
|
||||
provider_id=provider_id,
|
||||
config_values=request.config_values,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# Notify bot provider in background
|
||||
background_tasks.add_task(
|
||||
config_manager.notify_bot_config_change,
|
||||
provider_url,
|
||||
request.bot_name,
|
||||
request.lobby_id,
|
||||
config
|
||||
)
|
||||
|
||||
return BotConfigUpdateResponse(
|
||||
success=True,
|
||||
message="Configuration updated successfully",
|
||||
updated_config=config
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Validation error
|
||||
return BotConfigUpdateResponse(
|
||||
success=False,
|
||||
message=f"Validation error: {str(e)}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update bot config: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.delete("/lobby/{lobby_id}/bot/{bot_name}")
|
||||
async def delete_bot_config(lobby_id: str, bot_name: str) -> Dict[str, Any]:
|
||||
"""Delete bot configuration for a lobby"""
|
||||
try:
|
||||
success = config_manager.delete_bot_config(lobby_id, bot_name)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Configuration deleted successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete bot config: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.delete("/lobby/{lobby_id}")
|
||||
async def delete_lobby_configs(lobby_id: str) -> Dict[str, Any]:
|
||||
"""Delete all bot configurations for a lobby"""
|
||||
try:
|
||||
success = config_manager.delete_lobby_configs(lobby_id)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": "All lobby configurations deleted" if success else "No configurations found"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete lobby configs: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/statistics")
|
||||
async def get_config_statistics() -> Dict[str, Any]:
|
||||
"""Get configuration manager statistics"""
|
||||
try:
|
||||
return config_manager.get_statistics()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get config statistics: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.post("/refresh-schemas")
|
||||
async def refresh_bot_schemas(background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""Refresh all bot configuration schemas from providers"""
|
||||
try:
|
||||
async def refresh_task():
|
||||
refreshed = 0
|
||||
providers = bot_manager.get_providers()
|
||||
|
||||
for provider_id, provider in providers.items():
|
||||
try:
|
||||
provider_bots = await bot_manager.get_provider_bots(provider_id)
|
||||
|
||||
for bot in provider_bots.bots:
|
||||
try:
|
||||
schema = await config_manager.discover_bot_config_schema(
|
||||
bot.name, provider.base_url
|
||||
)
|
||||
if schema:
|
||||
refreshed += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to refresh schema for {bot.name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to refresh schemas from provider {provider_id}: {e}")
|
||||
|
||||
logger.info(f"Refreshed {refreshed} bot configuration schemas")
|
||||
|
||||
background_tasks.add_task(refresh_task)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Schema refresh started in background"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start schema refresh: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
class BotConfigUpdateHandler(MessageHandler):
|
||||
"""WebSocket handler for real-time bot configuration updates"""
|
||||
|
||||
def __init__(self, config_manager: BotConfigManager):
|
||||
self.config_manager = config_manager
|
||||
|
||||
async def handle(self, session, lobby, data: Dict[str, Any], websocket: WebSocket, managers: Dict[str, Any]):
|
||||
"""Handle real-time bot configuration updates via WebSocket"""
|
||||
try:
|
||||
# Extract update data
|
||||
lobby_id = lobby.lobby_id if lobby else data.get("lobby_id")
|
||||
bot_name = data.get("bot_name")
|
||||
config_values = data.get("config_values")
|
||||
session_id = session.session_id if session else "unknown"
|
||||
|
||||
if not all([lobby_id, bot_name, config_values]):
|
||||
await websocket.send_json({
|
||||
"type": "bot_config_error",
|
||||
"error": "Missing required fields: lobby_id, bot_name, config_values"
|
||||
})
|
||||
return
|
||||
|
||||
# Update configuration (this will validate the values)
|
||||
config = self.config_manager.set_bot_config(
|
||||
lobby_id=lobby_id,
|
||||
bot_name=bot_name,
|
||||
provider_id="", # Will be resolved
|
||||
config_values=config_values,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# Send success response
|
||||
await websocket.send_json({
|
||||
"type": "bot_config_updated",
|
||||
"config": {
|
||||
"bot_name": config.bot_name,
|
||||
"lobby_id": config.lobby_id,
|
||||
"config_values": config.config_values,
|
||||
"updated_at": config.updated_at
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(f"Bot configuration updated via WebSocket: {bot_name} in lobby {lobby_id}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error updating bot configuration: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
await websocket.send_json({
|
||||
"type": "bot_config_error",
|
||||
"error": error_msg
|
||||
})
|
||||
|
||||
|
||||
def setup_websocket_config_handlers(websocket_manager, config_manager: BotConfigManager):
|
||||
"""Setup WebSocket handlers for real-time configuration updates"""
|
||||
|
||||
# Register the bot configuration update handler
|
||||
config_handler = BotConfigUpdateHandler(config_manager)
|
||||
websocket_manager.message_router.register("bot_config_update", config_handler)
|
||||
|
||||
logger.info("Bot configuration WebSocket handlers registered")
|
375
server/core/bot_config_manager.py
Normal file
375
server/core/bot_config_manager.py
Normal file
@ -0,0 +1,375 @@
|
||||
"""
|
||||
Bot Configuration Manager
|
||||
|
||||
This module handles per-lobby bot configuration management including:
|
||||
- Configuration schema discovery from bot providers
|
||||
- Configuration storage and retrieval per lobby
|
||||
- Real-time configuration updates
|
||||
- Validation of configuration values
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import httpx
|
||||
from typing import Dict, List, Optional, Any
|
||||
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
|
||||
|
||||
|
||||
class BotConfigManager:
|
||||
"""Manages bot configurations for lobbies"""
|
||||
|
||||
def __init__(self, storage_dir: str = "./bot_configs"):
|
||||
self.storage_dir = Path(storage_dir)
|
||||
self.storage_dir.mkdir(exist_ok=True)
|
||||
|
||||
# In-memory cache for fast access
|
||||
self.config_cache: Dict[str, Dict[str, BotLobbyConfig]] = {} # lobby_id -> bot_name -> config
|
||||
self.schema_cache: Dict[str, BotConfigSchema] = {} # bot_name -> schema
|
||||
|
||||
# Load existing configurations
|
||||
self._load_configurations()
|
||||
|
||||
def _get_config_file(self, lobby_id: str) -> Path:
|
||||
"""Get configuration file path for a lobby"""
|
||||
return self.storage_dir / f"lobby_{lobby_id}.json"
|
||||
|
||||
def _get_schema_file(self, bot_name: str) -> Path:
|
||||
"""Get schema file path for a bot"""
|
||||
return self.storage_dir / f"schema_{bot_name}.json"
|
||||
|
||||
def _load_configurations(self):
|
||||
"""Load all configurations from disk"""
|
||||
try:
|
||||
# Load lobby configurations
|
||||
for config_file in self.storage_dir.glob("lobby_*.json"):
|
||||
try:
|
||||
lobby_id = config_file.stem.replace("lobby_", "")
|
||||
with open(config_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.config_cache[lobby_id] = {}
|
||||
for bot_name, config_data in data.items():
|
||||
config = BotLobbyConfig(**config_data)
|
||||
self.config_cache[lobby_id][bot_name] = config
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load lobby config {config_file}: {e}")
|
||||
|
||||
# Load bot schemas
|
||||
for schema_file in self.storage_dir.glob("schema_*.json"):
|
||||
try:
|
||||
bot_name = schema_file.stem.replace("schema_", "")
|
||||
with open(schema_file, 'r') as f:
|
||||
schema_data = json.load(f)
|
||||
|
||||
schema = BotConfigSchema(**schema_data)
|
||||
self.schema_cache[bot_name] = schema
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load bot schema {schema_file}: {e}")
|
||||
|
||||
logger.info(f"Loaded configurations for {len(self.config_cache)} lobbies and {len(self.schema_cache)} bot schemas")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load configurations: {e}")
|
||||
|
||||
def _save_lobby_config(self, lobby_id: str):
|
||||
"""Save lobby configuration to disk"""
|
||||
try:
|
||||
config_file = self._get_config_file(lobby_id)
|
||||
|
||||
if lobby_id not in self.config_cache:
|
||||
return
|
||||
|
||||
# Convert to serializable format
|
||||
data = {}
|
||||
for bot_name, config in self.config_cache[lobby_id].items():
|
||||
data[bot_name] = config.model_dump()
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save lobby config {lobby_id}: {e}")
|
||||
|
||||
def _save_bot_schema(self, bot_name: str):
|
||||
"""Save bot schema to disk"""
|
||||
try:
|
||||
if bot_name not in self.schema_cache:
|
||||
return
|
||||
|
||||
schema_file = self._get_schema_file(bot_name)
|
||||
schema_data = self.schema_cache[bot_name].model_dump()
|
||||
|
||||
with open(schema_file, 'w') as f:
|
||||
json.dump(schema_data, f, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save bot schema {bot_name}: {e}")
|
||||
|
||||
async def discover_bot_config_schema(self, bot_name: str, provider_url: str) -> Optional[BotConfigSchema]:
|
||||
"""Discover configuration schema from bot provider"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Try to get configuration schema from bot provider
|
||||
response = await client.get(
|
||||
f"{provider_url}/bots/{bot_name}/config-schema",
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
schema_data = response.json()
|
||||
schema = BotConfigSchema(**schema_data)
|
||||
|
||||
# Cache the schema
|
||||
self.schema_cache[bot_name] = schema
|
||||
self._save_bot_schema(bot_name)
|
||||
|
||||
logger.info(f"Discovered config schema for bot {bot_name}")
|
||||
return schema
|
||||
else:
|
||||
logger.warning(f"Bot {bot_name} does not support configuration (HTTP {response.status_code})")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to discover config schema for bot {bot_name}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def get_bot_config_schema(self, bot_name: str) -> Optional[BotConfigSchema]:
|
||||
"""Get cached configuration schema for a bot"""
|
||||
return self.schema_cache.get(bot_name)
|
||||
|
||||
def get_lobby_bot_config(self, lobby_id: str, bot_name: str) -> Optional[BotLobbyConfig]:
|
||||
"""Get bot configuration for a specific lobby"""
|
||||
if lobby_id in self.config_cache and bot_name in self.config_cache[lobby_id]:
|
||||
return self.config_cache[lobby_id][bot_name]
|
||||
return None
|
||||
|
||||
def get_lobby_configs(self, lobby_id: str) -> List[BotLobbyConfig]:
|
||||
"""Get all bot configurations for a lobby"""
|
||||
if lobby_id in self.config_cache:
|
||||
return list(self.config_cache[lobby_id].values())
|
||||
return []
|
||||
|
||||
def set_bot_config(self,
|
||||
lobby_id: str,
|
||||
bot_name: str,
|
||||
provider_id: str,
|
||||
config_values: Dict[str, Any],
|
||||
session_id: str) -> BotLobbyConfig:
|
||||
"""Set or update bot configuration for a lobby"""
|
||||
|
||||
# Validate configuration against schema if available
|
||||
schema = self.get_bot_config_schema(bot_name)
|
||||
if schema:
|
||||
validated_values = self._validate_config_values(config_values, schema)
|
||||
else:
|
||||
validated_values = config_values
|
||||
|
||||
# Create or update configuration
|
||||
now = time.time()
|
||||
|
||||
existing_config = self.get_lobby_bot_config(lobby_id, bot_name)
|
||||
if existing_config:
|
||||
# Update existing
|
||||
config = BotLobbyConfig(
|
||||
bot_name=bot_name,
|
||||
lobby_id=lobby_id,
|
||||
provider_id=provider_id,
|
||||
config_values=validated_values,
|
||||
created_at=existing_config.created_at,
|
||||
updated_at=now,
|
||||
created_by=existing_config.created_by
|
||||
)
|
||||
else:
|
||||
# Create new
|
||||
config = BotLobbyConfig(
|
||||
bot_name=bot_name,
|
||||
lobby_id=lobby_id,
|
||||
provider_id=provider_id,
|
||||
config_values=validated_values,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by=session_id
|
||||
)
|
||||
|
||||
# Store in cache
|
||||
if lobby_id not in self.config_cache:
|
||||
self.config_cache[lobby_id] = {}
|
||||
|
||||
self.config_cache[lobby_id][bot_name] = config
|
||||
|
||||
# Save to disk
|
||||
self._save_lobby_config(lobby_id)
|
||||
|
||||
logger.info(f"Updated config for bot {bot_name} in lobby {lobby_id}")
|
||||
return config
|
||||
|
||||
def _validate_config_values(self, values: Dict[str, Any], schema: BotConfigSchema) -> Dict[str, Any]:
|
||||
"""Validate configuration values against schema"""
|
||||
validated = {}
|
||||
|
||||
for param in schema.parameters:
|
||||
value = values.get(param.name)
|
||||
|
||||
# Check required parameters
|
||||
if param.required and value is None:
|
||||
if param.default_value is not None:
|
||||
value = param.default_value
|
||||
else:
|
||||
raise ValueError(f"Required parameter '{param.name}' is missing")
|
||||
|
||||
# Skip None values for optional parameters
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
# Type validation
|
||||
if param.type == "string":
|
||||
value = str(value)
|
||||
if param.max_length and len(value) > param.max_length:
|
||||
raise ValueError(f"Parameter '{param.name}' exceeds max length {param.max_length}")
|
||||
|
||||
elif param.type == "number":
|
||||
value = float(value)
|
||||
if param.min_value is not None and value < param.min_value:
|
||||
raise ValueError(f"Parameter '{param.name}' below minimum {param.min_value}")
|
||||
if param.max_value is not None and value > param.max_value:
|
||||
raise ValueError(f"Parameter '{param.name}' above maximum {param.max_value}")
|
||||
|
||||
elif param.type == "boolean":
|
||||
value = bool(value)
|
||||
|
||||
elif param.type == "select":
|
||||
if param.options:
|
||||
valid_values = [opt["value"] for opt in param.options]
|
||||
if value not in valid_values:
|
||||
raise ValueError(f"Parameter '{param.name}' must be one of {valid_values}")
|
||||
|
||||
elif param.type == "range":
|
||||
value = float(value)
|
||||
if param.min_value is not None and value < param.min_value:
|
||||
value = param.min_value
|
||||
if param.max_value is not None and value > param.max_value:
|
||||
value = param.max_value
|
||||
|
||||
validated[param.name] = value
|
||||
|
||||
return validated
|
||||
|
||||
def delete_bot_config(self, lobby_id: str, bot_name: str) -> bool:
|
||||
"""Delete bot configuration for a lobby"""
|
||||
if lobby_id in self.config_cache and bot_name in self.config_cache[lobby_id]:
|
||||
del self.config_cache[lobby_id][bot_name]
|
||||
|
||||
# Clean up empty lobby configs
|
||||
if not self.config_cache[lobby_id]:
|
||||
del self.config_cache[lobby_id]
|
||||
config_file = self._get_config_file(lobby_id)
|
||||
if config_file.exists():
|
||||
config_file.unlink()
|
||||
else:
|
||||
self._save_lobby_config(lobby_id)
|
||||
|
||||
logger.info(f"Deleted config for bot {bot_name} in lobby {lobby_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def delete_lobby_configs(self, lobby_id: str) -> bool:
|
||||
"""Delete all bot configurations for a lobby"""
|
||||
if lobby_id in self.config_cache:
|
||||
del self.config_cache[lobby_id]
|
||||
|
||||
config_file = self._get_config_file(lobby_id)
|
||||
if config_file.exists():
|
||||
config_file.unlink()
|
||||
|
||||
logger.info(f"Deleted all configs for lobby {lobby_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def notify_bot_config_change(self,
|
||||
provider_url: str,
|
||||
bot_name: str,
|
||||
lobby_id: str,
|
||||
config: BotLobbyConfig):
|
||||
"""Notify bot provider of configuration change"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Notify the bot provider of the configuration change
|
||||
response = await client.post(
|
||||
f"{provider_url}/bots/{bot_name}/config",
|
||||
json={
|
||||
"lobby_id": lobby_id,
|
||||
"config_values": config.config_values
|
||||
},
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Successfully notified bot {bot_name} of config change")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Failed to notify bot {bot_name} of config change: HTTP {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify bot {bot_name} of config change: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get configuration manager statistics"""
|
||||
total_configs = sum(len(lobby_configs) for lobby_configs in self.config_cache.values())
|
||||
|
||||
return {
|
||||
"total_lobbies": len(self.config_cache),
|
||||
"total_configs": total_configs,
|
||||
"cached_schemas": len(self.schema_cache),
|
||||
"lobbies": {
|
||||
lobby_id: len(configs)
|
||||
for lobby_id, configs in self.config_cache.items()
|
||||
}
|
||||
}
|
@ -31,11 +31,13 @@ try:
|
||||
from core.lobby_manager import LobbyManager
|
||||
from core.auth_manager import AuthManager
|
||||
from core.bot_manager import BotManager
|
||||
from core.bot_config_manager import BotConfigManager
|
||||
from websocket.connection import WebSocketConnectionManager
|
||||
from api.admin import AdminAPI
|
||||
from api.sessions import SessionAPI
|
||||
from api.lobbies import LobbyAPI
|
||||
from api.bots import create_bot_router
|
||||
from api.bot_config import create_bot_config_router, setup_websocket_config_handlers
|
||||
except ImportError:
|
||||
# Handle relative imports when running as module
|
||||
import sys
|
||||
@ -47,11 +49,13 @@ except ImportError:
|
||||
from core.lobby_manager import LobbyManager
|
||||
from core.auth_manager import AuthManager
|
||||
from core.bot_manager import BotManager
|
||||
from core.bot_config_manager import BotConfigManager
|
||||
from websocket.connection import WebSocketConnectionManager
|
||||
from api.admin import AdminAPI
|
||||
from api.sessions import SessionAPI
|
||||
from api.lobbies import LobbyAPI
|
||||
from api.bots import create_bot_router
|
||||
from api.bot_config import create_bot_config_router, setup_websocket_config_handlers
|
||||
|
||||
from logger import logger
|
||||
|
||||
@ -97,13 +101,21 @@ session_manager: SessionManager = None
|
||||
lobby_manager: LobbyManager = None
|
||||
auth_manager: AuthManager = None
|
||||
bot_manager: BotManager = None
|
||||
bot_config_manager: BotConfigManager = None
|
||||
websocket_manager: WebSocketConnectionManager = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Lifespan context manager for startup and shutdown events"""
|
||||
global session_manager, lobby_manager, auth_manager, bot_manager, websocket_manager
|
||||
global \
|
||||
session_manager, \
|
||||
lobby_manager, \
|
||||
auth_manager, \
|
||||
bot_manager, \
|
||||
bot_config_manager, \
|
||||
websocket_manager
|
||||
|
||||
|
||||
# Startup
|
||||
logger.info("Starting AI Voice Bot server with modular architecture...")
|
||||
@ -113,6 +125,7 @@ async def lifespan(app: FastAPI):
|
||||
lobby_manager = LobbyManager()
|
||||
auth_manager = AuthManager("sessions.json")
|
||||
bot_manager = BotManager()
|
||||
bot_config_manager = BotConfigManager("./bot_configs")
|
||||
|
||||
# Load existing data
|
||||
session_manager.load()
|
||||
@ -136,6 +149,9 @@ async def lifespan(app: FastAPI):
|
||||
auth_manager=auth_manager,
|
||||
)
|
||||
|
||||
# Setup WebSocket handlers for bot configuration
|
||||
setup_websocket_config_handlers(websocket_manager, bot_config_manager)
|
||||
|
||||
# Create and register API routes
|
||||
admin_api = AdminAPI(
|
||||
session_manager=session_manager,
|
||||
@ -156,11 +172,15 @@ async def lifespan(app: FastAPI):
|
||||
# Create bot API router
|
||||
bot_router = create_bot_router(bot_manager, session_manager, lobby_manager)
|
||||
|
||||
# Create bot configuration API router
|
||||
bot_config_router = create_bot_config_router(bot_config_manager, bot_manager)
|
||||
|
||||
# Register API routes during startup
|
||||
app.include_router(admin_api.router)
|
||||
app.include_router(session_api.router)
|
||||
app.include_router(lobby_api.router)
|
||||
app.include_router(bot_router, prefix=public_url.rstrip("/") + "/api")
|
||||
app.include_router(bot_config_router, prefix=public_url.rstrip("/"))
|
||||
|
||||
# Add monitoring router if available
|
||||
if monitoring_available and monitoring_router:
|
||||
|
@ -9,7 +9,7 @@ Test comment for shared reload detection - updated again
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List, Dict, Optional, Literal
|
||||
from typing import List, Dict, Optional, Literal, Any
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@ -340,6 +340,81 @@ class BotInfoModel(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
has_media: bool = True # Whether this bot provides audio/video streams
|
||||
configurable: bool = False # Whether this bot supports per-lobby configuration
|
||||
config_schema: Optional[Dict[str, Any]] = (
|
||||
None # JSON schema for configuration parameters
|
||||
)
|
||||
|
||||
|
||||
class BotConfigParameter(BaseModel):
|
||||
"""Definition of a bot configuration parameter"""
|
||||
|
||||
name: str
|
||||
type: Literal["string", "number", "boolean", "select", "range"]
|
||||
label: str
|
||||
description: str
|
||||
default_value: Optional[Any] = None
|
||||
required: bool = False
|
||||
|
||||
# For select type
|
||||
options: Optional[List[Dict[str, str]]] = (
|
||||
None # [{"value": "val", "label": "Label"}]
|
||||
)
|
||||
|
||||
# For range/number type
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
step: Optional[float] = None
|
||||
|
||||
# For string type
|
||||
max_length: Optional[int] = None
|
||||
pattern: Optional[str] = None # Regex pattern
|
||||
|
||||
|
||||
class BotConfigSchema(BaseModel):
|
||||
"""Schema defining all configurable parameters for a bot"""
|
||||
|
||||
bot_name: str
|
||||
version: str = "1.0"
|
||||
parameters: List[BotConfigParameter]
|
||||
categories: Optional[List[Dict[str, List[str]]]] = (
|
||||
None # Group parameters by category
|
||||
)
|
||||
|
||||
|
||||
class BotLobbyConfig(BaseModel):
|
||||
"""Bot configuration for a specific lobby"""
|
||||
|
||||
bot_name: str
|
||||
lobby_id: str
|
||||
provider_id: str
|
||||
config_values: Dict[str, Any] # Parameter name -> value mapping
|
||||
created_at: float
|
||||
updated_at: float
|
||||
created_by: str # Session ID of who created the config
|
||||
|
||||
|
||||
class BotConfigUpdateRequest(BaseModel):
|
||||
"""Request to update bot configuration"""
|
||||
|
||||
bot_name: str
|
||||
lobby_id: str
|
||||
config_values: Dict[str, Any]
|
||||
|
||||
|
||||
class BotConfigUpdateResponse(BaseModel):
|
||||
"""Response to bot configuration update"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
updated_config: Optional[BotLobbyConfig] = None
|
||||
|
||||
|
||||
class BotConfigListResponse(BaseModel):
|
||||
"""Response listing bot configurations for a lobby"""
|
||||
|
||||
lobby_id: str
|
||||
configs: List[BotLobbyConfig]
|
||||
|
||||
|
||||
class BotProviderBotsResponse(BaseModel):
|
||||
|
@ -13,7 +13,7 @@ import sys
|
||||
import os
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
# Add the parent directory to sys.path to allow absolute imports
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
@ -30,8 +30,30 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from shared.models import ChatMessageModel, BotInfoModel, BotProviderBotsResponse
|
||||
|
||||
|
||||
# Global variables for reconnection logic
|
||||
_server_url: Optional[str] = None
|
||||
_voicebot_url: Optional[str] = None
|
||||
_insecure: bool = False
|
||||
_provider_id: Optional[str] = None
|
||||
_reconnect_task: Optional[asyncio.Task] = None
|
||||
_shutdown_event = asyncio.Event()
|
||||
_provider_registration_status: bool = False
|
||||
|
||||
|
||||
def get_provider_registration_status() -> dict:
|
||||
"""Get the current provider registration status for use by bot clients."""
|
||||
return {
|
||||
"is_registered": _provider_registration_status,
|
||||
"provider_id": _provider_id,
|
||||
"server_url": _server_url,
|
||||
"last_check": time.time()
|
||||
}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global _reconnect_task, _shutdown_event
|
||||
|
||||
# Startup
|
||||
logger.info(f"🚀 Voicebot bot orchestrator started successfully at {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
# Log the discovered bots
|
||||
@ -45,17 +67,27 @@ async def lifespan(app: FastAPI):
|
||||
# Check for remote server registration
|
||||
remote_server_url = os.getenv('VOICEBOT_SERVER_URL')
|
||||
if remote_server_url:
|
||||
# Attempt to register with remote server
|
||||
# Set up global variables for reconnection logic
|
||||
global _server_url, _voicebot_url, _insecure, _provider_id
|
||||
_server_url = remote_server_url
|
||||
_insecure = os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true'
|
||||
|
||||
host = os.getenv('HOST', '0.0.0.0')
|
||||
port = os.getenv('PORT', '8788')
|
||||
_voicebot_url = _construct_voicebot_url(host, port)
|
||||
|
||||
# Attempt initial registration
|
||||
try:
|
||||
host = os.getenv('HOST', '0.0.0.0')
|
||||
port = os.getenv('PORT', '8788')
|
||||
insecure = os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true'
|
||||
|
||||
provider_id = await _perform_server_registration(remote_server_url, host, port, insecure)
|
||||
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {provider_id}")
|
||||
_provider_id = await _perform_server_registration(remote_server_url, host, port, _insecure)
|
||||
_provider_registration_status = True
|
||||
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {_provider_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to register with remote server: {e}")
|
||||
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
|
||||
_provider_registration_status = False
|
||||
logger.error(f"❌ Failed initial registration with remote server: {e}")
|
||||
logger.warning("⚠️ Will attempt reconnection in background")
|
||||
|
||||
# Start the reconnection monitoring task
|
||||
_reconnect_task = asyncio.create_task(reconnection_monitor())
|
||||
else:
|
||||
logger.info("ℹ️ No VOICEBOT_SERVER_URL provided - running in local-only mode")
|
||||
|
||||
@ -63,6 +95,106 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
# Shutdown
|
||||
logger.info("🛑 Voicebot bot orchestrator shutting down")
|
||||
_shutdown_event.set()
|
||||
if _reconnect_task:
|
||||
_reconnect_task.cancel()
|
||||
try:
|
||||
await _reconnect_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
async def reconnection_monitor():
|
||||
"""Background task that monitors server connectivity and re-registers if needed."""
|
||||
reconnect_interval = 15 # Check every 15 seconds (faster for testing)
|
||||
retry_interval = 5 # Retry failed connections every 5 seconds
|
||||
|
||||
global _provider_registration_status, _provider_id
|
||||
|
||||
logger.info(f"🔄 Starting provider reconnection monitor (check every {reconnect_interval}s)")
|
||||
|
||||
while not _shutdown_event.is_set():
|
||||
try:
|
||||
if _server_url and _voicebot_url and _provider_id:
|
||||
# First check if server is healthy
|
||||
is_server_healthy = await check_server_health(_server_url, _insecure)
|
||||
|
||||
if not is_server_healthy:
|
||||
logger.warning("⚠️ Server appears to be down or unreachable")
|
||||
_provider_registration_status = False
|
||||
# Try to re-register
|
||||
try:
|
||||
_provider_id = await register_with_server(_server_url, _voicebot_url, _insecure)
|
||||
_provider_registration_status = True
|
||||
logger.info(f"🔄 Successfully re-registered with server! Provider ID: {_provider_id}")
|
||||
# Use longer interval after successful reconnection
|
||||
await asyncio.sleep(reconnect_interval)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Re-registration failed: {e}")
|
||||
# Use shorter interval for retry
|
||||
await asyncio.sleep(retry_interval)
|
||||
else:
|
||||
# Server is healthy, now check if we're still registered
|
||||
is_registered = await check_provider_registration(_server_url, _provider_id, _insecure)
|
||||
_provider_registration_status = is_registered
|
||||
|
||||
if not is_registered:
|
||||
logger.warning("⚠️ Provider registration lost, attempting to re-register")
|
||||
try:
|
||||
_provider_id = await register_with_server(_server_url, _voicebot_url, _insecure)
|
||||
_provider_registration_status = True
|
||||
logger.info(f"🔄 Successfully re-registered with server! Provider ID: {_provider_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Re-registration failed: {e}")
|
||||
await asyncio.sleep(retry_interval)
|
||||
continue
|
||||
|
||||
# All good, check again after normal interval
|
||||
await asyncio.sleep(reconnect_interval)
|
||||
else:
|
||||
# Missing configuration, wait longer
|
||||
_provider_registration_status = False
|
||||
await asyncio.sleep(reconnect_interval * 2)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("🛑 Provider reconnection monitor cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Unexpected error in provider reconnection monitor: {e}")
|
||||
await asyncio.sleep(retry_interval)
|
||||
|
||||
|
||||
async def check_server_health(server_url: str, insecure: bool = False) -> bool:
|
||||
"""Check if the server is reachable and healthy."""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
verify = not insecure
|
||||
async with httpx.AsyncClient(verify=verify) as client:
|
||||
# Try to hit the health endpoint
|
||||
response = await client.get(f"{server_url}/api/health", timeout=5.0)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.debug(f"Health check failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def check_provider_registration(server_url: str, provider_id: str, insecure: bool = False) -> bool:
|
||||
"""Check if the bot provider is still registered with the server."""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
verify = not insecure
|
||||
async with httpx.AsyncClient(verify=verify) as client:
|
||||
# Check if our provider is still in the provider list
|
||||
response = await client.get(f"{server_url}/api/bots", timeout=5.0)
|
||||
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()]
|
||||
except Exception as e:
|
||||
logger.debug(f"Provider registration check failed: {e}")
|
||||
return False
|
||||
|
||||
app = FastAPI(title="voicebot-bot-orchestrator", lifespan=lifespan)
|
||||
|
||||
@ -134,6 +266,19 @@ def discover_bots() -> "List[BotInfoModel]":
|
||||
if hasattr(mod, "bind_send_chat_function") and callable(getattr(mod, "bind_send_chat_function")):
|
||||
bind_send_chat_function = getattr(mod, "bind_send_chat_function")
|
||||
|
||||
# Check for configuration schema support
|
||||
config_schema = None
|
||||
config_handler = None
|
||||
if hasattr(mod, "get_config_schema") and callable(getattr(mod, "get_config_schema")):
|
||||
try:
|
||||
config_schema = mod.get_config_schema()
|
||||
logger.info(f"Bot {bot_info.name} supports configuration")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get config schema for {bot_info.name}: {e}")
|
||||
|
||||
if hasattr(mod, "handle_config_update") and callable(getattr(mod, "handle_config_update")):
|
||||
config_handler = getattr(mod, "handle_config_update")
|
||||
|
||||
_bot_registry[bot_info.name] = {
|
||||
"module": name,
|
||||
"info": bot_info,
|
||||
@ -141,6 +286,8 @@ def discover_bots() -> "List[BotInfoModel]":
|
||||
"chat_handler": chat_handler,
|
||||
"track_handler": track_handler,
|
||||
"chat_bind": bind_send_chat_function,
|
||||
"config_schema": config_schema,
|
||||
"config_handler": config_handler,
|
||||
}
|
||||
|
||||
except Exception:
|
||||
@ -232,6 +379,65 @@ async def bot_join(bot_name: str, req: JoinRequest):
|
||||
return {"status": "started", "bot": bot_name, "run_id": run_id}
|
||||
|
||||
|
||||
@app.get("/provider/status")
|
||||
def get_provider_status():
|
||||
"""Get the current provider registration status."""
|
||||
return get_provider_registration_status()
|
||||
|
||||
|
||||
@app.get("/bots/{bot_name}/config-schema")
|
||||
def get_bot_config_schema(bot_name: str):
|
||||
"""Get configuration schema for a specific bot."""
|
||||
if bot_name not in _bot_registry:
|
||||
raise HTTPException(status_code=404, detail=f"Bot '{bot_name}' not found")
|
||||
|
||||
bot_entry = _bot_registry[bot_name]
|
||||
config_schema = bot_entry.get("config_schema")
|
||||
|
||||
if not config_schema:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Bot '{bot_name}' does not support configuration"
|
||||
)
|
||||
|
||||
return config_schema
|
||||
|
||||
|
||||
@app.post("/bots/{bot_name}/config")
|
||||
async def update_bot_config(bot_name: str, config_data: dict):
|
||||
"""Update bot configuration for a specific lobby."""
|
||||
if bot_name not in _bot_registry:
|
||||
raise HTTPException(status_code=404, detail=f"Bot '{bot_name}' not found")
|
||||
|
||||
bot_entry = _bot_registry[bot_name]
|
||||
config_handler = bot_entry.get("config_handler")
|
||||
|
||||
if not config_handler:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Bot '{bot_name}' does not support configuration updates"
|
||||
)
|
||||
|
||||
try:
|
||||
lobby_id = config_data.get("lobby_id")
|
||||
config_values = config_data.get("config_values", {})
|
||||
|
||||
if not lobby_id:
|
||||
raise HTTPException(status_code=400, detail="lobby_id is required")
|
||||
|
||||
# Call the bot's configuration handler
|
||||
success = await config_handler(lobby_id, config_values)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Configuration updated successfully"}
|
||||
else:
|
||||
return {"success": False, "message": "Configuration update failed"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update config for bot {bot_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.post("/bots/runs/{run_id}/stop")
|
||||
async def stop_run(run_id: str):
|
||||
"""Stop a running bot."""
|
||||
@ -364,6 +570,12 @@ def start_bot_provider(
|
||||
"""Start the bot provider API server and optionally register with main server"""
|
||||
import time
|
||||
|
||||
# Set up global variables for reconnection logic
|
||||
global _server_url, _voicebot_url, _insecure, _provider_id
|
||||
_server_url = server_url
|
||||
_insecure = insecure
|
||||
_voicebot_url = _construct_voicebot_url(host, str(port))
|
||||
|
||||
# Start the FastAPI server in a background thread
|
||||
# Add reload functionality for development
|
||||
if reload:
|
||||
@ -386,7 +598,7 @@ def start_bot_provider(
|
||||
logger.info(f"Starting bot provider API server on {host}:{port}...")
|
||||
server_thread.start()
|
||||
|
||||
# If server_url is provided, register with the main server
|
||||
# If server_url is provided, attempt initial registration
|
||||
if server_url:
|
||||
logger.info(f"🔄 Server URL provided - will attempt registration with: {server_url}")
|
||||
# Give the server a moment to start
|
||||
@ -394,11 +606,25 @@ def start_bot_provider(
|
||||
time.sleep(2)
|
||||
|
||||
try:
|
||||
provider_id = _perform_server_registration_sync(server_url, host, str(port), insecure)
|
||||
logger.info(f"🎉 Registration completed successfully! Provider ID: {provider_id}")
|
||||
_provider_id = _perform_server_registration_sync(server_url, host, str(port), insecure)
|
||||
logger.info(f"🎉 Registration completed successfully! Provider ID: {_provider_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to register with server: {e}")
|
||||
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
|
||||
logger.error(f"❌ Failed initial registration with server: {e}")
|
||||
logger.warning("⚠️ Bot orchestrator will continue running and attempt reconnection")
|
||||
|
||||
# Start a background thread for reconnection monitoring
|
||||
def run_reconnection_monitor():
|
||||
"""Run reconnection monitor in a separate thread with its own event loop."""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(reconnection_monitor())
|
||||
except Exception as e:
|
||||
logger.error(f"Reconnection monitor thread failed: {e}")
|
||||
|
||||
reconnect_thread = threading.Thread(target=run_reconnection_monitor, daemon=True)
|
||||
reconnect_thread.start()
|
||||
logger.info("🔄 Started reconnection monitor in background thread")
|
||||
else:
|
||||
logger.info("ℹ️ No remote server URL provided - running in local-only mode")
|
||||
|
||||
@ -408,3 +634,4 @@ def start_bot_provider(
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down bot provider...")
|
||||
_shutdown_event.set()
|
||||
|
@ -238,12 +238,14 @@ def agent_info() -> Dict[str, str]:
|
||||
"name": AGENT_NAME,
|
||||
"description": AGENT_DESCRIPTION,
|
||||
"has_media": "false",
|
||||
"configurable": "true", # This bot supports per-lobby configuration
|
||||
"features": [
|
||||
"multi_provider_ai",
|
||||
"personality_system",
|
||||
"conversation_memory",
|
||||
"streaming_responses",
|
||||
"health_monitoring"
|
||||
"health_monitoring",
|
||||
"per_lobby_config"
|
||||
]
|
||||
}
|
||||
|
||||
@ -357,3 +359,159 @@ async def switch_ai_provider(provider_type: str) -> bool:
|
||||
logger.error(f"Failed to switch AI provider: {e}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_config_schema() -> Dict[str, Any]:
|
||||
"""Get the configuration schema for this bot"""
|
||||
return {
|
||||
"bot_name": AGENT_NAME,
|
||||
"version": "1.0",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "personality",
|
||||
"type": "select",
|
||||
"label": "Bot Personality",
|
||||
"description": "The personality and communication style of the bot",
|
||||
"default_value": "helpful_assistant",
|
||||
"required": False,
|
||||
"options": [
|
||||
{"value": "helpful_assistant", "label": "Helpful Assistant"},
|
||||
{"value": "technical_expert", "label": "Technical Expert"},
|
||||
{"value": "creative_companion", "label": "Creative Companion"},
|
||||
{"value": "business_advisor", "label": "Business Advisor"},
|
||||
{"value": "comedy_bot", "label": "Comedy Bot"},
|
||||
{"value": "wise_mentor", "label": "Wise Mentor"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ai_provider",
|
||||
"type": "select",
|
||||
"label": "AI Provider",
|
||||
"description": "The AI service to use for generating responses",
|
||||
"default_value": "openai",
|
||||
"required": False,
|
||||
"options": [
|
||||
{"value": "openai", "label": "OpenAI (GPT)"},
|
||||
{"value": "anthropic", "label": "Anthropic (Claude)"},
|
||||
{"value": "local", "label": "Local Model"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "streaming",
|
||||
"type": "boolean",
|
||||
"label": "Streaming Responses",
|
||||
"description": "Enable real-time streaming of responses as they are generated",
|
||||
"default_value": False,
|
||||
"required": False
|
||||
},
|
||||
{
|
||||
"name": "memory_enabled",
|
||||
"type": "boolean",
|
||||
"label": "Conversation Memory",
|
||||
"description": "Remember conversation context and history",
|
||||
"default_value": True,
|
||||
"required": False
|
||||
},
|
||||
{
|
||||
"name": "response_length",
|
||||
"type": "select",
|
||||
"label": "Response Length",
|
||||
"description": "Preferred length of bot responses",
|
||||
"default_value": "medium",
|
||||
"required": False,
|
||||
"options": [
|
||||
{"value": "short", "label": "Short & Concise"},
|
||||
{"value": "medium", "label": "Medium Length"},
|
||||
{"value": "long", "label": "Detailed & Comprehensive"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "creativity_level",
|
||||
"type": "range",
|
||||
"label": "Creativity Level",
|
||||
"description": "How creative and varied the responses should be (0-100)",
|
||||
"default_value": 50,
|
||||
"required": False,
|
||||
"min_value": 0,
|
||||
"max_value": 100,
|
||||
"step": 10
|
||||
},
|
||||
{
|
||||
"name": "response_style",
|
||||
"type": "select",
|
||||
"label": "Response Style",
|
||||
"description": "The communication style for responses",
|
||||
"default_value": "conversational",
|
||||
"required": False,
|
||||
"options": [
|
||||
{"value": "formal", "label": "Formal & Professional"},
|
||||
{"value": "conversational", "label": "Conversational & Friendly"},
|
||||
{"value": "casual", "label": "Casual & Relaxed"},
|
||||
{"value": "academic", "label": "Academic & Technical"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "AI Settings",
|
||||
"parameters": ["personality", "ai_provider", "streaming"]
|
||||
},
|
||||
{
|
||||
"name": "Behavior Settings",
|
||||
"parameters": ["memory_enabled", "response_length", "creativity_level"]
|
||||
},
|
||||
{
|
||||
"name": "Communication Style",
|
||||
"parameters": ["response_style"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def handle_config_update(lobby_id: str, config_values: Dict[str, Any]) -> bool:
|
||||
"""Handle configuration update for a specific lobby"""
|
||||
global _bot_instance
|
||||
|
||||
try:
|
||||
logger.info(f"Updating config for lobby {lobby_id}: {config_values}")
|
||||
|
||||
# Apply configuration changes
|
||||
config_applied = False
|
||||
|
||||
if "personality" in config_values:
|
||||
success = await switch_personality(config_values["personality"])
|
||||
if success:
|
||||
config_applied = True
|
||||
logger.info(f"Applied personality: {config_values['personality']}")
|
||||
|
||||
if "ai_provider" in config_values:
|
||||
success = await switch_ai_provider(config_values["ai_provider"])
|
||||
if success:
|
||||
config_applied = True
|
||||
logger.info(f"Applied AI provider: {config_values['ai_provider']}")
|
||||
|
||||
if "streaming" in config_values:
|
||||
global BOT_STREAMING
|
||||
BOT_STREAMING = bool(config_values["streaming"])
|
||||
config_applied = True
|
||||
logger.info(f"Applied streaming: {BOT_STREAMING}")
|
||||
|
||||
if "memory_enabled" in config_values:
|
||||
global BOT_MEMORY_ENABLED
|
||||
BOT_MEMORY_ENABLED = bool(config_values["memory_enabled"])
|
||||
config_applied = True
|
||||
logger.info(f"Applied memory: {BOT_MEMORY_ENABLED}")
|
||||
|
||||
# Store other configuration values for use in response generation
|
||||
if _bot_instance:
|
||||
if not hasattr(_bot_instance, 'lobby_configs'):
|
||||
_bot_instance.lobby_configs = {}
|
||||
|
||||
_bot_instance.lobby_configs[lobby_id] = config_values
|
||||
config_applied = True
|
||||
|
||||
return config_applied
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply config update: {e}")
|
||||
return False
|
||||
|
Loading…
x
Reference in New Issue
Block a user