Reworking auto type system
This commit is contained in:
parent
4b33b40637
commit
d679c8cecf
@ -1,250 +1,16 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FastAPI",
|
||||
"version": "0.1.0"
|
||||
"title": "AI Voice Bot Server (Refactored)",
|
||||
"description": "WebRTC voice chat server with modular architecture",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/ai-voicebot/api/admin/names": {
|
||||
"/ai-voicebot/api/system/health": {
|
||||
"get": {
|
||||
"summary": "Admin List Names",
|
||||
"operationId": "admin_list_names_ai_voicebot_api_admin_names_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminNamesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/api/admin/set_password": {
|
||||
"post": {
|
||||
"summary": "Admin Set Password",
|
||||
"operationId": "admin_set_password_ai_voicebot_api_admin_set_password_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminSetPassword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/api/admin/clear_password": {
|
||||
"post": {
|
||||
"summary": "Admin Clear Password",
|
||||
"operationId": "admin_clear_password_ai_voicebot_api_admin_clear_password_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminClearPassword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/api/health": {
|
||||
"get": {
|
||||
"summary": "Health",
|
||||
"operationId": "health_ai_voicebot_api_health_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HealthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/api/session": {
|
||||
"get": {
|
||||
"summary": "Session",
|
||||
"operationId": "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",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/api/lobby": {
|
||||
"get": {
|
||||
"summary": "Get Lobbies",
|
||||
"operationId": "get_lobbies_ai_voicebot_api_lobby_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LobbiesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/api/lobby/{session_id}": {
|
||||
"post": {
|
||||
"summary": "Lobby Create",
|
||||
"operationId": "lobby_create_ai_voicebot_api_lobby__session_id__post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Session Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LobbyCreateRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LobbyCreateResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai-voicebot/{path}": {
|
||||
"put": {
|
||||
"summary": "Proxy Static",
|
||||
"operationId": "proxy_static_ai_voicebot__path__put",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Path"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "System Health",
|
||||
"description": "System health check showing manager status and enhanced monitoring",
|
||||
"operationId": "system_health_ai_voicebot_api_system_health_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
@ -253,528 +19,8 @@
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"summary": "Proxy Static",
|
||||
"operationId": "proxy_static_ai_voicebot__path__put",
|
||||
"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__put",
|
||||
"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__put",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Proxy Static",
|
||||
"operationId": "proxy_static_ai_voicebot__path__put",
|
||||
"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__put",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"summary": "Proxy Static",
|
||||
"operationId": "proxy_static_ai_voicebot__path__put",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"AdminActionResponse": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ok",
|
||||
"not_found"
|
||||
],
|
||||
"title": "Status"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status",
|
||||
"name"
|
||||
],
|
||||
"title": "AdminActionResponse",
|
||||
"description": "Response for admin actions"
|
||||
},
|
||||
"AdminClearPassword": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "AdminClearPassword",
|
||||
"description": "Request model for clearing admin password"
|
||||
},
|
||||
"AdminNamesResponse": {
|
||||
"properties": {
|
||||
"name_passwords": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/NamePasswordRecord"
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Name Passwords"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name_passwords"
|
||||
],
|
||||
"title": "AdminNamesResponse",
|
||||
"description": "Response for admin names endpoint"
|
||||
},
|
||||
"AdminSetPassword": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"title": "Password"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"password"
|
||||
],
|
||||
"title": "AdminSetPassword",
|
||||
"description": "Request model for setting admin password"
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Detail"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError"
|
||||
},
|
||||
"HealthResponse": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"title": "Status"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "HealthResponse",
|
||||
"description": "Health check response"
|
||||
},
|
||||
"LobbiesResponse": {
|
||||
"properties": {
|
||||
"lobbies": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LobbyListItem"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Lobbies"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"lobbies"
|
||||
],
|
||||
"title": "LobbiesResponse",
|
||||
"description": "Response containing list of lobbies"
|
||||
},
|
||||
"LobbyCreateData": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"private": {
|
||||
"type": "boolean",
|
||||
"title": "Private",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "LobbyCreateData",
|
||||
"description": "Data for lobby creation"
|
||||
},
|
||||
"LobbyCreateRequest": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "lobby_create",
|
||||
"title": "Type"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/LobbyCreateData"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"data"
|
||||
],
|
||||
"title": "LobbyCreateRequest",
|
||||
"description": "Request for creating a lobby"
|
||||
},
|
||||
"LobbyCreateResponse": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "lobby_created",
|
||||
"title": "Type"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/LobbyModel"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"data"
|
||||
],
|
||||
"title": "LobbyCreateResponse",
|
||||
"description": "Response for lobby creation"
|
||||
},
|
||||
"LobbyListItem": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"title": "LobbyListItem",
|
||||
"description": "Lobby item for list responses"
|
||||
},
|
||||
"LobbyModel": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"private": {
|
||||
"type": "boolean",
|
||||
"title": "Private",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"title": "LobbyModel",
|
||||
"description": "Core lobby model used across components"
|
||||
},
|
||||
"NamePasswordRecord": {
|
||||
"properties": {
|
||||
"salt": {
|
||||
"type": "string",
|
||||
"title": "Salt"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"title": "Hash"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"salt",
|
||||
"hash"
|
||||
],
|
||||
"title": "NamePasswordRecord",
|
||||
"description": "Password hash record for reserved names"
|
||||
},
|
||||
"SessionResponse": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"lobbies": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LobbyModel"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Lobbies"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"lobbies"
|
||||
],
|
||||
"title": "SessionResponse",
|
||||
"description": "Session response model"
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Error Type"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"title": "ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ type User = {
|
||||
live: boolean;
|
||||
local: boolean /* Client side variable */;
|
||||
protected?: boolean;
|
||||
is_bot?: boolean;
|
||||
has_media?: boolean; // Whether this user provides audio/video streams
|
||||
bot_run_id?: string;
|
||||
bot_provider_id?: string;
|
||||
@ -49,7 +48,7 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||
const apiClient = new ApiClient();
|
||||
|
||||
const handleBotLeave = async (user: User) => {
|
||||
if (!user.is_bot || !user.bot_instance_id) return;
|
||||
if (!user.bot_instance_id) return;
|
||||
|
||||
setLeavingBots((prev) => new Set(prev).add(user.session_id));
|
||||
|
||||
@ -168,13 +167,13 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||
🔒
|
||||
</div>
|
||||
)}
|
||||
{user.is_bot && (
|
||||
{user.bot_instance_id && (
|
||||
<div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
|
||||
🤖
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
{user.is_bot && (
|
||||
{user.bot_instance_id && (
|
||||
<Box style={{ display: "flex-wrap", gap: "4px", border: "3px solid magenta" }}>
|
||||
{user.bot_run_id && (
|
||||
<IconButton
|
||||
|
@ -248,6 +248,11 @@ export class ApiClient {
|
||||
// Auto-generated endpoints will be added here by update-api-client.js
|
||||
// DO NOT MANUALLY EDIT BELOW THIS LINE
|
||||
|
||||
// Auto-generated endpoints
|
||||
async systemHealth(): Promise<any> {
|
||||
return this.request<any>(this.getApiPath("/ai-voicebot/api/system/health"), { method: "GET" });
|
||||
}
|
||||
|
||||
// Auto-generated endpoints
|
||||
async lobbyCreate(session_id: string, data: any): Promise<any> {
|
||||
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${session_id}`), { method: "POST", body: data });
|
||||
@ -282,6 +287,7 @@ class ApiEvolutionChecker {
|
||||
'GET:/ai-voicebot/api/health',
|
||||
'GET:/ai-voicebot/api/lobby',
|
||||
'GET:/ai-voicebot/api/session',
|
||||
'GET:/ai-voicebot/api/system/health',
|
||||
'POST:/ai-voicebot/api/admin/clear_password',
|
||||
'POST:/ai-voicebot/api/admin/set_password',
|
||||
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
||||
|
@ -34,13 +34,7 @@ export class AdvancedApiEvolutionChecker {
|
||||
// a list that should be updated when new endpoints are added to the schema.
|
||||
// This list is automatically updated by the update-api-client.js script
|
||||
const knownSchemaEndpoints = [
|
||||
{ path: '/ai-voicebot/api/admin/names', method: 'GET', operationId: 'admin_list_names_ai_voicebot_api_admin_names_get' },
|
||||
{ path: '/ai-voicebot/api/admin/set_password', method: 'POST', operationId: 'admin_set_password_ai_voicebot_api_admin_set_password_post' },
|
||||
{ path: '/ai-voicebot/api/admin/clear_password', method: 'POST', operationId: 'admin_clear_password_ai_voicebot_api_admin_clear_password_post' },
|
||||
{ path: '/ai-voicebot/api/health', method: 'GET', operationId: 'health_ai_voicebot_api_health_get' },
|
||||
{ path: '/ai-voicebot/api/session', method: 'GET', operationId: 'session_ai_voicebot_api_session_get' },
|
||||
{ path: '/ai-voicebot/api/lobby', method: 'GET', operationId: 'get_lobbies_ai_voicebot_api_lobby_get' },
|
||||
{ path: '/ai-voicebot/api/lobby/{session_id}', method: 'POST', operationId: 'lobby_create_ai_voicebot_api_lobby__session_id__post' }
|
||||
{ path: '/ai-voicebot/api/system/health', method: 'GET', operationId: 'system_health_ai_voicebot_api_system_health_get' }
|
||||
];
|
||||
|
||||
// Get implemented endpoints from ApiClient
|
||||
@ -68,7 +62,8 @@ export class AdvancedApiEvolutionChecker {
|
||||
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
||||
'GET:/ai-voicebot/api/bots/providers',
|
||||
'GET:/ai-voicebot/api/bots',
|
||||
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||
'POST:/ai-voicebot/api/lobby/{session_id}',
|
||||
'GET:/ai-voicebot/api/system/health'
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -5,218 +5,18 @@
|
||||
|
||||
|
||||
export interface paths {
|
||||
"/ai-voicebot/api/admin/names": {
|
||||
/** Admin List Names */
|
||||
get: operations["admin_list_names_ai_voicebot_api_admin_names_get"];
|
||||
};
|
||||
"/ai-voicebot/api/admin/set_password": {
|
||||
/** Admin Set Password */
|
||||
post: operations["admin_set_password_ai_voicebot_api_admin_set_password_post"];
|
||||
};
|
||||
"/ai-voicebot/api/admin/clear_password": {
|
||||
/** Admin Clear Password */
|
||||
post: operations["admin_clear_password_ai_voicebot_api_admin_clear_password_post"];
|
||||
};
|
||||
"/ai-voicebot/api/health": {
|
||||
/** Health */
|
||||
get: operations["health_ai_voicebot_api_health_get"];
|
||||
};
|
||||
"/ai-voicebot/api/session": {
|
||||
/** Session */
|
||||
get: operations["session_ai_voicebot_api_session_get"];
|
||||
};
|
||||
"/ai-voicebot/api/lobby": {
|
||||
/** Get Lobbies */
|
||||
get: operations["get_lobbies_ai_voicebot_api_lobby_get"];
|
||||
};
|
||||
"/ai-voicebot/api/lobby/{session_id}": {
|
||||
/** Lobby Create */
|
||||
post: operations["lobby_create_ai_voicebot_api_lobby__session_id__post"];
|
||||
};
|
||||
"/ai-voicebot/{path}": {
|
||||
/** Proxy Static */
|
||||
get: operations["proxy_static_ai_voicebot__path__put"];
|
||||
/** Proxy Static */
|
||||
put: operations["proxy_static_ai_voicebot__path__put"];
|
||||
/** Proxy Static */
|
||||
post: operations["proxy_static_ai_voicebot__path__put"];
|
||||
/** Proxy Static */
|
||||
delete: operations["proxy_static_ai_voicebot__path__put"];
|
||||
/** Proxy Static */
|
||||
options: operations["proxy_static_ai_voicebot__path__put"];
|
||||
/** Proxy Static */
|
||||
head: operations["proxy_static_ai_voicebot__path__put"];
|
||||
/** Proxy Static */
|
||||
patch: operations["proxy_static_ai_voicebot__path__put"];
|
||||
"/ai-voicebot/api/system/health": {
|
||||
/**
|
||||
* System Health
|
||||
* @description System health check showing manager status and enhanced monitoring
|
||||
*/
|
||||
get: operations["system_health_ai_voicebot_api_system_health_get"];
|
||||
};
|
||||
}
|
||||
|
||||
export type webhooks = Record<string, never>;
|
||||
|
||||
export interface components {
|
||||
schemas: {
|
||||
/**
|
||||
* AdminActionResponse
|
||||
* @description Response for admin actions
|
||||
*/
|
||||
AdminActionResponse: {
|
||||
/**
|
||||
* Status
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "ok" | "not_found";
|
||||
/** Name */
|
||||
name: string;
|
||||
};
|
||||
/**
|
||||
* AdminClearPassword
|
||||
* @description Request model for clearing admin password
|
||||
*/
|
||||
AdminClearPassword: {
|
||||
/** Name */
|
||||
name: string;
|
||||
};
|
||||
/**
|
||||
* AdminNamesResponse
|
||||
* @description Response for admin names endpoint
|
||||
*/
|
||||
AdminNamesResponse: {
|
||||
/** Name Passwords */
|
||||
name_passwords: {
|
||||
[key: string]: components["schemas"]["NamePasswordRecord"];
|
||||
};
|
||||
};
|
||||
/**
|
||||
* AdminSetPassword
|
||||
* @description Request model for setting admin password
|
||||
*/
|
||||
AdminSetPassword: {
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Password */
|
||||
password: string;
|
||||
};
|
||||
/** HTTPValidationError */
|
||||
HTTPValidationError: {
|
||||
/** Detail */
|
||||
detail?: components["schemas"]["ValidationError"][];
|
||||
};
|
||||
/**
|
||||
* HealthResponse
|
||||
* @description Health check response
|
||||
*/
|
||||
HealthResponse: {
|
||||
/** Status */
|
||||
status: string;
|
||||
};
|
||||
/**
|
||||
* LobbiesResponse
|
||||
* @description Response containing list of lobbies
|
||||
*/
|
||||
LobbiesResponse: {
|
||||
/** Lobbies */
|
||||
lobbies: components["schemas"]["LobbyListItem"][];
|
||||
};
|
||||
/**
|
||||
* LobbyCreateData
|
||||
* @description Data for lobby creation
|
||||
*/
|
||||
LobbyCreateData: {
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Private
|
||||
* @default false
|
||||
*/
|
||||
private?: boolean;
|
||||
};
|
||||
/**
|
||||
* LobbyCreateRequest
|
||||
* @description Request for creating a lobby
|
||||
*/
|
||||
LobbyCreateRequest: {
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
*/
|
||||
type: "lobby_create";
|
||||
data: components["schemas"]["LobbyCreateData"];
|
||||
};
|
||||
/**
|
||||
* LobbyCreateResponse
|
||||
* @description Response for lobby creation
|
||||
*/
|
||||
LobbyCreateResponse: {
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
*/
|
||||
type: "lobby_created";
|
||||
data: components["schemas"]["LobbyModel"];
|
||||
};
|
||||
/**
|
||||
* LobbyListItem
|
||||
* @description Lobby item for list responses
|
||||
*/
|
||||
LobbyListItem: {
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Name */
|
||||
name: string;
|
||||
};
|
||||
/**
|
||||
* LobbyModel
|
||||
* @description Core lobby model used across components
|
||||
*/
|
||||
LobbyModel: {
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Private
|
||||
* @default false
|
||||
*/
|
||||
private?: boolean;
|
||||
};
|
||||
/**
|
||||
* NamePasswordRecord
|
||||
* @description Password hash record for reserved names
|
||||
*/
|
||||
NamePasswordRecord: {
|
||||
/** Salt */
|
||||
salt: string;
|
||||
/** Hash */
|
||||
hash: string;
|
||||
};
|
||||
/**
|
||||
* SessionResponse
|
||||
* @description Session response model
|
||||
*/
|
||||
SessionResponse: {
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Lobbies */
|
||||
lobbies: components["schemas"]["LobbyModel"][];
|
||||
};
|
||||
/** ValidationError */
|
||||
ValidationError: {
|
||||
/** Location */
|
||||
loc: (string | number)[];
|
||||
/** Message */
|
||||
msg: string;
|
||||
/** Error Type */
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type components = Record<string, never>;
|
||||
|
||||
export type $defs = Record<string, never>;
|
||||
|
||||
@ -224,139 +24,11 @@ export type external = Record<string, never>;
|
||||
|
||||
export interface operations {
|
||||
|
||||
/** Admin List Names */
|
||||
admin_list_names_ai_voicebot_api_admin_names_get: {
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminNamesResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Admin Set Password */
|
||||
admin_set_password_ai_voicebot_api_admin_set_password_post: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminSetPassword"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminActionResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Admin Clear Password */
|
||||
admin_clear_password_ai_voicebot_api_admin_clear_password_post: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminClearPassword"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminActionResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Health */
|
||||
health_ai_voicebot_api_health_get: {
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HealthResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Session */
|
||||
session_ai_voicebot_api_session_get: {
|
||||
parameters: {
|
||||
cookie?: {
|
||||
session_id?: string | null;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SessionResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Get Lobbies */
|
||||
get_lobbies_ai_voicebot_api_lobby_get: {
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["LobbiesResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Lobby Create */
|
||||
lobby_create_ai_voicebot_api_lobby__session_id__post: {
|
||||
parameters: {
|
||||
path: {
|
||||
session_id: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["LobbyCreateRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["LobbyCreateResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Proxy Static */
|
||||
proxy_static_ai_voicebot__path__put: {
|
||||
parameters: {
|
||||
path: {
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* System Health
|
||||
* @description System health check showing manager status and enhanced monitoring
|
||||
*/
|
||||
system_health_ai_voicebot_api_system_health_get: {
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
@ -364,12 +36,6 @@ export interface operations {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
echo "📋 Step 1: Generating OpenAPI schema from FastAPI server..."
|
||||
docker compose exec server uv run python3 generate_schema_simple.py
|
||||
docker compose cp server:/client/openapi-schema.json ./client/openapi-schema.json
|
||||
|
||||
echo "📋 Step 2: Ensuring frontend container is running..."
|
||||
docker compose up -d client
|
||||
|
@ -45,8 +45,8 @@ class SessionAPI:
|
||||
name=session.name or "",
|
||||
lobbies=[], # New sessions start with no lobbies
|
||||
protected=False,
|
||||
is_bot=session.is_bot,
|
||||
has_media=session.has_media,
|
||||
bot_run_id=session.bot_run_id,
|
||||
bot_provider_id=session.bot_provider_id,
|
||||
bot_instance_id=session.bot_instance_id,
|
||||
)
|
||||
|
@ -31,7 +31,8 @@ from shared.models import (
|
||||
BotJoinPayload,
|
||||
BotInstanceModel,
|
||||
)
|
||||
|
||||
from core.session_manager import SessionManager
|
||||
from core.lobby_manager import LobbyManager
|
||||
|
||||
class BotProviderConfig:
|
||||
"""Configuration class for bot provider management"""
|
||||
@ -181,7 +182,13 @@ class BotManager:
|
||||
logger.error(f"Error fetching bots from provider {provider.name}: {e}")
|
||||
return BotProviderBotsResponse(bots=[])
|
||||
|
||||
async def request_bot_join(self, bot_name: str, request: BotJoinLobbyRequest, session_manager, lobby_manager) -> BotJoinLobbyResponse:
|
||||
async def request_bot_join(
|
||||
self,
|
||||
bot_name: str,
|
||||
request: BotJoinLobbyRequest,
|
||||
session_manager: SessionManager,
|
||||
lobby_manager: LobbyManager,
|
||||
) -> BotJoinLobbyResponse:
|
||||
"""Request a bot to join a specific lobby"""
|
||||
|
||||
# Find which provider has this bot and determine its media capability
|
||||
@ -196,10 +203,14 @@ class BotManager:
|
||||
for provider_id, provider in providers_copy.items():
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{provider.base_url}/bots", timeout=5.0)
|
||||
response = await client.get(
|
||||
f"{provider.base_url}/bots", timeout=5.0
|
||||
)
|
||||
if response.status_code == 200:
|
||||
# Use Pydantic model to validate the response
|
||||
bots_response = BotProviderBotsResponse.model_validate(response.json())
|
||||
bots_response = BotProviderBotsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
# Look for the bot by name
|
||||
for bot_info in bots_response.bots:
|
||||
if bot_info.name == bot_name:
|
||||
@ -217,10 +228,14 @@ class BotManager:
|
||||
provider = self.bot_providers[target_provider_id]
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{provider.base_url}/bots", timeout=5.0)
|
||||
response = await client.get(
|
||||
f"{provider.base_url}/bots", timeout=5.0
|
||||
)
|
||||
if response.status_code == 200:
|
||||
# Use Pydantic model to validate the response
|
||||
bots_response = BotProviderBotsResponse.model_validate(response.json())
|
||||
bots_response = BotProviderBotsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
# Look for the bot by name
|
||||
for bot_info in bots_response.bots:
|
||||
if bot_info.name == bot_name:
|
||||
@ -245,10 +260,15 @@ class BotManager:
|
||||
|
||||
# Create a session for the bot
|
||||
bot_session_id = secrets.token_hex(16)
|
||||
bot_instance_id = str(uuid.uuid4())
|
||||
|
||||
# Create the Session object for the bot
|
||||
bot_session = session_manager.get_or_create_session(bot_session_id, is_bot=True, has_media=bot_has_media)
|
||||
logger.info(f"Created bot session for: {bot_session.getName()} (has_media={bot_has_media})")
|
||||
bot_session = session_manager.get_or_create_session(
|
||||
bot_session_id, bot_instance_id=bot_instance_id, has_media=bot_has_media
|
||||
)
|
||||
logger.info(
|
||||
f"Created bot session for: {bot_session.getName()} (has_media={bot_has_media})"
|
||||
)
|
||||
|
||||
# Determine server URL for the bot to connect back to
|
||||
# Use the server's public URL or construct from request
|
||||
@ -282,7 +302,9 @@ class BotManager:
|
||||
if response.status_code == 200:
|
||||
# Use Pydantic model to parse and validate response
|
||||
try:
|
||||
join_response = BotProviderJoinResponse.model_validate(response.json())
|
||||
join_response = BotProviderJoinResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
run_id = join_response.run_id
|
||||
|
||||
# Update bot session with run and provider information
|
||||
@ -291,7 +313,6 @@ class BotManager:
|
||||
bot_session.bot_provider_id = target_provider_id
|
||||
|
||||
# Create a unique bot instance ID and track the bot instance
|
||||
bot_instance_id = str(uuid.uuid4())
|
||||
bot_instance = BotInstanceModel(
|
||||
bot_instance_id=bot_instance_id,
|
||||
bot_name=bot_name,
|
||||
@ -324,9 +345,13 @@ class BotManager:
|
||||
)
|
||||
except ValidationError as e:
|
||||
logger.error(f"Invalid response from bot provider: {e}")
|
||||
raise ValueError(f"Bot provider returned invalid response: {str(e)}")
|
||||
raise ValueError(
|
||||
f"Bot provider returned invalid response: {str(e)}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Bot provider returned error: HTTP {response.status_code}: {response.text}")
|
||||
logger.error(
|
||||
f"Bot provider returned error: HTTP {response.status_code}: {response.text}"
|
||||
)
|
||||
raise ValueError(f"Bot provider error: {response.status_code}")
|
||||
|
||||
except httpx.TimeoutException:
|
||||
@ -335,7 +360,9 @@ class BotManager:
|
||||
logger.error(f"Error requesting bot join: {e}")
|
||||
raise ValueError(f"Internal server error: {str(e)}")
|
||||
|
||||
async def request_bot_leave(self, request: BotLeaveLobbyRequest, session_manager) -> BotLeaveLobbyResponse:
|
||||
async def request_bot_leave(
|
||||
self, request: BotLeaveLobbyRequest, session_manager: SessionManager
|
||||
) -> BotLeaveLobbyResponse:
|
||||
"""Request a bot to leave from all lobbies and disconnect"""
|
||||
|
||||
# Find the bot instance
|
||||
@ -349,7 +376,7 @@ class BotManager:
|
||||
if not bot_session:
|
||||
raise ValueError("Bot session not found")
|
||||
|
||||
if not bot_session.is_bot:
|
||||
if not bot_session.bot_instance_id:
|
||||
raise ValueError("Session is not a bot")
|
||||
|
||||
logger.info(
|
||||
@ -412,14 +439,14 @@ class BotManager:
|
||||
run_id=bot_instance.run_id,
|
||||
)
|
||||
|
||||
async def get_bot_instance(self, bot_instance_id: str) -> dict:
|
||||
async def get_bot_instance(self, bot_instance_id: str) -> BotInstanceModel:
|
||||
"""Get information about a specific bot instance"""
|
||||
with self.lock:
|
||||
if bot_instance_id not in self.bot_instances:
|
||||
raise ValueError("Bot instance not found")
|
||||
bot_instance = self.bot_instances[bot_instance_id]
|
||||
|
||||
return bot_instance.model_dump()
|
||||
return bot_instance
|
||||
|
||||
def get_bot_instance_id_by_session_id(self, session_id: str) -> Optional[str]:
|
||||
"""Get bot_instance_id by session_id"""
|
||||
|
@ -84,11 +84,13 @@ class Lobby:
|
||||
name=s.name,
|
||||
live=True if s.ws else False,
|
||||
session_id=s.id,
|
||||
protected=True if s.name and self._is_name_protected(s.name) else False,
|
||||
is_bot=s.is_bot,
|
||||
protected=True
|
||||
if s.name and self._is_name_protected(s.name)
|
||||
else False,
|
||||
has_media=s.has_media,
|
||||
bot_run_id=s.bot_run_id,
|
||||
bot_provider_id=s.bot_provider_id,
|
||||
bot_instance_id=s.bot_instance_id,
|
||||
)
|
||||
for s in self.sessions.values()
|
||||
if s.name
|
||||
@ -343,7 +345,7 @@ class LobbyManager:
|
||||
removed_count = 0
|
||||
|
||||
with self.lock:
|
||||
lobbies_to_remove = []
|
||||
lobbies_to_remove: list[Lobby] = []
|
||||
for lobby in self.lobbies.values():
|
||||
if lobby.is_empty() and not lobby.private:
|
||||
lobbies_to_remove.append(lobby)
|
||||
|
@ -18,19 +18,33 @@ from pydantic import ValidationError
|
||||
# Import shared models
|
||||
try:
|
||||
# Try relative import first (when running as part of the package)
|
||||
from ...shared.models import SessionSaved, LobbySaved, SessionsPayload, NamePasswordRecord
|
||||
from ...shared.models import (
|
||||
SessionSaved,
|
||||
LobbySaved,
|
||||
SessionsPayload,
|
||||
NamePasswordRecord,
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
# Try absolute import (when running directly)
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from shared.models import SessionSaved, LobbySaved, SessionsPayload, NamePasswordRecord
|
||||
|
||||
sys.path.append(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
)
|
||||
from shared.models import (
|
||||
SessionSaved,
|
||||
LobbySaved,
|
||||
SessionsPayload,
|
||||
NamePasswordRecord,
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
f"Failed to import shared models: {e}. Ensure shared/models.py is accessible and PYTHONPATH is correctly set."
|
||||
)
|
||||
|
||||
from core.lobby_manager import Lobby
|
||||
from logger import logger
|
||||
|
||||
# Import WebRTC signaling for peer management
|
||||
@ -38,19 +52,41 @@ from websocket.webrtc_signaling import WebRTCSignalingHandlers
|
||||
|
||||
# Use try/except for importing events to handle both relative and absolute imports
|
||||
try:
|
||||
from ..models.events import event_bus, SessionDisconnected, UserNameChanged, SessionJoinedLobby, SessionLeftLobby
|
||||
from ..models.events import (
|
||||
event_bus,
|
||||
SessionDisconnected,
|
||||
UserNameChanged,
|
||||
SessionJoinedLobby,
|
||||
SessionLeftLobby,
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
from models.events import event_bus, SessionDisconnected, UserNameChanged, SessionJoinedLobby, SessionLeftLobby
|
||||
from models.events import (
|
||||
event_bus,
|
||||
SessionDisconnected,
|
||||
UserNameChanged,
|
||||
SessionJoinedLobby,
|
||||
SessionLeftLobby,
|
||||
)
|
||||
except ImportError:
|
||||
# Create dummy event system for standalone testing
|
||||
class DummyEventBus:
|
||||
async def publish(self, event): pass
|
||||
async def publish(self, event):
|
||||
pass
|
||||
|
||||
event_bus = DummyEventBus()
|
||||
class SessionDisconnected: pass
|
||||
class UserNameChanged: pass
|
||||
class SessionJoinedLobby: pass
|
||||
class SessionLeftLobby: pass
|
||||
|
||||
class SessionDisconnected:
|
||||
pass
|
||||
|
||||
class UserNameChanged:
|
||||
pass
|
||||
|
||||
class SessionJoinedLobby:
|
||||
pass
|
||||
|
||||
class SessionLeftLobby:
|
||||
pass
|
||||
|
||||
|
||||
class SessionConfig:
|
||||
@ -74,23 +110,29 @@ class SessionConfig:
|
||||
class Session:
|
||||
"""Individual session representing a user or bot connection"""
|
||||
|
||||
def __init__(self, id: str, is_bot: bool = False, has_media: bool = True):
|
||||
def __init__(
|
||||
self, id: str, bot_instance_id: Optional[str] = None, has_media: bool = True
|
||||
):
|
||||
logger.info(
|
||||
f"Instantiating new session {id} (bot: {is_bot}, media: {has_media})"
|
||||
f"Instantiating new session {id} (bot: {True if bot_instance_id else False}, media: {has_media})"
|
||||
)
|
||||
self.id = id
|
||||
self.short = id[:8]
|
||||
self.name = ""
|
||||
self.lobbies: List[Any] = [] # List of lobby objects this session is in
|
||||
self.lobby_peers: Dict[str, List[str]] = {} # lobby ID -> list of peer session IDs
|
||||
self.lobby_peers: Dict[
|
||||
str, List[str]
|
||||
] = {} # lobby ID -> list of peer session IDs
|
||||
self.ws: Optional[WebSocket] = None
|
||||
self.created_at = time.time()
|
||||
self.last_used = time.time()
|
||||
self.displaced_at: Optional[float] = None # When name was taken over
|
||||
self.is_bot = is_bot # Whether this session represents a bot
|
||||
self.has_media = has_media # Whether this session provides audio/video streams
|
||||
self.bot_run_id: Optional[str] = None # Bot run ID for tracking
|
||||
self.bot_provider_id: Optional[str] = None # Bot provider ID
|
||||
self.bot_instance_id = (
|
||||
bot_instance_id # Bot instance ID if this is a bot session
|
||||
)
|
||||
self.bot_instance_id: Optional[str] = None # Bot instance ID for tracking
|
||||
self.session_lock = threading.RLock() # Instance-level lock
|
||||
|
||||
@ -108,12 +150,16 @@ class Session:
|
||||
lobby_ids = [lobby.id for lobby in self.lobbies]
|
||||
|
||||
# Publish name change event (don't await here to avoid blocking)
|
||||
asyncio.create_task(event_bus.publish(UserNameChanged(
|
||||
session_id=self.id,
|
||||
old_name=old_name,
|
||||
new_name=name,
|
||||
lobby_ids=lobby_ids
|
||||
)))
|
||||
asyncio.create_task(
|
||||
event_bus.publish(
|
||||
UserNameChanged(
|
||||
session_id=self.id,
|
||||
old_name=old_name,
|
||||
new_name=name,
|
||||
lobby_ids=lobby_ids,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def update_last_used(self):
|
||||
"""Update the last_used timestamp"""
|
||||
@ -125,7 +171,7 @@ class Session:
|
||||
with self.session_lock:
|
||||
self.displaced_at = time.time()
|
||||
|
||||
async def join_lobby(self, lobby):
|
||||
async def join_lobby(self, lobby: Lobby):
|
||||
"""Join a lobby and establish WebRTC peer connections"""
|
||||
with self.session_lock:
|
||||
if lobby not in self.lobbies:
|
||||
@ -139,7 +185,7 @@ class Session:
|
||||
await lobby.addSession(self)
|
||||
|
||||
# Get existing peer sessions in this lobby for WebRTC setup
|
||||
peer_sessions = []
|
||||
peer_sessions: list[Session] = []
|
||||
for session in lobby.sessions.values():
|
||||
if (
|
||||
session.id != self.id and session.ws
|
||||
@ -151,16 +197,18 @@ class Session:
|
||||
await WebRTCSignalingHandlers.handle_add_peer(self, peer_session, lobby)
|
||||
|
||||
# Publish join event
|
||||
await event_bus.publish(SessionJoinedLobby(
|
||||
session_id=self.id,
|
||||
lobby_id=lobby.id,
|
||||
session_name=self.name or self.short
|
||||
))
|
||||
await event_bus.publish(
|
||||
SessionJoinedLobby(
|
||||
session_id=self.id,
|
||||
lobby_id=lobby.id,
|
||||
session_name=self.name or self.short,
|
||||
)
|
||||
)
|
||||
|
||||
async def leave_lobby(self, lobby):
|
||||
async def leave_lobby(self, lobby: Lobby):
|
||||
"""Leave a lobby and clean up WebRTC peer connections"""
|
||||
# Get peer sessions before removing from lobby
|
||||
peer_sessions = []
|
||||
peer_sessions: list[Session] = []
|
||||
if lobby.id in self.lobby_peers:
|
||||
for peer_id in self.lobby_peers[lobby.id]:
|
||||
peer_session = None
|
||||
@ -188,11 +236,13 @@ class Session:
|
||||
await lobby.removeSession(self)
|
||||
|
||||
# Publish leave event
|
||||
await event_bus.publish(SessionLeftLobby(
|
||||
session_id=self.id,
|
||||
lobby_id=lobby.id,
|
||||
session_name=self.name or self.short
|
||||
))
|
||||
await event_bus.publish(
|
||||
SessionLeftLobby(
|
||||
session_id=self.id,
|
||||
lobby_id=lobby.id,
|
||||
session_name=self.name or self.short,
|
||||
)
|
||||
)
|
||||
|
||||
def model_dump(self) -> Dict[str, Any]:
|
||||
"""Convert session to dictionary format for API responses"""
|
||||
@ -200,25 +250,19 @@ class Session:
|
||||
data: Dict[str, Any] = {
|
||||
"id": self.id,
|
||||
"name": self.name or "",
|
||||
"is_bot": self.is_bot,
|
||||
"bot_instance_id": self.bot_instance_id,
|
||||
"has_media": self.has_media,
|
||||
"created_at": self.created_at,
|
||||
"last_used": self.last_used,
|
||||
}
|
||||
|
||||
# Include bot_instance_id if this is a bot session and it has one
|
||||
if self.is_bot and self.bot_instance_id:
|
||||
data["bot_instance_id"] = self.bot_instance_id
|
||||
|
||||
return data
|
||||
|
||||
def to_saved(self) -> SessionSaved:
|
||||
"""Convert session to saved format for persistence"""
|
||||
with self.session_lock:
|
||||
lobbies_list: List[LobbySaved] = [
|
||||
LobbySaved(
|
||||
id=lobby.id, name=lobby.name, private=lobby.private
|
||||
)
|
||||
LobbySaved(id=lobby.id, name=lobby.name, private=lobby.private)
|
||||
for lobby in self.lobbies
|
||||
]
|
||||
return SessionSaved(
|
||||
@ -228,7 +272,6 @@ class Session:
|
||||
created_at=self.created_at,
|
||||
last_used=self.last_used,
|
||||
displaced_at=self.displaced_at,
|
||||
is_bot=self.is_bot,
|
||||
has_media=self.has_media,
|
||||
bot_run_id=self.bot_run_id,
|
||||
bot_provider_id=self.bot_provider_id,
|
||||
@ -251,7 +294,12 @@ class SessionManager:
|
||||
self.validation_task_running = False
|
||||
self.validation_task: Optional[asyncio.Task] = None
|
||||
|
||||
def create_session(self, session_id: Optional[str] = None, is_bot: bool = False, has_media: bool = True) -> Session:
|
||||
def create_session(
|
||||
self,
|
||||
session_id: Optional[str] = None,
|
||||
bot_instance_id: Optional[str] = None,
|
||||
has_media: bool = True,
|
||||
) -> Session:
|
||||
"""Create a new session with given or generated ID"""
|
||||
if not session_id:
|
||||
session_id = secrets.token_hex(16)
|
||||
@ -266,20 +314,29 @@ class SessionManager:
|
||||
return existing_session
|
||||
|
||||
# Create new session
|
||||
session = Session(session_id, is_bot=is_bot, has_media=has_media)
|
||||
session = Session(
|
||||
session_id, bot_instance_id=bot_instance_id, has_media=has_media
|
||||
)
|
||||
self._instances.append(session)
|
||||
|
||||
self.save()
|
||||
return session
|
||||
|
||||
def get_or_create_session(self, session_id: Optional[str] = None, is_bot: bool = False, has_media: bool = True) -> Session:
|
||||
def get_or_create_session(
|
||||
self,
|
||||
session_id: Optional[str] = None,
|
||||
bot_instance_id: Optional[str] = None,
|
||||
has_media: bool = True,
|
||||
) -> Session:
|
||||
"""Get existing session or create a new one"""
|
||||
if session_id:
|
||||
existing_session = self.get_session(session_id)
|
||||
if existing_session:
|
||||
return existing_session
|
||||
|
||||
return self.create_session(session_id, is_bot=is_bot, has_media=has_media)
|
||||
return self.create_session(
|
||||
session_id, bot_instance_id=bot_instance_id, has_media=has_media
|
||||
)
|
||||
|
||||
def get_session(self, session_id: str) -> Optional[Session]:
|
||||
"""Get session by ID"""
|
||||
@ -324,11 +381,15 @@ class SessionManager:
|
||||
|
||||
# Publish disconnect event
|
||||
lobby_ids = [lobby.id for lobby in session.lobbies]
|
||||
asyncio.create_task(event_bus.publish(SessionDisconnected(
|
||||
session_id=session.id,
|
||||
session_name=session.name or session.short,
|
||||
lobby_ids=lobby_ids
|
||||
)))
|
||||
asyncio.create_task(
|
||||
event_bus.publish(
|
||||
SessionDisconnected(
|
||||
session_id=session.id,
|
||||
session_name=session.name or session.short,
|
||||
lobby_ids=lobby_ids,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""Save all sessions to disk"""
|
||||
@ -355,9 +416,7 @@ class SessionManager:
|
||||
# Atomic rename
|
||||
os.rename(temp_file, self._save_file)
|
||||
|
||||
logger.info(
|
||||
f"Saved {len(sessions_list)} sessions to {self._save_file}"
|
||||
)
|
||||
logger.info(f"Saved {len(sessions_list)} sessions to {self._save_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save sessions: {e}")
|
||||
# Clean up temp file if it exists
|
||||
@ -423,17 +482,15 @@ class SessionManager:
|
||||
|
||||
session = Session(
|
||||
s_saved.id,
|
||||
is_bot=getattr(s_saved, "is_bot", False),
|
||||
has_media=getattr(s_saved, "has_media", True),
|
||||
bot_instance_id=s_saved.bot_instance_id,
|
||||
has_media=s_saved.has_media,
|
||||
)
|
||||
session.name = name
|
||||
session.created_at = created_at
|
||||
session.last_used = last_used
|
||||
session.displaced_at = displaced_at
|
||||
session.is_bot = getattr(s_saved, "is_bot", False)
|
||||
session.has_media = getattr(s_saved, "has_media", True)
|
||||
session.bot_run_id = getattr(s_saved, "bot_run_id", None)
|
||||
session.bot_provider_id = getattr(s_saved, "bot_provider_id", None)
|
||||
session.bot_run_id = s_saved.bot_run_id
|
||||
session.bot_provider_id = s_saved.bot_provider_id
|
||||
|
||||
# Note: Lobby restoration will be handled by LobbyManager
|
||||
|
||||
@ -482,7 +539,7 @@ class SessionManager:
|
||||
removed_count = 0
|
||||
|
||||
with self.lock:
|
||||
sessions_to_remove = []
|
||||
sessions_to_remove: list[Session] = []
|
||||
|
||||
for session in self._instances:
|
||||
with session.session_lock:
|
||||
@ -496,7 +553,10 @@ class SessionManager:
|
||||
):
|
||||
sessions_to_remove.append(session)
|
||||
|
||||
if len(sessions_to_remove) >= SessionConfig.MAX_SESSIONS_PER_CLEANUP:
|
||||
if (
|
||||
len(sessions_to_remove)
|
||||
>= SessionConfig.MAX_SESSIONS_PER_CLEANUP
|
||||
):
|
||||
break
|
||||
|
||||
# Remove sessions
|
||||
@ -516,7 +576,9 @@ class SessionManager:
|
||||
logger.info(f"Cleaned up session {session.getName()}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cleaning up session {session.getName()}: {e}")
|
||||
logger.warning(
|
||||
f"Error cleaning up session {session.getName()}: {e}"
|
||||
)
|
||||
|
||||
if removed_count > 0:
|
||||
self.save()
|
||||
@ -560,7 +622,9 @@ class SessionManager:
|
||||
try:
|
||||
removed_count = self.cleanup_old_sessions()
|
||||
if removed_count > 0:
|
||||
logger.info(f"Periodic cleanup removed {removed_count} old sessions")
|
||||
logger.info(
|
||||
f"Periodic cleanup removed {removed_count} old sessions"
|
||||
)
|
||||
cleanup_errors = 0 # Reset error counter on success
|
||||
|
||||
# Run cleanup at configured interval
|
||||
@ -586,7 +650,9 @@ class SessionManager:
|
||||
try:
|
||||
issues = self.validate_session_integrity()
|
||||
if issues:
|
||||
logger.warning(f"Session integrity issues found: {len(issues)} issues")
|
||||
logger.warning(
|
||||
f"Session integrity issues found: {len(issues)} issues"
|
||||
)
|
||||
for issue in issues[:10]: # Log first 10 issues
|
||||
logger.warning(f"Integrity issue: {issue}")
|
||||
|
||||
@ -607,17 +673,26 @@ class SessionManager:
|
||||
issues.append(f"Session with empty ID: {session}")
|
||||
|
||||
if session.created_at > time.time():
|
||||
issues.append(f"Session {session.getName()} has future creation time")
|
||||
issues.append(
|
||||
f"Session {session.getName()} has future creation time"
|
||||
)
|
||||
|
||||
if session.last_used > time.time():
|
||||
issues.append(f"Session {session.getName()} has future last_used time")
|
||||
issues.append(
|
||||
f"Session {session.getName()} has future last_used time"
|
||||
)
|
||||
|
||||
# Check for duplicate names
|
||||
if session.name:
|
||||
count = sum(1 for s in self._instances
|
||||
if s.name and s.name.lower() == session.name.lower())
|
||||
count = sum(
|
||||
1
|
||||
for s in self._instances
|
||||
if s.name and s.name.lower() == session.name.lower()
|
||||
)
|
||||
if count > 1:
|
||||
issues.append(f"Duplicate name '{session.name}' found in {count} sessions")
|
||||
issues.append(
|
||||
f"Duplicate name '{session.name}' found in {count} sessions"
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
@ -629,7 +704,9 @@ class SessionManager:
|
||||
if session.ws:
|
||||
await session.ws.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing WebSocket for {session.getName()}: {e}")
|
||||
logger.warning(
|
||||
f"Error closing WebSocket for {session.getName()}: {e}"
|
||||
)
|
||||
|
||||
logger.info("All sessions cleaned up")
|
||||
|
||||
|
@ -43,10 +43,10 @@ class ParticipantModel(BaseModel):
|
||||
session_id: str
|
||||
live: bool
|
||||
protected: bool
|
||||
is_bot: bool = False
|
||||
has_media: bool = True # Whether this participant provides audio/video streams
|
||||
bot_run_id: Optional[str] = None
|
||||
bot_provider_id: Optional[str] = None
|
||||
bot_instance_id: Optional[str] = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@ -138,6 +138,11 @@ class SessionResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
lobbies: List[LobbyModel]
|
||||
protected: bool = False
|
||||
has_media: bool = False
|
||||
bot_run_id: Optional[str] = None
|
||||
bot_provider_id: Optional[str] = None
|
||||
bot_instance_id: Optional[str] = None
|
||||
|
||||
|
||||
class LobbyCreateData(BaseModel):
|
||||
@ -317,7 +322,6 @@ class SessionSaved(BaseModel):
|
||||
created_at: float = 0.0
|
||||
last_used: float = 0.0
|
||||
displaced_at: Optional[float] = None # When name was taken over
|
||||
is_bot: bool = False # Whether this session represents a bot
|
||||
has_media: bool = True # Whether this session provides audio/video streams
|
||||
bot_run_id: Optional[str] = None # Bot run ID for tracking
|
||||
bot_provider_id: Optional[str] = None # Bot provider ID
|
||||
|
Loading…
x
Reference in New Issue
Block a user