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,
|
Add as AddIcon,
|
||||||
ExpandMore as ExpandMoreIcon,
|
ExpandMore as ExpandMoreIcon,
|
||||||
Refresh as RefreshIcon,
|
Refresh as RefreshIcon,
|
||||||
|
Settings as SettingsIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { botsApi, BotInfoModel, BotProviderModel, BotJoinLobbyRequest } from "./api-client";
|
import { botsApi, BotInfoModel, BotProviderModel, BotJoinLobbyRequest } from "./api-client";
|
||||||
|
import BotConfig from "./BotConfig";
|
||||||
|
import BotConfigComponent from "./BotConfig";
|
||||||
|
|
||||||
interface BotManagerProps {
|
interface BotManagerProps {
|
||||||
lobbyId: string;
|
lobbyId: string;
|
||||||
@ -44,6 +47,9 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
const [selectedBot, setSelectedBot] = useState<string>("");
|
const [selectedBot, setSelectedBot] = useState<string>("");
|
||||||
const [botNick, setBotNick] = useState("");
|
const [botNick, setBotNick] = useState("");
|
||||||
const [addingBot, setAddingBot] = useState(false);
|
const [addingBot, setAddingBot] = useState(false);
|
||||||
|
// New state for bot configuration
|
||||||
|
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||||
|
const [configBotName, setConfigBotName] = useState<string>("");
|
||||||
|
|
||||||
const loadBots = async () => {
|
const loadBots = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -113,6 +119,29 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
return provider ? provider.name : "Unknown Provider";
|
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 botCount = bots.length;
|
||||||
const providerCount = botProviders.length;
|
const providerCount = botProviders.length;
|
||||||
|
|
||||||
@ -184,6 +213,15 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
}
|
}
|
||||||
secondaryTypographyProps={{ component: "div" }}
|
secondaryTypographyProps={{ component: "div" }}
|
||||||
/>
|
/>
|
||||||
|
{isBotConfigurable(botInfo) && (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handleOpenConfigDialog(botInfo.name)}
|
||||||
|
size="small"
|
||||||
|
title="Configure Bot"
|
||||||
|
>
|
||||||
|
<SettingsIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -283,6 +321,19 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</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>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,8 @@ export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
|||||||
export interface BotInfoModel {
|
export interface BotInfoModel {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
configurable?: boolean;
|
||||||
|
features?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BotProviderModel {
|
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.lobby_manager import LobbyManager
|
||||||
from core.auth_manager import AuthManager
|
from core.auth_manager import AuthManager
|
||||||
from core.bot_manager import BotManager
|
from core.bot_manager import BotManager
|
||||||
|
from core.bot_config_manager import BotConfigManager
|
||||||
from websocket.connection import WebSocketConnectionManager
|
from websocket.connection import WebSocketConnectionManager
|
||||||
from api.admin import AdminAPI
|
from api.admin import AdminAPI
|
||||||
from api.sessions import SessionAPI
|
from api.sessions import SessionAPI
|
||||||
from api.lobbies import LobbyAPI
|
from api.lobbies import LobbyAPI
|
||||||
from api.bots import create_bot_router
|
from api.bots import create_bot_router
|
||||||
|
from api.bot_config import create_bot_config_router, setup_websocket_config_handlers
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Handle relative imports when running as module
|
# Handle relative imports when running as module
|
||||||
import sys
|
import sys
|
||||||
@ -47,11 +49,13 @@ except ImportError:
|
|||||||
from core.lobby_manager import LobbyManager
|
from core.lobby_manager import LobbyManager
|
||||||
from core.auth_manager import AuthManager
|
from core.auth_manager import AuthManager
|
||||||
from core.bot_manager import BotManager
|
from core.bot_manager import BotManager
|
||||||
|
from core.bot_config_manager import BotConfigManager
|
||||||
from websocket.connection import WebSocketConnectionManager
|
from websocket.connection import WebSocketConnectionManager
|
||||||
from api.admin import AdminAPI
|
from api.admin import AdminAPI
|
||||||
from api.sessions import SessionAPI
|
from api.sessions import SessionAPI
|
||||||
from api.lobbies import LobbyAPI
|
from api.lobbies import LobbyAPI
|
||||||
from api.bots import create_bot_router
|
from api.bots import create_bot_router
|
||||||
|
from api.bot_config import create_bot_config_router, setup_websocket_config_handlers
|
||||||
|
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
@ -97,13 +101,21 @@ session_manager: SessionManager = None
|
|||||||
lobby_manager: LobbyManager = None
|
lobby_manager: LobbyManager = None
|
||||||
auth_manager: AuthManager = None
|
auth_manager: AuthManager = None
|
||||||
bot_manager: BotManager = None
|
bot_manager: BotManager = None
|
||||||
|
bot_config_manager: BotConfigManager = None
|
||||||
websocket_manager: WebSocketConnectionManager = None
|
websocket_manager: WebSocketConnectionManager = None
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Lifespan context manager for startup and shutdown events"""
|
"""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
|
# Startup
|
||||||
logger.info("Starting AI Voice Bot server with modular architecture...")
|
logger.info("Starting AI Voice Bot server with modular architecture...")
|
||||||
@ -113,6 +125,7 @@ async def lifespan(app: FastAPI):
|
|||||||
lobby_manager = LobbyManager()
|
lobby_manager = LobbyManager()
|
||||||
auth_manager = AuthManager("sessions.json")
|
auth_manager = AuthManager("sessions.json")
|
||||||
bot_manager = BotManager()
|
bot_manager = BotManager()
|
||||||
|
bot_config_manager = BotConfigManager("./bot_configs")
|
||||||
|
|
||||||
# Load existing data
|
# Load existing data
|
||||||
session_manager.load()
|
session_manager.load()
|
||||||
@ -136,6 +149,9 @@ async def lifespan(app: FastAPI):
|
|||||||
auth_manager=auth_manager,
|
auth_manager=auth_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Setup WebSocket handlers for bot configuration
|
||||||
|
setup_websocket_config_handlers(websocket_manager, bot_config_manager)
|
||||||
|
|
||||||
# Create and register API routes
|
# Create and register API routes
|
||||||
admin_api = AdminAPI(
|
admin_api = AdminAPI(
|
||||||
session_manager=session_manager,
|
session_manager=session_manager,
|
||||||
@ -156,11 +172,15 @@ async def lifespan(app: FastAPI):
|
|||||||
# Create bot API router
|
# Create bot API router
|
||||||
bot_router = create_bot_router(bot_manager, session_manager, lobby_manager)
|
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
|
# Register API routes during startup
|
||||||
app.include_router(admin_api.router)
|
app.include_router(admin_api.router)
|
||||||
app.include_router(session_api.router)
|
app.include_router(session_api.router)
|
||||||
app.include_router(lobby_api.router)
|
app.include_router(lobby_api.router)
|
||||||
app.include_router(bot_router, prefix=public_url.rstrip("/") + "/api")
|
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
|
# Add monitoring router if available
|
||||||
if monitoring_available and monitoring_router:
|
if monitoring_available and monitoring_router:
|
||||||
|
@ -9,7 +9,7 @@ Test comment for shared reload detection - updated again
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import List, Dict, Optional, Literal
|
from typing import List, Dict, Optional, Literal, Any
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@ -340,6 +340,81 @@ class BotInfoModel(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
has_media: bool = True # Whether this bot provides audio/video streams
|
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):
|
class BotProviderBotsResponse(BaseModel):
|
||||||
|
@ -13,7 +13,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from contextlib import asynccontextmanager
|
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
|
# Add the parent directory to sys.path to allow absolute imports
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
@ -30,8 +30,30 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from shared.models import ChatMessageModel, BotInfoModel, BotProviderBotsResponse
|
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
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
global _reconnect_task, _shutdown_event
|
||||||
|
|
||||||
# Startup
|
# Startup
|
||||||
logger.info(f"🚀 Voicebot bot orchestrator started successfully at {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
logger.info(f"🚀 Voicebot bot orchestrator started successfully at {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
# Log the discovered bots
|
# Log the discovered bots
|
||||||
@ -45,17 +67,27 @@ async def lifespan(app: FastAPI):
|
|||||||
# Check for remote server registration
|
# Check for remote server registration
|
||||||
remote_server_url = os.getenv('VOICEBOT_SERVER_URL')
|
remote_server_url = os.getenv('VOICEBOT_SERVER_URL')
|
||||||
if remote_server_url:
|
if remote_server_url:
|
||||||
# Attempt to register with remote server
|
# Set up global variables for reconnection logic
|
||||||
try:
|
global _server_url, _voicebot_url, _insecure, _provider_id
|
||||||
host = os.getenv('HOST', '0.0.0.0')
|
_server_url = remote_server_url
|
||||||
port = os.getenv('PORT', '8788')
|
_insecure = os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true'
|
||||||
insecure = os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true'
|
|
||||||
|
|
||||||
provider_id = await _perform_server_registration(remote_server_url, host, port, insecure)
|
host = os.getenv('HOST', '0.0.0.0')
|
||||||
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {provider_id}")
|
port = os.getenv('PORT', '8788')
|
||||||
|
_voicebot_url = _construct_voicebot_url(host, port)
|
||||||
|
|
||||||
|
# Attempt initial registration
|
||||||
|
try:
|
||||||
|
_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:
|
except Exception as e:
|
||||||
logger.error(f"❌ Failed to register with remote server: {e}")
|
_provider_registration_status = False
|
||||||
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
|
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:
|
else:
|
||||||
logger.info("ℹ️ No VOICEBOT_SERVER_URL provided - running in local-only mode")
|
logger.info("ℹ️ No VOICEBOT_SERVER_URL provided - running in local-only mode")
|
||||||
|
|
||||||
@ -63,6 +95,106 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
logger.info("🛑 Voicebot bot orchestrator shutting down")
|
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)
|
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")):
|
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")
|
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] = {
|
_bot_registry[bot_info.name] = {
|
||||||
"module": name,
|
"module": name,
|
||||||
"info": bot_info,
|
"info": bot_info,
|
||||||
@ -141,6 +286,8 @@ def discover_bots() -> "List[BotInfoModel]":
|
|||||||
"chat_handler": chat_handler,
|
"chat_handler": chat_handler,
|
||||||
"track_handler": track_handler,
|
"track_handler": track_handler,
|
||||||
"chat_bind": bind_send_chat_function,
|
"chat_bind": bind_send_chat_function,
|
||||||
|
"config_schema": config_schema,
|
||||||
|
"config_handler": config_handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception:
|
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}
|
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")
|
@app.post("/bots/runs/{run_id}/stop")
|
||||||
async def stop_run(run_id: str):
|
async def stop_run(run_id: str):
|
||||||
"""Stop a running bot."""
|
"""Stop a running bot."""
|
||||||
@ -364,6 +570,12 @@ def start_bot_provider(
|
|||||||
"""Start the bot provider API server and optionally register with main server"""
|
"""Start the bot provider API server and optionally register with main server"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# 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
|
# Start the FastAPI server in a background thread
|
||||||
# Add reload functionality for development
|
# Add reload functionality for development
|
||||||
if reload:
|
if reload:
|
||||||
@ -386,7 +598,7 @@ def start_bot_provider(
|
|||||||
logger.info(f"Starting bot provider API server on {host}:{port}...")
|
logger.info(f"Starting bot provider API server on {host}:{port}...")
|
||||||
server_thread.start()
|
server_thread.start()
|
||||||
|
|
||||||
# If server_url is provided, register with the main server
|
# If server_url is provided, attempt initial registration
|
||||||
if server_url:
|
if server_url:
|
||||||
logger.info(f"🔄 Server URL provided - will attempt registration with: {server_url}")
|
logger.info(f"🔄 Server URL provided - will attempt registration with: {server_url}")
|
||||||
# Give the server a moment to start
|
# Give the server a moment to start
|
||||||
@ -394,11 +606,25 @@ def start_bot_provider(
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider_id = _perform_server_registration_sync(server_url, host, str(port), insecure)
|
_provider_id = _perform_server_registration_sync(server_url, host, str(port), insecure)
|
||||||
logger.info(f"🎉 Registration completed successfully! Provider ID: {provider_id}")
|
logger.info(f"🎉 Registration completed successfully! Provider ID: {_provider_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Failed to register with server: {e}")
|
logger.error(f"❌ Failed initial registration with server: {e}")
|
||||||
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
|
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:
|
else:
|
||||||
logger.info("ℹ️ No remote server URL provided - running in local-only mode")
|
logger.info("ℹ️ No remote server URL provided - running in local-only mode")
|
||||||
|
|
||||||
@ -408,3 +634,4 @@ def start_bot_provider(
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("Shutting down bot provider...")
|
logger.info("Shutting down bot provider...")
|
||||||
|
_shutdown_event.set()
|
||||||
|
@ -238,12 +238,14 @@ def agent_info() -> Dict[str, str]:
|
|||||||
"name": AGENT_NAME,
|
"name": AGENT_NAME,
|
||||||
"description": AGENT_DESCRIPTION,
|
"description": AGENT_DESCRIPTION,
|
||||||
"has_media": "false",
|
"has_media": "false",
|
||||||
|
"configurable": "true", # This bot supports per-lobby configuration
|
||||||
"features": [
|
"features": [
|
||||||
"multi_provider_ai",
|
"multi_provider_ai",
|
||||||
"personality_system",
|
"personality_system",
|
||||||
"conversation_memory",
|
"conversation_memory",
|
||||||
"streaming_responses",
|
"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}")
|
logger.error(f"Failed to switch AI provider: {e}")
|
||||||
|
|
||||||
return False
|
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