Bot config no longer gives errors on frontend

This commit is contained in:
James Ketr 2025-09-15 12:25:29 -07:00
parent bef73a8af4
commit 716c508f45
25 changed files with 3327 additions and 569 deletions

View File

@ -3,24 +3,13 @@
!server !server
!client !client
!shared !shared
node_modules **/node_modules
build **/build
dist **/dist
**/__pycache__ **/__pycache__
**/.venv **/.venv
*.pyc **/.env
*.pyo **/*.pem
*.pyd **/*.key
*.log **/package-lock.json
*.swp **/*.pyc
*.swo
.env
.env.*
*.pem
*.key
*.bak
*.tmp
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml

View File

@ -1,4 +1,4 @@
FROM ubuntu:oracular FROM ubuntu:noble
# Stick with Python3.12 (plucky has 3.13) # Stick with Python3.12 (plucky has 3.13)
# Install some utilities frequently used # Install some utilities frequently used

View File

@ -1,9 +0,0 @@
#!/bin/bash
find . -name .venv | while read path; do
echo "Removing ${path}"
sudo rm -rf "${path}"
done
find . -name .python-version | while read path; do
echo "Removing ${path}"
sudo rm "${path}"
done

View File

@ -2,6 +2,6 @@ To deploy:
```bash ```bash
export PUBLIC_URL=/ai-voicebot export PUBLIC_URL=/ai-voicebot
npm run build docker compose exec client npm run build
rsync --delete -avrpl build/ webserver:/var/www/ketrenos.com/ai-voicebot/ rsync --delete -avrpl client/build/ webserver:/var/www/ketrenos.com/ai-voicebot/
``` ```

0
client/entrypoint.sh Normal file → Executable file
View File

View File

@ -727,14 +727,14 @@
} }
} }
}, },
"/ai-voicebot/api/bots/config/lobby/{lobby_id}/bot/{bot_name}": { "/ai-voicebot/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}": {
"get": { "get": {
"tags": [ "tags": [
"Bot Configuration" "Bot Configuration"
], ],
"summary": "Get Lobby Bot Config", "summary": "Get Lobby Bot Config",
"description": "Get specific bot configuration for a lobby", "description": "Get specific bot configuration for a lobby",
"operationId": "get_lobby_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_name__get", "operationId": "get_lobby_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_instance_id__get",
"parameters": [ "parameters": [
{ {
"name": "lobby_id", "name": "lobby_id",
@ -746,12 +746,12 @@
} }
}, },
{ {
"name": "bot_name", "name": "bot_instance_id",
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
"title": "Bot Name" "title": "Bot Instance Id"
} }
} }
], ],
@ -784,7 +784,7 @@
], ],
"summary": "Delete Bot Config", "summary": "Delete Bot Config",
"description": "Delete bot configuration for a lobby", "description": "Delete bot configuration for a lobby",
"operationId": "delete_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_name__delete", "operationId": "delete_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_instance_id__delete",
"parameters": [ "parameters": [
{ {
"name": "lobby_id", "name": "lobby_id",
@ -796,12 +796,12 @@
} }
}, },
{ {
"name": "bot_name", "name": "bot_instance_id",
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
"title": "Bot Name" "title": "Bot Instance Id"
} }
} }
], ],
@ -813,7 +813,7 @@
"schema": { "schema": {
"type": "object", "type": "object",
"additionalProperties": true, "additionalProperties": true,
"title": "Response Delete Bot Config Ai Voicebot Api Bots Config Lobby Lobby Id Bot Bot Name Delete" "title": "Response Delete Bot Config Ai Voicebot Api Bots Config Lobby Lobby Id Bot Bot Instance Id Delete"
} }
} }
} }
@ -1644,9 +1644,9 @@
}, },
"BotConfigUpdateRequest": { "BotConfigUpdateRequest": {
"properties": { "properties": {
"bot_name": { "bot_instance_id": {
"type": "string", "type": "string",
"title": "Bot Name" "title": "Bot Instance Id"
}, },
"lobby_id": { "lobby_id": {
"type": "string", "type": "string",
@ -1660,7 +1660,7 @@
}, },
"type": "object", "type": "object",
"required": [ "required": [
"bot_name", "bot_instance_id",
"lobby_id", "lobby_id",
"config_values" "config_values"
], ],

View File

@ -213,6 +213,9 @@ const LobbyView: React.FC<LobbyProps> = (props: LobbyProps) => {
<Box sx={{ mb: 2, display: "flex", gap: 2, alignItems: "flex-start", flexDirection: "column" }}> <Box sx={{ mb: 2, display: "flex", gap: 2, alignItems: "flex-start", flexDirection: "column" }}>
<Box> <Box>
<Typography variant="h5">AI Voice Chat Lobby: {lobbyName}</Typography> <Typography variant="h5">AI Voice Chat Lobby: {lobbyName}</Typography>
<Typography variant="caption">
{lobby ? `Lobby ID: ${lobby.id}` : creatingLobby ? "Joining lobby..." : "Not in a lobby"}
</Typography>
</Box> </Box>
<Box> <Box>
{session.name && <Typography>You are logged in as: {session.name}</Typography>} {session.name && <Typography>You are logged in as: {session.name}</Typography>}

View File

@ -6,51 +6,21 @@
* form controls for each configuration parameter. * form controls for each configuration parameter.
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from "react";
import { base } from "./Common"; import { base } from "./Common";
import "./BotConfig.css"; import "./BotConfig.css";
import type { components } from "./api-types";
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 { interface BotConfigProps {
botInstanceId?: string;
botName: string; botName: string;
lobbyId: string; lobbyId: string;
onConfigUpdate?: (config: BotConfig) => void; onConfigUpdate?: (config: components["schemas"]["BotLobbyConfig"]) => void;
} }
const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConfigUpdate }) => { const BotConfigComponent: React.FC<BotConfigProps> = ({ botInstanceId, botName, lobbyId, onConfigUpdate }) => {
const [schema, setSchema] = useState<ConfigSchema | null>(null); const [schema, setSchema] = useState<components["schemas"]["BotConfigSchema"] | null>(null);
const [currentConfig, setCurrentConfig] = useState<BotConfig | null>(null); const [currentConfig, setCurrentConfig] = useState<components["schemas"]["BotLobbyConfig"] | null>(null);
const [configValues, setConfigValues] = useState<{ [key: string]: any }>({}); const [configValues, setConfigValues] = useState<{ [key: string]: any }>({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -58,50 +28,52 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// Fetch configuration schema // Fetch configuration schema
const fetchSchema = async (forceRefresh = false) => { const fetchSchema = useCallback(
try { async (forceRefresh = false) => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
// Use refresh endpoint if force refresh is requested // Use refresh endpoint if force refresh is requested
const url = forceRefresh const url = forceRefresh
? `${base}/api/bots/config/schema/${botName}/refresh` ? `${base}/api/bots/config/schema/${botName}/refresh`
: `${base}/api/bots/config/schema/${botName}`; : `${base}/api/bots/config/schema/${botName}`;
const method = forceRefresh ? "POST" : "GET"; const method = forceRefresh ? "POST" : "GET";
const response = await fetch(url, { method }); const response = await fetch(url, { method });
if (response.ok) { if (response.ok) {
const responseData = await response.json(); const responseData = await response.json();
// For refresh endpoint, the schema is in the 'schema' field // For refresh endpoint, the schema is in the 'schema' field
const schemaData = forceRefresh ? responseData.schema : responseData; const schemaData = forceRefresh ? responseData.schema : responseData;
setSchema(schemaData);
setSchema(schemaData); // Initialize config values with defaults
const defaultValues: { [key: string]: any } = {};
// Initialize config values with defaults schemaData.parameters.forEach((param: components["schemas"]["BotConfigParameter"]) => {
const defaultValues: { [key: string]: any } = {}; if (param.default_value !== undefined) {
schemaData.parameters.forEach((param: ConfigParameter) => { defaultValues[param.name] = param.default_value;
if (param.default_value !== undefined) { }
defaultValues[param.name] = param.default_value; });
} setConfigValues(defaultValues);
}); } else if (response.status === 404) {
setConfigValues(defaultValues); setError(`Bot "${botName}" does not support configuration`);
} else if (response.status === 404) { } else {
setError(`Bot "${botName}" does not support configuration`); setError("Failed to fetch configuration schema");
} else { }
setError("Failed to fetch configuration schema"); } catch (err) {
setError("Network error while fetching configuration schema");
} finally {
setLoading(false);
} }
} catch (err) { },
setError("Network error while fetching configuration schema"); [botName]
} finally { );
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
fetchSchema(); fetchSchema();
}, [botName]); }, [botName, fetchSchema]);
// Handle schema refresh // Handle schema refresh
const handleRefreshSchema = async () => { const handleRefreshSchema = async () => {
@ -114,12 +86,12 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
useEffect(() => { useEffect(() => {
const fetchCurrentConfig = async () => { const fetchCurrentConfig = async () => {
try { try {
const response = await fetch(`${base}/api/bots/config/lobby/${lobbyId}/bot/${botName}`); const response = await fetch(`${base}/api/bots/config/lobby/${lobbyId}/bot/${botInstanceId}`);
if (response.ok) { if (response.ok) {
const config = await response.json(); const config = await response.json();
setCurrentConfig(config); setCurrentConfig(config);
setConfigValues({ ...configValues, ...config.config_values }); setConfigValues((prev) => ({ ...prev, ...config.config_values }));
} }
// If 404, no existing config - that's fine // If 404, no existing config - that's fine
} catch (err) { } catch (err) {
@ -127,10 +99,15 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
} }
}; };
if (schema) { if (schema && botInstanceId) {
fetchCurrentConfig(); fetchCurrentConfig();
} }
}, [botName, lobbyId, schema]); }, [botName, lobbyId, schema, botInstanceId]);
// Check if bot instance ID is available
if (!botInstanceId) {
return <div className="bot-config-unavailable">Configuration not available - no bot instance selected</div>;
}
const handleValueChange = (paramName: string, value: any) => { const handleValueChange = (paramName: string, value: any) => {
setConfigValues((prev) => ({ setConfigValues((prev) => ({
@ -150,7 +127,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
bot_name: botName, bot_instance_id: botInstanceId,
lobby_id: lobbyId, lobby_id: lobbyId,
config_values: configValues, config_values: configValues,
}), }),
@ -173,7 +150,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
} }
}; };
const renderParameter = (param: ConfigParameter) => { const renderParameter = (param: components["schemas"]["BotConfigParameter"]) => {
const value = configValues[param.name]; const value = configValues[param.name];
switch (param.type) { switch (param.type) {
@ -201,11 +178,18 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
onChange={(e) => handleValueChange(param.name, e.target.value)} onChange={(e) => handleValueChange(param.name, e.target.value)}
className="config-select" className="config-select"
> >
{param.options?.map((option) => ( {param.options?.map((option, index) => {
<option key={option.value} value={option.value}> // Handle both formats: {value: string, label: string} and generic {[key: string]: string}
{option.label} const optionValue =
</option> typeof option === "object" && "value" in option ? option.value : Object.keys(option)[0];
))} const optionLabel =
typeof option === "object" && "label" in option ? option.label : Object.values(option)[0];
return (
<option key={index} value={optionValue}>
{optionLabel}
</option>
);
})}
</select> </select>
<p className="config-description">{param.description}</p> <p className="config-description">{param.description}</p>
</div> </div>
@ -237,9 +221,9 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
<label className="config-label">{param.label}</label> <label className="config-label">{param.label}</label>
<input <input
type="number" type="number"
min={param.min_value} min={param.min_value ?? undefined}
max={param.max_value} max={param.max_value ?? undefined}
step={param.step || 1} step={param.step ?? 1}
value={value || ""} value={value || ""}
onChange={(e) => handleValueChange(param.name, Number(e.target.value))} onChange={(e) => handleValueChange(param.name, Number(e.target.value))}
className="config-input" className="config-input"
@ -255,8 +239,8 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
<label className="config-label">{param.label}</label> <label className="config-label">{param.label}</label>
<input <input
type="text" type="text"
maxLength={param.max_length} maxLength={param.max_length ?? undefined}
pattern={param.pattern} pattern={param.pattern ?? undefined}
value={value || ""} value={value || ""}
onChange={(e) => handleValueChange(param.name, e.target.value)} onChange={(e) => handleValueChange(param.name, e.target.value)}
className="config-input" className="config-input"
@ -271,17 +255,22 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
if (!schema) return null; if (!schema) return null;
if (schema.categories && schema.categories.length > 0) { if (schema.categories && schema.categories.length > 0) {
return schema.categories.map((category) => ( return schema.categories.map((category, categoryIndex) => {
<div key={category.name} className="config-category"> // Handle API format: each category is an object with string keys mapping to string arrays
<h4 className="category-title">{category.name}</h4> const categoryName = Object.keys(category)[0];
<div className="category-parameters"> const parameterNames = category[categoryName];
{category.parameters?.map((paramName) => { return (
const param = schema.parameters?.find((p) => p.name === paramName); <div key={categoryIndex} className="config-category">
return param ? <div key={paramName}>{renderParameter(param)}</div> : null; <h4 className="category-title">{categoryName}</h4>
}) || null} <div className="category-parameters">
{parameterNames?.map((paramName: string) => {
const param = schema.parameters?.find((p) => p.name === paramName);
return param ? <div key={paramName}>{renderParameter(param)}</div> : null;
}) || null}
</div>
</div> </div>
</div> );
)); });
} else { } else {
return schema.parameters?.map((param) => <div key={param.name}>{renderParameter(param)}</div>) || null; return schema.parameters?.map((param) => <div key={param.name}>{renderParameter(param)}</div>) || null;
} }
@ -303,7 +292,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
<div className="bot-config-container"> <div className="bot-config-container">
<div className="bot-config-header"> <div className="bot-config-header">
<div className="bot-config-title"> <div className="bot-config-title">
<h3>Configure {botName}</h3> <h3>Configure Bot {botInstanceId}</h3>
<button <button
onClick={handleRefreshSchema} onClick={handleRefreshSchema}
disabled={refreshing || loading} disabled={refreshing || loading}
@ -319,7 +308,9 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConf
)} )}
</div> </div>
<div className="bot-config-form">{renderParametersByCategory()}</div> <div className="bot-config-form">
<React.Fragment key="parameters-container">{renderParametersByCategory()}</React.Fragment>
</div>
<div className="bot-config-actions"> <div className="bot-config-actions">
<button onClick={handleSave} disabled={saving} className="config-save-button"> <button onClick={handleSave} disabled={saving} className="config-save-button">

View File

@ -29,7 +29,6 @@ import {
} from "@mui/icons-material"; } from "@mui/icons-material";
import { botsApi, BotInfoModel, BotProviderPublicModel, BotJoinLobbyRequest } from "./api-client"; import { botsApi, BotInfoModel, BotProviderPublicModel, BotJoinLobbyRequest } from "./api-client";
import BotConfig from "./BotConfig"; import BotConfig from "./BotConfig";
import BotConfigComponent from "./BotConfig";
interface BotManagerProps { interface BotManagerProps {
lobbyId: string; lobbyId: string;
@ -239,7 +238,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
} }
secondaryTypographyProps={{ component: "div" }} secondaryTypographyProps={{ component: "div" }}
/> />
{isBotConfigurable(botInfo) && ( {false && isBotConfigurable(botInfo) && (
<IconButton <IconButton
onClick={() => handleOpenConfigDialog(botInfo.name)} onClick={() => handleOpenConfigDialog(botInfo.name)}
size="small" size="small"

View File

@ -237,6 +237,7 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
<DialogContent> <DialogContent>
{selectedBotForConfig && ( {selectedBotForConfig && (
<BotConfig <BotConfig
botInstanceId={selectedBotForConfig.bot_instance_id || ""}
botName={selectedBotForConfig.name?.replace(/-bot$/, "") || "unknown"} botName={selectedBotForConfig.name?.replace(/-bot$/, "") || "unknown"}
lobbyId={lobbyId} lobbyId={lobbyId}
onConfigUpdate={(config) => { onConfigUpdate={(config) => {

View File

@ -182,7 +182,10 @@ export class ApiClient {
} }
async createRegisterBotProvider(data: any): Promise<any> { async createRegisterBotProvider(data: any): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/providers/register`), { method: "POST", body: data }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/providers/register`), {
method: "POST",
body: data,
});
} }
async getListBotProviders(): Promise<any> { async getListBotProviders(): Promise<any> {
@ -198,7 +201,9 @@ export class ApiClient {
} }
async createRequestBotLeaveLobby(bot_instance_id: string): Promise<any> { async createRequestBotLeaveLobby(bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/instances/${bot_instance_id}/leave`), { method: "POST" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/instances/${bot_instance_id}/leave`), {
method: "POST",
});
} }
async getBotInstance(bot_instance_id: string): Promise<any> { async getBotInstance(bot_instance_id: string): Promise<any> {
@ -217,16 +222,24 @@ export class ApiClient {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "DELETE" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "DELETE" });
} }
async getLobbyBotConfig(lobby_id: string, bot_name: string): Promise<any> { async getLobbyBotConfig(lobby_id: string, bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_name}`), { method: "GET" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_instance_id}`), {
method: "GET",
});
} }
async deleteBotConfig(lobby_id: string, bot_name: string): Promise<any> { async deleteBotConfig(lobby_id: string, bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_name}`), { method: "DELETE" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_instance_id}`), {
method: "DELETE",
});
} }
async createUpdateBotConfig(data: any, params?: Record<string, string>): Promise<any> { async createUpdateBotConfig(data: any, params?: Record<string, string>): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/update`), { method: "POST", body: data, params }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/update`), {
method: "POST",
body: data,
params,
});
} }
async getConfigStatistics(): Promise<any> { async getConfigStatistics(): Promise<any> {
@ -238,11 +251,15 @@ export class ApiClient {
} }
async createRefreshBotSchema(bot_name: string): Promise<any> { async createRefreshBotSchema(bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/refresh`), { method: "POST" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/refresh`), {
method: "POST",
});
} }
async deleteClearBotSchemaCache(bot_name: string): Promise<any> { async deleteClearBotSchemaCache(bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/cache`), { method: "DELETE" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/cache`), {
method: "DELETE",
});
} }
async getReadinessProbe(): Promise<any> { async getReadinessProbe(): Promise<any> {
@ -323,13 +340,13 @@ export const botsApi = {
getSchema: (bot_name: string) => apiClient.getBotConfigSchema(bot_name), getSchema: (bot_name: string) => apiClient.getBotConfigSchema(bot_name),
getBotConfigs: (lobby_id: string) => apiClient.getLobbyBotConfigs(lobby_id), getBotConfigs: (lobby_id: string) => apiClient.getLobbyBotConfigs(lobby_id),
deleteBotConfigs: (lobby_id: string) => apiClient.deleteLobbyConfigs(lobby_id), deleteBotConfigs: (lobby_id: string) => apiClient.deleteLobbyConfigs(lobby_id),
getBotConfig: (lobby_id: string, bot_name: string) => apiClient.getLobbyBotConfig(lobby_id, bot_name), getBotConfig: (lobby_id: string, bot_instance_id: string) => apiClient.getLobbyBotConfig(lobby_id, bot_instance_id),
deleteBotConfig: (lobby_id: string, bot_name: string) => apiClient.deleteBotConfig(lobby_id, bot_name), deleteBotConfig: (lobby_id: string, bot_instance_id: string) => apiClient.deleteBotConfig(lobby_id, bot_instance_id),
updateConfig: (data: any) => apiClient.createUpdateBotConfig(data), updateConfig: (data: any) => apiClient.createUpdateBotConfig(data),
getConfigStatistics: () => apiClient.getConfigStatistics(), getConfigStatistics: () => apiClient.getConfigStatistics(),
refreshAllSchemas: () => apiClient.createRefreshBotSchemas(), refreshAllSchemas: () => apiClient.createRefreshBotSchemas(),
refreshSchema: (bot_name: string) => apiClient.createRefreshBotSchema(bot_name), refreshSchema: (bot_name: string) => apiClient.createRefreshBotSchema(bot_name),
clearSchemaCache: (bot_name: string) => apiClient.deleteClearBotSchemaCache(bot_name) clearSchemaCache: (bot_name: string) => apiClient.deleteClearBotSchemaCache(bot_name),
}; };
export const metricsApi = { export const metricsApi = {

View File

@ -5,14 +5,14 @@ import { base } from './Common';
export const knownEndpoints = new Set([ export const knownEndpoints = new Set([
`DELETE:${base}/api/bots/config/lobby/{lobby_id}`, `DELETE:${base}/api/bots/config/lobby/{lobby_id}`,
`DELETE:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_name}`, `DELETE:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}`,
`DELETE:${base}/api/bots/config/schema/{bot_name}/cache`, `DELETE:${base}/api/bots/config/schema/{bot_name}/cache`,
`GET:${base}/api/admin/names`, `GET:${base}/api/admin/names`,
`GET:${base}/api/admin/session_metrics`, `GET:${base}/api/admin/session_metrics`,
`GET:${base}/api/admin/validate_sessions`, `GET:${base}/api/admin/validate_sessions`,
`GET:${base}/api/bots`, `GET:${base}/api/bots`,
`GET:${base}/api/bots/config/lobby/{lobby_id}`, `GET:${base}/api/bots/config/lobby/{lobby_id}`,
`GET:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_name}`, `GET:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}`,
`GET:${base}/api/bots/config/schema/{bot_name}`, `GET:${base}/api/bots/config/schema/{bot_name}`,
`GET:${base}/api/bots/config/statistics`, `GET:${base}/api/bots/config/statistics`,
`GET:${base}/api/bots/instances/{bot_instance_id}`, `GET:${base}/api/bots/instances/{bot_instance_id}`,

View File

@ -127,17 +127,17 @@ export interface paths {
*/ */
delete: operations["delete_lobby_configs_ai_voicebot_api_bots_config_lobby__lobby_id__delete"]; delete: operations["delete_lobby_configs_ai_voicebot_api_bots_config_lobby__lobby_id__delete"];
}; };
"/ai-voicebot/api/bots/config/lobby/{lobby_id}/bot/{bot_name}": { "/ai-voicebot/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}": {
/** /**
* Get Lobby Bot Config * Get Lobby Bot Config
* @description Get specific bot configuration for a lobby * @description Get specific bot configuration for a lobby
*/ */
get: operations["get_lobby_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_name__get"]; get: operations["get_lobby_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_instance_id__get"];
/** /**
* Delete Bot Config * Delete Bot Config
* @description Delete bot configuration for a lobby * @description Delete bot configuration for a lobby
*/ */
delete: operations["delete_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_name__delete"]; delete: operations["delete_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_instance_id__delete"];
}; };
"/ai-voicebot/api/bots/config/update": { "/ai-voicebot/api/bots/config/update": {
/** /**
@ -402,9 +402,11 @@ export interface components {
*/ */
required?: boolean; required?: boolean;
/** Options */ /** Options */
options?: { options?:
[key: string]: string; | {
}[] | null; [key: string]: string;
}[]
| null;
/** Min Value */ /** Min Value */
min_value?: number | null; min_value?: number | null;
/** Max Value */ /** Max Value */
@ -431,17 +433,19 @@ export interface components {
/** Parameters */ /** Parameters */
parameters: components["schemas"]["BotConfigParameter"][]; parameters: components["schemas"]["BotConfigParameter"][];
/** Categories */ /** Categories */
categories?: { categories?:
[key: string]: string[]; | {
}[] | null; [key: string]: string[];
}[]
| null;
}; };
/** /**
* BotConfigUpdateRequest * BotConfigUpdateRequest
* @description Request to update bot configuration * @description Request to update bot configuration
*/ */
BotConfigUpdateRequest: { BotConfigUpdateRequest: {
/** Bot Name */ /** Bot Instance Id */
bot_name: string; bot_instance_id: string;
/** Lobby Id */ /** Lobby Id */
lobby_id: string; lobby_id: string;
/** Config Values */ /** Config Values */
@ -793,7 +797,6 @@ export type $defs = Record<string, never>;
export type external = Record<string, never>; export type external = Record<string, never>;
export interface operations { export interface operations {
/** /**
* System Health * System Health
* @description System health check showing manager status and enhanced monitoring * @description System health check showing manager status and enhanced monitoring
@ -1225,11 +1228,11 @@ export interface operations {
* Get Lobby Bot Config * Get Lobby Bot Config
* @description Get specific bot configuration for a lobby * @description Get specific bot configuration for a lobby
*/ */
get_lobby_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_name__get: { get_lobby_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_instance_id__get: {
parameters: { parameters: {
path: { path: {
lobby_id: string; lobby_id: string;
bot_name: string; bot_instance_id: string;
}; };
}; };
responses: { responses: {
@ -1251,11 +1254,11 @@ export interface operations {
* Delete Bot Config * Delete Bot Config
* @description Delete bot configuration for a lobby * @description Delete bot configuration for a lobby
*/ */
delete_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_name__delete: { delete_bot_config_ai_voicebot_api_bots_config_lobby__lobby_id__bot__bot_instance_id__delete: {
parameters: { parameters: {
path: { path: {
lobby_id: string; lobby_id: string;
bot_name: string; bot_instance_id: string;
}; };
}; };
responses: { responses: {

View File

@ -52,6 +52,7 @@ services:
- ./shared:/shared:ro - ./shared:/shared:ro
- ./server:/server:rw - ./server:/server:rw
- ./server/.venv:/server/.venv:rw - ./server/.venv:/server/.venv:rw
- ./server/entrypoint.sh:/entrypoint.sh:ro
- ./client/build:/client/build:ro - ./client/build:/client/build:ro
- ./dev-keys:/keys:ro - ./dev-keys:/keys:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
@ -79,6 +80,7 @@ services:
- ./shared:/shared:ro - ./shared:/shared:ro
- ./voicebot:/voicebot:rw - ./voicebot:/voicebot:rw
- ./voicebot/.venv:/voicebot/.venv:rw - ./voicebot/.venv:/voicebot/.venv:rw
- ./voicebot/entrypoint.sh:/entrypoint.sh:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
# network_mode: host # network_mode: host

View File

@ -51,11 +51,13 @@ except ImportError:
def create_bot_config_router( def create_bot_config_router(
config_manager: BotConfigManager, bot_manager: BotManager config_manager: BotConfigManager, bot_manager: BotManager, public_url: str = "/"
) -> APIRouter: ) -> APIRouter:
"""Create FastAPI router for bot configuration endpoints""" """Create FastAPI router for bot configuration endpoints"""
router = APIRouter(prefix="/api/bots/config", tags=["Bot Configuration"]) router = APIRouter(
prefix=f"{public_url}api/bots/config", tags=["Bot Configuration"]
)
@router.get("/schema/{bot_name}") @router.get("/schema/{bot_name}")
async def get_bot_config_schema(bot_name: str) -> BotConfigSchema: # type: ignore async def get_bot_config_schema(bot_name: str) -> BotConfigSchema: # type: ignore
@ -137,24 +139,41 @@ def create_bot_config_router(
logger.error(f"Failed to get lobby configs for {lobby_id}: {e}") logger.error(f"Failed to get lobby configs for {lobby_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/lobby/{lobby_id}/bot/{bot_name}") @router.get("/lobby/{lobby_id}/bot/{bot_instance_id}")
async def get_lobby_bot_config(lobby_id: str, bot_name: str) -> BotLobbyConfig: async def get_lobby_bot_config(
lobby_id: str, bot_instance_id: str
) -> BotLobbyConfig:
"""Get specific bot configuration for a lobby""" """Get specific bot configuration for a lobby"""
logger.info(
f"Route handler called: get_lobby_bot_config({lobby_id}, {bot_instance_id})"
)
try: try:
# Get bot instance to find the bot_name
bot_instance = await bot_manager.get_bot_instance(bot_instance_id)
bot_name = bot_instance.bot_name
config = config_manager.get_lobby_bot_config(lobby_id, bot_name) config = config_manager.get_lobby_bot_config(lobby_id, bot_name)
if not config: if not config:
logger.warning(
f"No configuration found for bot instance '{bot_instance_id}' (bot: '{bot_name}') in lobby '{lobby_id}'"
)
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'", detail=f"No configuration found for bot instance '{bot_instance_id}' in lobby '{lobby_id}'",
) )
return config return config
except ValueError:
# Bot instance not found
raise HTTPException(
status_code=404, detail=f"Bot instance '{bot_instance_id}' not found"
)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Failed to get config for bot {bot_name} in lobby {lobby_id}: {e}" f"Failed to get config for bot instance {bot_instance_id} in lobby {lobby_id}: {e}"
) )
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@ -165,54 +184,39 @@ def create_bot_config_router(
session_id: str = "unknown", # TODO: Get from auth/session context session_id: str = "unknown", # TODO: Get from auth/session context
) -> BotConfigUpdateResponse: ) -> BotConfigUpdateResponse:
"""Update bot configuration for a lobby""" """Update bot configuration for a lobby"""
logger.info(
f"Route handler called: update_bot_config({request.bot_instance_id}, {request.lobby_id})"
)
try: try:
# Find the provider for this bot # Get bot instance information
provider_id = None bot_instance = await bot_manager.get_bot_instance(request.bot_instance_id)
provider_url = None bot_name = bot_instance.bot_name
provider_id = bot_instance.provider_id
providers_response = bot_manager.list_providers() # Find the provider URL
for provider in providers_response.providers: provider = bot_manager.get_provider(provider_id)
try: if not provider:
provider_bots = await bot_manager.get_provider_bots(
provider.provider_id
)
bot_names = [bot.name for bot in provider_bots.bots]
if request.bot_name in bot_names:
provider_id = provider.provider_id
provider_url = provider.base_url
break
except Exception:
continue
if not provider_id:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404, detail=f"Provider '{provider_id}' not found"
detail=f"Bot '{request.bot_name}' not found in any provider"
) )
provider_url = provider.base_url
# Update configuration # Update configuration
if not provider_id or not provider_url:
raise HTTPException(
status_code=404,
detail=f"Bot {request.bot_name} not found in any registered provider",
)
config = config_manager.set_bot_config( config = config_manager.set_bot_config(
lobby_id=request.lobby_id, lobby_id=request.lobby_id,
bot_name=request.bot_name, bot_name=bot_name,
provider_id=provider_id, provider_id=provider_id,
config_values=request.config_values, config_values=request.config_values,
session_id=session_id session_id=session_id,
) )
# Notify bot provider in background # Notify bot provider in background
background_tasks.add_task( background_tasks.add_task(
config_manager.notify_bot_config_change, config_manager.notify_bot_config_change,
provider_url, provider_url,
request.bot_name, bot_name,
request.lobby_id, request.lobby_id,
config config,
) )
return BotConfigUpdateResponse( return BotConfigUpdateResponse(
@ -222,31 +226,39 @@ def create_bot_config_router(
) )
except ValueError as e: except ValueError as e:
# Validation error # Bot instance not found
return BotConfigUpdateResponse( return BotConfigUpdateResponse(
success=False, success=False, message=f"Bot instance not found: {str(e)}"
message=f"Validation error: {str(e)}"
) )
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Failed to update bot config: {e}") logger.error(f"Failed to update bot config: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/lobby/{lobby_id}/bot/{bot_name}") @router.delete("/lobby/{lobby_id}/bot/{bot_instance_id}")
async def delete_bot_config(lobby_id: str, bot_name: str) -> Dict[str, Any]: async def delete_bot_config(lobby_id: str, bot_instance_id: str) -> Dict[str, Any]:
"""Delete bot configuration for a lobby""" """Delete bot configuration for a lobby"""
try: try:
# Get bot instance to find the bot_name
bot_instance = await bot_manager.get_bot_instance(bot_instance_id)
bot_name = bot_instance.bot_name
success = config_manager.delete_bot_config(lobby_id, bot_name) success = config_manager.delete_bot_config(lobby_id, bot_name)
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'" detail=f"No configuration found for bot instance '{bot_instance_id}' in lobby '{lobby_id}'",
) )
return {"success": True, "message": "Configuration deleted successfully"} return {"success": True, "message": "Configuration deleted successfully"}
except ValueError:
# Bot instance not found
raise HTTPException(
status_code=404, detail=f"Bot instance '{bot_instance_id}' not found"
)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -328,8 +340,13 @@ def create_bot_config_router(
# Find the provider for this bot # Find the provider for this bot
providers_response = bot_manager.list_providers() providers_response = bot_manager.list_providers()
for provider in providers_response.providers: for provider_public in providers_response.providers:
try: try:
# Get the full provider object to access base_url
provider = bot_manager.get_provider(provider_public.provider_id)
if not provider:
continue
provider_bots = await bot_manager.get_provider_bots( provider_bots = await bot_manager.get_provider_bots(
provider.provider_id provider.provider_id
) )
@ -353,7 +370,7 @@ def create_bot_config_router(
except Exception as e: except Exception as e:
logger.warning( logger.warning(
f"Failed to check provider {provider.provider_id}: {e}" f"Failed to check provider {provider_public.provider_id}: {e}"
) )
continue continue

View File

@ -20,10 +20,12 @@ except ImportError as e:
) )
def create_bot_router(bot_manager, session_manager, lobby_manager): def create_bot_router(
bot_manager, session_manager, lobby_manager, public_url: str = "/"
) -> APIRouter:
"""Create bot API router with dependencies""" """Create bot API router with dependencies"""
router = APIRouter(prefix="/bots", tags=["bots"]) router = APIRouter(prefix=f"{public_url}api/bots", tags=["bots"])
@router.post("/providers/register", response_model=BotProviderRegisterResponse) @router.post("/providers/register", response_model=BotProviderRegisterResponse)
async def register_bot_provider(request: BotProviderRegisterRequest) -> BotProviderRegisterResponse: async def register_bot_provider(request: BotProviderRegisterRequest) -> BotProviderRegisterResponse:

0
server/entrypoint.sh Normal file → Executable file
View File

View File

@ -19,6 +19,7 @@ import os
import asyncio import asyncio
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime from datetime import datetime
import time
from fastapi import FastAPI, WebSocket, Path, Request from fastapi import FastAPI, WebSocket, Path, Request
from fastapi.responses import Response from fastapi.responses import Response
@ -64,10 +65,14 @@ try:
from api.monitoring import router as monitoring_router from api.monitoring import router as monitoring_router
from core.performance import metrics_collector from core.performance import metrics_collector
from core.health import ( from core.health import (
health_monitor, DatabaseHealthCheck, WebSocketHealthCheck, health_monitor,
LobbyHealthCheck, SystemResourceHealthCheck DatabaseHealthCheck,
WebSocketHealthCheck,
LobbyHealthCheck,
SystemResourceHealthCheck,
) )
from core.cache import cache_manager from core.cache import cache_manager
monitoring_available = True monitoring_available = True
logger.info("Performance monitoring modules loaded successfully") logger.info("Performance monitoring modules loaded successfully")
except ImportError as e: except ImportError as e:
@ -100,6 +105,27 @@ app = FastAPI(
redoc_url=redoc_url, redoc_url=redoc_url,
) )
@app.middleware("http")
async def log_requests(request: Request, call_next): # type: ignore
start_time = time.time()
# Log information before the request is processed
logger.info(
f"Incoming request: Method={request.method}, URL={request.url.path}, Client={request.client.host}"
)
response = await call_next(request)
# Log information after the response is generated
process_time = time.time() - start_time
logger.info(
f"Outgoing response: Status={response.status_code}, Time Taken={process_time:.4f}s"
)
return response
logger.info(f"Starting server with public URL: {public_url}") logger.info(f"Starting server with public URL: {public_url}")
@ -123,7 +149,6 @@ async def lifespan(app: FastAPI):
bot_config_manager, \ bot_config_manager, \
websocket_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...")
@ -177,18 +202,36 @@ 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, public_url=public_url
)
# Create bot configuration API router # Create bot configuration API router
bot_config_router = create_bot_config_router(bot_config_manager, bot_manager) bot_config_router = create_bot_config_router(
bot_config_manager, bot_manager, public_url=public_url
)
logger.info(
f"Bot config router created with {len(bot_config_router.routes)} routes"
)
# 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)
app.include_router(bot_config_router, prefix=public_url.rstrip("/"))
try:
app.include_router(bot_config_router)
logger.info(
f"Bot config router included successfully with {len(bot_config_router.routes)} routes"
)
logger.info(f"Bot config router prefix: {bot_config_router.prefix}")
except Exception as e:
logger.error(f"Failed to include bot config router: {e}")
raise
logger.info(f"Total routes in app after inclusion: {len(app.routes)}")
# Add monitoring router if available # Add monitoring router if available
if monitoring_available and monitoring_router: if monitoring_available and monitoring_router:
app.include_router(monitoring_router, prefix=public_url.rstrip("/")) app.include_router(monitoring_router, prefix=public_url.rstrip("/"))
@ -197,14 +240,16 @@ async def lifespan(app: FastAPI):
# Initialize and start performance monitoring if available # Initialize and start performance monitoring if available
if monitoring_available: if monitoring_available:
logger.info("Starting performance monitoring...") logger.info("Starting performance monitoring...")
# Register health check components # Register health check components
if health_monitor: if health_monitor:
health_monitor.register_component(DatabaseHealthCheck(session_manager)) health_monitor.register_component(DatabaseHealthCheck(session_manager))
health_monitor.register_component(WebSocketHealthCheck(session_manager)) health_monitor.register_component(WebSocketHealthCheck(session_manager))
health_monitor.register_component(LobbyHealthCheck(lobby_manager)) health_monitor.register_component(LobbyHealthCheck(lobby_manager))
health_monitor.register_component(SystemResourceHealthCheck(metrics_collector)) health_monitor.register_component(
SystemResourceHealthCheck(metrics_collector)
)
# Start monitoring tasks # Start monitoring tasks
if metrics_collector: if metrics_collector:
await metrics_collector.start_collection() await metrics_collector.start_collection()
@ -214,7 +259,7 @@ async def lifespan(app: FastAPI):
await cache_manager.start_all() await cache_manager.start_all()
# Warm up caches with current data # Warm up caches with current data
await cache_manager.warm_cache(session_manager, lobby_manager) await cache_manager.warm_cache(session_manager, lobby_manager)
logger.info("Performance monitoring started successfully!") logger.info("Performance monitoring started successfully!")
else: else:
logger.info("Performance monitoring disabled - running in basic mode") logger.info("Performance monitoring disabled - running in basic mode")
@ -383,26 +428,28 @@ async def system_health():
"bot_manager": "active" if bot_manager else "inactive", "bot_manager": "active" if bot_manager else "inactive",
"websocket_manager": "active" if websocket_manager else "inactive", "websocket_manager": "active" if websocket_manager else "inactive",
} }
# Get enhanced monitoring status # Get enhanced monitoring status
monitoring_status = { monitoring_status = {
"performance_monitoring": "active" if metrics_collector else "inactive", "performance_monitoring": "active" if metrics_collector else "inactive",
"health_monitoring": "active" if health_monitor else "inactive", "health_monitoring": "active" if health_monitor else "inactive",
"cache_management": "active" if cache_manager else "inactive", "cache_management": "active" if cache_manager else "inactive",
} }
# Get basic statistics # Get basic statistics
statistics = { statistics = {
"sessions": session_manager.get_session_count() if session_manager else 0, "sessions": session_manager.get_session_count() if session_manager else 0,
"lobbies": lobby_manager.get_lobby_count() if lobby_manager else 0, "lobbies": lobby_manager.get_lobby_count() if lobby_manager else 0,
"protected_names": auth_manager.get_protection_count() if auth_manager else 0, "protected_names": auth_manager.get_protection_count()
if auth_manager
else 0,
} }
# Get performance metrics if available # Get performance metrics if available
performance_summary = {} performance_summary = {}
if metrics_collector: if metrics_collector:
performance_summary = metrics_collector.get_performance_summary() performance_summary = metrics_collector.get_performance_summary()
return { return {
"status": "ok", "status": "ok",
"architecture": "modular_with_monitoring", "architecture": "modular_with_monitoring",
@ -410,16 +457,18 @@ async def system_health():
"managers": manager_status, "managers": manager_status,
"monitoring": monitoring_status, "monitoring": monitoring_status,
"statistics": statistics, "statistics": statistics,
"performance": performance_summary.get("health_status", "unknown") if performance_summary else "unknown", "performance": performance_summary.get("health_status", "unknown")
"timestamp": datetime.now().isoformat() if performance_summary
else "unknown",
"timestamp": datetime.now().isoformat(),
} }
except Exception as e: except Exception as e:
logger.error(f"Error in system health check: {e}") logger.error(f"Error in system health check: {e}")
return { return {
"status": "error", "status": "error",
"message": str(e), "message": str(e),
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat(),
} }

View File

@ -126,8 +126,8 @@ def _setup_logging(level: str=logging_level) -> logging.Logger:
"uvicorn", "uvicorn",
"uvicorn.error", "uvicorn.error",
"uvicorn.access", "uvicorn.access",
"fastapi", # "fastapi",
"starlette", # "starlette",
): ):
logger = logging.getLogger(noisy_logger) logger = logging.getLogger(noisy_logger)
logger.setLevel(logging.WARNING) logger.setLevel(logging.WARNING)

View File

@ -402,7 +402,7 @@ class BotLobbyConfig(BaseModel):
class BotConfigUpdateRequest(BaseModel): class BotConfigUpdateRequest(BaseModel):
"""Request to update bot configuration""" """Request to update bot configuration"""
bot_name: str bot_instance_id: str
lobby_id: str lobby_id: str
config_values: Dict[str, Any] config_values: Dict[str, Any]
@ -467,7 +467,7 @@ class BotProviderPublicModel(BaseModel):
last_seen: float last_seen: float
class BotProviderListResponse(BaseModel): class kBotProviderListResponse(BaseModel):
"""Response listing all registered bot providers""" """Response listing all registered bot providers"""
providers: List[BotProviderPublicModel] providers: List[BotProviderPublicModel]

View File

@ -243,12 +243,16 @@ def discover_bots() -> "List[BotInfoModel]":
if hasattr(mod, "agent_info") and callable(getattr(mod, "agent_info")): if hasattr(mod, "agent_info") and callable(getattr(mod, "agent_info")):
try: try:
info = mod.agent_info() info = mod.agent_info()
# Convert string has_media to boolean for compatibility # Convert string has_media and configurable to boolean for compatibility
processed_info = dict(info) processed_info = dict(info)
has_media_value = processed_info.get("has_media", True) has_media_value = processed_info.get("has_media", True)
if isinstance(has_media_value, str): if isinstance(has_media_value, str):
processed_info["has_media"] = has_media_value.lower() in ("true", "1", "yes") processed_info["has_media"] = has_media_value.lower() in ("true", "1", "yes")
configurable_value = processed_info.get("configurable", False)
if isinstance(configurable_value, str):
processed_info["configurable"] = configurable_value.lower() in ("true", "1", "yes")
# Create BotInfoModel using model_validate # Create BotInfoModel using model_validate
bot_info = BotInfoModel.model_validate(processed_info) bot_info = BotInfoModel.model_validate(processed_info)
bots.append(bot_info) bots.append(bot_info)
@ -279,6 +283,8 @@ def discover_bots() -> "List[BotInfoModel]":
try: try:
config_schema = mod.get_config_schema() config_schema = mod.get_config_schema()
logger.info(f"Bot {bot_info.name} supports configuration") logger.info(f"Bot {bot_info.name} supports configuration")
# Set the config_schema on the BotInfoModel
bot_info.config_schema = config_schema
except Exception as e: except Exception as e:
logger.warning(f"Failed to get config schema for {bot_info.name}: {e}") logger.warning(f"Failed to get config schema for {bot_info.name}: {e}")

0
voicebot/entrypoint.sh Normal file → Executable file
View File

View File

@ -5,12 +5,164 @@ description = "AI Voicebot Environment"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"about-time>=4.2.1",
"aiofiles>=24.1.0",
"aiohappyeyeballs>=2.6.1",
"aiohttp>=3.12.15",
"aioice>=0.10.1",
"aiortc>=1.13.0", "aiortc>=1.13.0",
"numpy<2.3", "aiosignal>=1.4.0",
"opencv-python>=4.11.0.86", "alive-progress>=3.2.0",
"pydantic>=2.11.7", "annotated-types>=0.7.0",
"anyio>=4.10.0",
"attrs>=25.3.0",
"audioread>=3.0.1",
"autograd>=1.8.0",
"av>=14.4.0",
"brotli>=1.1.0",
"certifi>=2025.8.3",
"cffi>=2.0.0",
"charset-normalizer>=3.4.3",
"click>=8.2.1",
"cma>=4.3.0",
"contourpy>=1.3.3",
"cryptography>=45.0.7",
"cycler>=0.12.1",
"datasets>=4.1.0",
"decorator>=5.2.1",
"deprecated>=1.2.18",
"dill>=0.4.0",
"dnspython>=2.8.0",
"fastapi>=0.116.1",
"ffmpy>=0.6.1",
"filelock>=3.19.1",
"fonttools>=4.59.2",
"frozenlist>=1.7.0",
"fsspec>=2025.9.0",
"google-crc32c>=1.7.1",
"gradio>=5.45.0",
"gradio-client>=1.13.0",
"graphemeu>=0.8.0",
"groovy>=0.1.2",
"h11>=0.16.0",
"hf-xet>=1.1.10",
"httpcore>=1.0.9",
"httpx>=0.28.1",
"huggingface-hub>=0.34.5",
"idna>=3.10",
"ifaddr>=0.2.0",
"iniconfig>=2.1.0",
"jinja2>=3.1.6",
"jiwer>=4.0.0",
"joblib>=1.5.2",
"jsonschema>=4.25.1",
"jsonschema-specifications>=2025.9.1",
"kiwisolver>=1.4.9",
"lazy-loader>=0.4",
"librosa>=0.11.0",
"llvmlite>=0.44.0",
"markdown-it-py>=4.0.0",
"markupsafe>=3.0.2",
"matplotlib>=3.10.6",
"mdurl>=0.1.2",
"ml-dtypes>=0.5.3",
"more-itertools>=10.8.0",
"mpmath>=1.3.0",
"msgpack>=1.1.1",
"multidict>=6.6.4",
"multiprocess>=0.70.16",
"natsort>=8.4.0",
"networkx>=3.4.2",
"ninja>=1.13.0",
"nncf>=2.18.0",
"numba>=0.61.2",
"numpy>=2.2.6",
"onnx>=1.19.0",
"openai-whisper",
"opencv-python>=4.12.0.88",
"openvino>=2025.3.0",
"openvino-genai>=2025.3.0.0",
"openvino-telemetry>=2025.2.0",
"openvino-tokenizers>=2025.3.0.0",
"optimum>=1.27.0",
"optimum-intel",
"orjson>=3.11.3",
"packaging>=25.0",
"pandas>=2.3.2",
"pillow>=11.3.0",
"platformdirs>=4.4.0",
"pluggy>=1.6.0",
"pooch>=1.8.2",
"propcache>=0.3.2",
"protobuf>=6.32.1",
"psutil>=7.0.0",
"pyarrow>=21.0.0",
"pycparser>=2.23",
"pydantic>=2.11.9",
"pydantic-core>=2.33.2",
"pydot>=3.0.4",
"pydub>=0.25.1",
"pyee>=13.0.0",
"pygments>=2.19.2",
"pylibsrtp>=0.12.0",
"pymoo>=0.6.1.5",
"pyopencl>=2025.2.6",
"pyopenssl>=25.2.0",
"pyparsing>=3.2.4",
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"python-dateutil>=2.9.0.post0",
"python-ffmpeg>=2.0.12",
"python-multipart>=0.0.20",
"pytools>=2025.2.4",
"pytz>=2025.2",
"pyyaml>=6.0.2",
"rapidfuzz>=3.14.1",
"referencing>=0.36.2",
"regex>=2025.9.1",
"requests>=2.32.5",
"resampy>=0.4.3",
"rich>=14.1.0",
"rpds-py>=0.27.1",
"ruff>=0.13.0",
"safehttpx>=0.1.6",
"safetensors>=0.6.2",
"scikit-learn>=1.7.2",
"scipy>=1.16.2",
"semantic-version>=2.10.0",
"setuptools>=80.9.0",
"shellingham>=1.5.4",
"siphash24>=1.8",
"six>=1.17.0",
"sniffio>=1.3.1",
"soundfile>=0.13.1",
"soxr>=1.0.0",
"speechrecognition>=3.14.3",
"starlette>=0.47.3",
"sympy>=1.14.0",
"tabulate>=0.9.0",
"threadpoolctl>=3.6.0",
"tiktoken>=0.11.0",
"tokenizers>=0.21.4",
"tomlkit>=0.13.3",
"torch>=2.8.0",
"torchvision>=0.23.0",
"tqdm>=4.67.1",
"transformers>=4.53.3",
"triton>=3.4.0",
"typer>=0.17.4",
"typing-extensions>=4.15.0",
"typing-inspection>=0.4.1",
"tzdata>=2025.2",
"urllib3>=2.5.0",
"uvicorn>=0.35.0",
"watchdog>=6.0.0",
"websockets>=15.0.1", "websockets>=15.0.1",
"librosa>=0.10.0", "wrapt>=1.17.3",
"torch>=2.0.0", "xxhash>=3.5.0",
"openai-whisper>=20231117", "yarl>=1.20.1",
] ]
[tool.uv.sources]
openai-whisper = { git = "https://github.com/openai/whisper.git", rev = "c0d2f624c09dc18e709e37c2ad90c039a4eb72a2" }
optimum-intel = { git = "https://github.com/huggingface/optimum-intel.git", rev = "b9c151fec6b414d9ca78be8643d08e267b133bfc" }

View File

@ -1,171 +1,156 @@
about-time==4.2.1 about-time
-e file:///voicebot aiofiles
aiofiles==24.1.0 aiohappyeyeballs
aiohappyeyeballs==2.6.1 aiohttp
aiohttp==3.12.15 aioice
aioice==0.10.1 aiortc
aiortc==1.13.0 aiosignal
aiosignal==1.4.0 alive-progress
alive-progress==3.3.0 annotated-types
annotated-types==0.7.0 anyio
anyio==4.10.0 attrs
attrs==25.3.0 audioread
audioread==3.0.1 autograd
autograd==1.8.0 av
av==14.4.0 brotli
brotli==1.1.0 certifi
certifi==2025.8.3 cffi
cffi==1.17.1 charset-normalizer
charset-normalizer==3.4.3 click
click==8.2.1 cma
cma==4.3.0 contourpy
contourpy==1.3.3 cryptography
cryptography==45.0.7 cycler
cycler==0.12.1 datasets
datasets==4.0.0 decorator
decorator==5.2.1 deprecated
deprecated==1.2.18 dill
dill==0.3.8 dnspython
dnspython==2.7.0 fastapi
fastapi==0.116.1 ffmpy
ffmpy==0.6.1 filelock
filelock==3.19.1 fonttools
fonttools==4.59.2 frozenlist
frozenlist==1.7.0 fsspec
fsspec==2025.3.0 google-crc32c
google-crc32c==1.7.1 gradio
gradio==5.44.1 gradio-client
gradio-client==1.12.1 graphemeu
graphemeu==0.7.2 groovy
groovy==0.1.2 h11
h11==0.16.0 hf-xet
hf-xet==1.1.9 httpcore
httpcore==1.0.9 httpx
httpx==0.28.1 huggingface-hub
huggingface-hub==0.34.4 idna
idna==3.10 ifaddr
ifaddr==0.2.0 iniconfig
iniconfig==2.1.0 jinja2
jinja2==3.1.6 jiwer
jiwer==4.0.0 joblib
joblib==1.5.2 jsonschema
jsonschema==4.25.1 jsonschema-specifications
jsonschema-specifications==2025.9.1 kiwisolver
kiwisolver==1.4.9 lazy-loader
lazy-loader==0.4 librosa
librosa==0.11.0 llvmlite
llvmlite==0.44.0 markdown-it-py
markdown-it-py==4.0.0 markupsafe
markupsafe==3.0.2 matplotlib
matplotlib==3.10.6 mdurl
mdurl==0.1.2 ml-dtypes
ml-dtypes==0.5.3 more-itertools
more-itertools==10.8.0 mpmath
mpmath==1.3.0 msgpack
msgpack==1.1.1 multidict
multidict==6.6.4 multiprocess
multiprocess==0.70.16 natsort
natsort==8.4.0 networkx
networkx==3.5 ninja
ninja==1.13.0 nncf
nncf==2.18.0 numba
numba==0.61.2 numpy
numpy==2.2.6 onnx
nvidia-cublas-cu12==12.8.4.1
nvidia-cuda-cupti-cu12==12.8.90
nvidia-cuda-nvrtc-cu12==12.8.93
nvidia-cuda-runtime-cu12==12.8.90
nvidia-cudnn-cu12==9.10.2.21
nvidia-cufft-cu12==11.3.3.83
nvidia-cufile-cu12==1.13.1.3
nvidia-curand-cu12==10.3.9.90
nvidia-cusolver-cu12==11.7.3.90
nvidia-cusparse-cu12==12.5.8.93
nvidia-cusparselt-cu12==0.7.1
nvidia-nccl-cu12==2.27.3
nvidia-nvjitlink-cu12==12.8.93
nvidia-nvtx-cu12==12.8.90
onnx==1.19.0
openai-whisper @ git+https://github.com/openai/whisper.git@c0d2f624c09dc18e709e37c2ad90c039a4eb72a2 openai-whisper @ git+https://github.com/openai/whisper.git@c0d2f624c09dc18e709e37c2ad90c039a4eb72a2
opencv-python==4.11.0.86 opencv-python
openvino==2025.3.0 openvino
openvino-genai==2025.3.0.0 openvino-genai
openvino-telemetry==2025.2.0 openvino-telemetry
openvino-tokenizers==2025.3.0.0 openvino-tokenizers
optimum==1.27.0 optimum
optimum-intel @ git+https://github.com/huggingface/optimum-intel.git@b9c151fec6b414d9ca78be8643d08e267b133bfc optimum-intel @ git+https://github.com/huggingface/optimum-intel.git@b9c151fec6b414d9ca78be8643d08e267b133bfc
orjson==3.11.3 orjson
packaging==25.0 packaging
pandas==2.3.2 pandas
pillow==11.3.0 pillow
platformdirs==4.4.0 platformdirs
pluggy==1.6.0 pluggy
pooch==1.8.2 pooch
propcache==0.3.2 propcache
protobuf==6.32.0 protobuf
psutil==7.0.0 psutil
pyarrow==21.0.0 pyarrow
pycparser==2.22 pycparser
pydantic==2.11.7 pydantic
pydantic-core==2.33.2 pydantic-core
pydot==3.0.4 pydot
pydub==0.25.1 pydub
pyee==13.0.0 pyee
pygments==2.19.2 pygments
pylibsrtp==0.12.0 pylibsrtp
pymoo==0.6.1.5 pymoo
pyopencl==2025.2.6 pyopencl
pyopenssl==25.1.0 pyopenssl
pyparsing==3.2.3 pyparsing
pytest==8.4.2 pytest
pytest-asyncio==1.1.0 pytest-asyncio
python-dateutil==2.9.0.post0 python-dateutil
python-ffmpeg==1.0.16 python-ffmpeg
python-multipart==0.0.20 python-multipart
pytools==2025.2.4 pytools
pytz==2025.2 pytz
pyyaml==6.0.2 pyyaml
rapidfuzz==3.14.0 rapidfuzz
referencing==0.36.2 referencing
regex==2025.9.1 regex
requests==2.32.5 requests
resampy==0.4.3 resampy
rich==14.1.0 rich
rpds-py==0.27.1 rpds-py
ruff==0.12.11 ruff
safehttpx==0.1.6 safehttpx
safetensors==0.6.2 safetensors
scikit-learn==1.7.1 scikit-learn
scipy==1.16.1 scipy
semantic-version==2.10.0 semantic-version
setuptools==80.9.0 setuptools
shellingham==1.5.4 shellingham
siphash24==1.8 siphash24
six==1.17.0 six
sniffio==1.3.1 sniffio
soundfile==0.13.1 soundfile
soxr==0.5.0.post1 soxr
speechrecognition==3.14.3 speechrecognition
starlette==0.47.3 starlette
sympy==1.14.0 sympy
tabulate==0.9.0 tabulate
threadpoolctl==3.6.0 threadpoolctl
tiktoken==0.11.0 tiktoken
tokenizers==0.21.4 tokenizers
tomlkit==0.13.3 tomlkit
torch==2.8.0 torch
torchvision==0.23.0 torchvision
tqdm==4.67.1 tqdm
transformers==4.53.3 transformers
triton==3.4.0 triton
typer==0.17.3 typer
typing-extensions==4.15.0 typing-extensions
typing-inspection==0.4.1 typing-inspection
tzdata==2025.2 tzdata
urllib3==2.5.0 urllib3
uvicorn==0.35.0 uvicorn
watchdog==6.0.0 watchdog
websockets==15.0.1 websockets
wrapt==1.17.3 wrapt
xxhash==3.5.0 xxhash
yarl==1.20.1 yarl

2837
voicebot/uv.lock generated

File diff suppressed because it is too large Load Diff