diff --git a/client/openapi-schema.json b/client/openapi-schema.json index 6ee8b14..c1edf97 100644 --- a/client/openapi-schema.json +++ b/client/openapi-schema.json @@ -213,6 +213,24 @@ "get": { "summary": "Get Session", "operationId": "get_session_ai_voicebot_api_session_get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id" + } + } + ], "responses": { "200": { "description": "Successful Response", @@ -223,6 +241,16 @@ } } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } } } } @@ -1223,9 +1251,9 @@ } }, "/ai-voicebot/{path}": { - "patch": { + "post": { "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", + "operationId": "proxy_static_ai_voicebot__path__post", "parameters": [ { "name": "path", @@ -1258,9 +1286,9 @@ } } }, - "get": { + "patch": { "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", + "operationId": "proxy_static_ai_voicebot__path__post", "parameters": [ { "name": "path", @@ -1295,42 +1323,7 @@ }, "delete": { "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", - "parameters": [ - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Path" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - }, - "options": { - "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", + "operationId": "proxy_static_ai_voicebot__path__post", "parameters": [ { "name": "path", @@ -1365,42 +1358,7 @@ }, "head": { "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", - "parameters": [ - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Path" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - }, - "post": { - "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", + "operationId": "proxy_static_ai_voicebot__path__post", "parameters": [ { "name": "path", @@ -1435,7 +1393,77 @@ }, "put": { "summary": "Proxy Static", - "operationId": "proxy_static_ai_voicebot__path__patch", + "operationId": "proxy_static_ai_voicebot__path__post", + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Path" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "options": { + "summary": "Proxy Static", + "operationId": "proxy_static_ai_voicebot__path__post", + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Path" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "summary": "Proxy Static", + "operationId": "proxy_static_ai_voicebot__path__post", "parameters": [ { "name": "path", @@ -2134,7 +2162,7 @@ "properties": { "providers": { "items": { - "$ref": "#/components/schemas/BotProviderModel" + "$ref": "#/components/schemas/BotProviderPublicModel" }, "type": "array", "title": "Providers" @@ -2147,16 +2175,12 @@ "title": "BotProviderListResponse", "description": "Response listing all registered bot providers" }, - "BotProviderModel": { + "BotProviderPublicModel": { "properties": { "provider_id": { "type": "string", "title": "Provider Id" }, - "base_url": { - "type": "string", - "title": "Base Url" - }, "name": { "type": "string", "title": "Name" @@ -2166,10 +2190,6 @@ "title": "Description", "default": "" }, - "provider_key": { - "type": "string", - "title": "Provider Key" - }, "registered_at": { "type": "number", "title": "Registered At" @@ -2182,14 +2202,12 @@ "type": "object", "required": [ "provider_id", - "base_url", "name", - "provider_key", "registered_at", "last_seen" ], - "title": "BotProviderModel", - "description": "Bot provider registration information" + "title": "BotProviderPublicModel", + "description": "Public bot provider information (safe for frontend)" }, "BotProviderRegisterRequest": { "properties": { @@ -2209,13 +2227,18 @@ "provider_key": { "type": "string", "title": "Provider Key" + }, + "provider_id": { + "type": "string", + "title": "Provider Id" } }, "type": "object", "required": [ "base_url", "name", - "provider_key" + "provider_key", + "provider_id" ], "title": "BotProviderRegisterRequest", "description": "Request to register a bot provider" diff --git a/client/src/BotManager.tsx b/client/src/BotManager.tsx index 4de6601..4105892 100644 --- a/client/src/BotManager.tsx +++ b/client/src/BotManager.tsx @@ -27,7 +27,7 @@ import { Refresh as RefreshIcon, Settings as SettingsIcon, } from "@mui/icons-material"; -import { botsApi, BotInfoModel, BotProviderModel, BotJoinLobbyRequest } from "./api-client"; +import { botsApi, BotInfoModel, BotProviderPublicModel, BotJoinLobbyRequest } from "./api-client"; import BotConfig from "./BotConfig"; import BotConfigComponent from "./BotConfig"; @@ -40,7 +40,7 @@ interface BotManagerProps { const BotManager: React.FC = ({ lobbyId, onBotAdded, sx }) => { const [bots, setBots] = useState([]); const [providers, setProviders] = useState>({}); - const [botProviders, setBotProviders] = useState([]); + const [botProviders, setBotProviders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); @@ -77,7 +77,7 @@ const BotManager: React.FC = ({ lobbyId, onBotAdded, sx }) => { setAddingBot(true); setError(""); // Clear any previous errors - + try { const request: BotJoinLobbyRequest = { lobby_id: lobbyId, @@ -88,21 +88,21 @@ const BotManager: React.FC = ({ lobbyId, onBotAdded, sx }) => { // Retry logic for handling service restart scenarios let retries = 3; let response; - + while (retries > 0) { try { response = await botsApi.requestJoinLobby(selectedBot, request); break; // Success, exit retry loop } catch (err: any) { retries--; - + // If it's a 404 error and we have retries left, wait and retry if (err?.status === 404 && retries > 0) { console.log(`Bot join failed with 404, retrying... (${retries} attempts left)`); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second continue; } - + // If it's not a 404 or we're out of retries, throw the error throw err; } @@ -272,9 +272,6 @@ const BotManager: React.FC = ({ lobbyId, onBotAdded, sx }) => { {provider.description || "No description"} - - {provider.base_url} - } secondaryTypographyProps={{ component: "div" }} diff --git a/client/src/api-client.ts b/client/src/api-client.ts index 0151198..0fd9ea9 100644 --- a/client/src/api-client.ts +++ b/client/src/api-client.ts @@ -28,7 +28,7 @@ export type BotLeaveLobbyResponse = components["schemas"]["BotLeaveLobbyResponse export type BotListResponse = components["schemas"]["BotListResponse"]; export type BotLobbyConfig = components["schemas"]["BotLobbyConfig"]; export type BotProviderListResponse = components["schemas"]["BotProviderListResponse"]; -export type BotProviderModel = components["schemas"]["BotProviderModel"]; +export type BotProviderPublicModel = components["schemas"]["BotProviderPublicModel"]; export type BotProviderRegisterRequest = components["schemas"]["BotProviderRegisterRequest"]; export type BotProviderRegisterResponse = components["schemas"]["BotProviderRegisterResponse"]; export type ChatMessageModel = components["schemas"]["ChatMessageModel"]; @@ -277,33 +277,33 @@ export class ApiClient { return this.request(this.getApiPath(`/ai-voicebot/api/metrics/export`), { method: "GET" }); } - async updateProxyStatic(path: string): Promise { - return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" }); + async createProxyStatic(path: string): Promise { + return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" }); } - async getProxyStatic(path: string): Promise { - return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" }); + async updateProxyStatic(path: string): Promise { + return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" }); } async deleteProxyStatic(path: string): Promise { return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "DELETE" }); } - async optionsProxyStatic(path: string): Promise { - return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "OPTIONS" }); - } - async headProxyStatic(path: string): Promise { return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "HEAD" }); } - async createProxyStatic(path: string): Promise { - return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" }); - } - async replaceProxyStatic(path: string): Promise { return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "PUT" }); } + + async optionsProxyStatic(path: string): Promise { + return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "OPTIONS" }); + } + + async getProxyStatic(path: string): Promise { + return this.request(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" }); + } } // Default client instance diff --git a/client/src/api-types.ts b/client/src/api-types.ts index cfff621..3e8eb28 100644 --- a/client/src/api-types.ts +++ b/client/src/api-types.ts @@ -263,19 +263,19 @@ export interface paths { }; "/ai-voicebot/{path}": { /** Proxy Static */ - get: operations["proxy_static_ai_voicebot__path__patch"]; + get: operations["proxy_static_ai_voicebot__path__post"]; /** Proxy Static */ - put: operations["proxy_static_ai_voicebot__path__patch"]; + put: operations["proxy_static_ai_voicebot__path__post"]; /** Proxy Static */ - post: operations["proxy_static_ai_voicebot__path__patch"]; + post: operations["proxy_static_ai_voicebot__path__post"]; /** Proxy Static */ - delete: operations["proxy_static_ai_voicebot__path__patch"]; + delete: operations["proxy_static_ai_voicebot__path__post"]; /** Proxy Static */ - options: operations["proxy_static_ai_voicebot__path__patch"]; + options: operations["proxy_static_ai_voicebot__path__post"]; /** Proxy Static */ - head: operations["proxy_static_ai_voicebot__path__patch"]; + head: operations["proxy_static_ai_voicebot__path__post"]; /** Proxy Static */ - patch: operations["proxy_static_ai_voicebot__path__patch"]; + patch: operations["proxy_static_ai_voicebot__path__post"]; }; } @@ -587,17 +587,15 @@ export interface components { */ BotProviderListResponse: { /** Providers */ - providers: components["schemas"]["BotProviderModel"][]; + providers: components["schemas"]["BotProviderPublicModel"][]; }; /** - * BotProviderModel - * @description Bot provider registration information + * BotProviderPublicModel + * @description Public bot provider information (safe for frontend) */ - BotProviderModel: { + BotProviderPublicModel: { /** Provider Id */ provider_id: string; - /** Base Url */ - base_url: string; /** Name */ name: string; /** @@ -605,8 +603,6 @@ export interface components { * @default */ description?: string; - /** Provider Key */ - provider_key: string; /** Registered At */ registered_at: number; /** Last Seen */ @@ -628,6 +624,8 @@ export interface components { description?: string; /** Provider Key */ provider_key: string; + /** Provider Id */ + provider_id: string; }; /** * BotProviderRegisterResponse @@ -944,6 +942,11 @@ export interface operations { }; /** Get Session */ get_session_ai_voicebot_api_session_get: { + parameters: { + cookie?: { + session_id?: string | null; + }; + }; responses: { /** @description Successful Response */ 200: { @@ -951,6 +954,12 @@ export interface operations { "application/json": components["schemas"]["SessionResponse"]; }; }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; }; }; /** List Lobbies */ @@ -1568,7 +1577,7 @@ export interface operations { }; }; /** Proxy Static */ - proxy_static_ai_voicebot__path__patch: { + proxy_static_ai_voicebot__path__post: { parameters: { path: { path: string; diff --git a/server/core/bot_manager.py b/server/core/bot_manager.py index e6b3df6..8a49b83 100644 --- a/server/core/bot_manager.py +++ b/server/core/bot_manager.py @@ -19,6 +19,7 @@ sys.path.append( ) from shared.models import ( BotProviderModel, + BotProviderPublicModel, BotProviderRegisterRequest, BotProviderRegisterResponse, BotProviderListResponse, @@ -190,23 +191,20 @@ class BotManager: logger.warning(f"Rejected bot provider registration with invalid key: {request.provider_key}") raise ValueError("Invalid provider key. Bot provider is not authorized to register.") - # Check if there's already an active provider with this key and remove it - providers_to_remove: List[str] = [] - with self.lock: - for existing_provider_id, existing_provider in self.bot_providers.items(): - if existing_provider.provider_key == request.provider_key: - providers_to_remove.append(existing_provider_id) - logger.info(f"Removing stale bot provider: {existing_provider.name} (ID: {existing_provider_id})") + # Require provider_id for static registration + if not request.provider_id: + logger.warning("Rejected bot provider registration without provider_id") + raise ValueError("provider_id is required for bot provider registration.") - # Remove stale providers - for provider_id_to_remove in providers_to_remove: - del self.bot_providers[provider_id_to_remove] - - # Save after removing stale providers - if providers_to_remove: - self._save_bot_providers() + provider_id = request.provider_id - provider_id = str(uuid.uuid4()) + # Remove any existing provider with the same provider_id (this handles restarts/re-registration) + with self.lock: + if provider_id in self.bot_providers: + logger.info( + f"Removing existing provider with ID: {provider_id} for re-registration" + ) + del self.bot_providers[provider_id] now = time.time() provider = BotProviderModel( @@ -232,9 +230,21 @@ class BotManager: return BotProviderRegisterResponse(provider_id=provider_id) def list_providers(self) -> BotProviderListResponse: - """List all registered bot providers""" + """List all registered bot providers (public information only)""" with self.lock: - return BotProviderListResponse(providers=list(self.bot_providers.values())) + # Create safe public versions of provider info + public_providers: List[BotProviderPublicModel] = [] + for provider in self.bot_providers.values(): + public_provider = BotProviderPublicModel( + provider_id=provider.provider_id, + name=provider.name, + description=provider.description, + registered_at=provider.registered_at, + last_seen=provider.last_seen + ) + public_providers.append(public_provider) + + return BotProviderListResponse(providers=public_providers) async def list_bots(self) -> BotListResponse: """List all available bots from all registered providers""" diff --git a/shared/models.py b/shared/models.py index b58356a..150bf5c 100644 --- a/shared/models.py +++ b/shared/models.py @@ -447,6 +447,7 @@ class BotProviderRegisterRequest(BaseModel): name: str description: str = "" provider_key: str + provider_id: str # Required static provider ID class BotProviderRegisterResponse(BaseModel): @@ -456,10 +457,20 @@ class BotProviderRegisterResponse(BaseModel): status: str = "registered" +class BotProviderPublicModel(BaseModel): + """Public bot provider information (safe for frontend)""" + + provider_id: str + name: str + description: str = "" + registered_at: float + last_seen: float + + class BotProviderListResponse(BaseModel): """Response listing all registered bot providers""" - providers: List[BotProviderModel] + providers: List[BotProviderPublicModel] class BotListResponse(BaseModel): diff --git a/voicebot/bot_orchestrator.py b/voicebot/bot_orchestrator.py index 838337d..b71889c 100644 --- a/voicebot/bot_orchestrator.py +++ b/voicebot/bot_orchestrator.py @@ -529,16 +529,24 @@ async def register_with_server(server_url: str, voicebot_url: str, insecure: boo # Import httpx locally to avoid dependency issues import httpx - # Get provider key from environment variable - provider_key = os.getenv('VOICEBOT_PROVIDER_KEY', 'default-voicebot') + # Get provider key and ID from environment variables + provider_key = os.getenv('VOICEBOT_PROVIDER_KEY') + provider_id = os.getenv('VOICEBOT_PROVIDER_ID') + + if not provider_key: + raise ValueError("VOICEBOT_PROVIDER_KEY environment variable is required") + if not provider_id: + raise ValueError("VOICEBOT_PROVIDER_ID environment variable is required") payload = { "base_url": voicebot_url.rstrip('/'), "name": "voicebot-provider", "description": "AI voicebot provider with speech recognition and synthetic media capabilities", - "provider_key": provider_key + "provider_key": provider_key, + "provider_id": provider_id } + logger.info(f"📍 Using static provider ID: {provider_id}") logger.info(f"📤 Sending registration payload: {payload}") # Prepare SSL context if needed diff --git a/voicebot/bots/whisper.py b/voicebot/bots/whisper.py index c42ae60..db20e17 100644 --- a/voicebot/bots/whisper.py +++ b/voicebot/bots/whisper.py @@ -265,6 +265,7 @@ async def handle_track_received(peer: Peer, track: MediaStreamTrack): # Receive audio frame frame = await track.recv() if isinstance(frame, AudioFrame): + logger.info(f"Received audio frame: {frame.sample_rate}Hz, {frame.format.name}, {frame.layout.name}") # Convert AudioFrame to numpy array audio_data = frame.to_ndarray() @@ -292,6 +293,8 @@ async def handle_track_received(peer: Peer, track: MediaStreamTrack): # Send to audio processor if _audio_processor: _audio_processor.add_audio_data(audio_data_float32) + else: + logger.warning(f"Received non-audio frame on audio track from {peer.peer_name}") except Exception as e: logger.error(f"Error processing audio track from {peer.peer_name}: {e}", exc_info=True)