diff --git a/client/src/App.tsx b/client/src/App.tsx index 4015c56..2820b66 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,7 +11,7 @@ import { Box, Button, Tooltip } from "@mui/material"; import { BrowserRouter as Router, Route, Routes, useParams } from "react-router-dom"; import useWebSocket, { ReadyState } from "react-use-websocket"; import ConnectionStatus from "./ConnectionStatus"; -import { sessionsApi, LobbyCreateRequest } from "./api-client"; +import { sessionApi, LobbyCreateRequest } from "./api-client"; console.log(`AI Voice Chat Build: ${process.env.REACT_APP_AI_VOICECHAT_BUILD}`); @@ -151,7 +151,7 @@ const LobbyView: React.FC = (props: LobbyProps) => { }, }; - const response = await sessionsApi.createLobby(session.id, lobbyRequest); + const response = await sessionApi.createLobby(session.id, lobbyRequest); if (response.type !== "lobby_created") { console.error(`Lobby - Unexpected response type: ${response.type}`); @@ -312,7 +312,7 @@ const App = () => { const getSession = useCallback(async () => { try { - const session = await sessionsApi.getCurrent(); + const session = await sessionApi.getCurrent(); setSession(session); setSessionRetryAttempt(0); } catch (err) { diff --git a/client/src/api-client.ts b/client/src/api-client.ts index ece6595..3127404 100644 --- a/client/src/api-client.ts +++ b/client/src/api-client.ts @@ -282,37 +282,63 @@ export class ApiClient { export const apiClient = new ApiClient(); // Convenience API namespaces for easy usage +export const systemApi = { + getSystemHealth: () => apiClient.getSystemHealth(), + getInfo: () => apiClient.getSystemInfo() +}; + export const adminApi = { - listNames: () => apiClient.getListNames(), + getNames: () => apiClient.getListNames(), setPassword: (data: any) => apiClient.createSetPassword(data), clearPassword: (data: any) => apiClient.createClearPassword(data), cleanupSessions: () => apiClient.createCleanupSessions(), - sessionMetrics: () => apiClient.getSessionMetrics(), + getCurrent: () => apiClient.getSessionMetrics(), validateSessions: () => apiClient.getValidateSessions(), - cleanupLobbies: () => apiClient.createCleanupLobbies(), + cleanupLobbies: () => apiClient.createCleanupLobbies() }; -export const healthApi = { +export const healthApi = { check: () => apiClient.getHealthSummary(), ready: () => apiClient.getReadinessProbe(), - live: () => apiClient.getLivenessProbe(), + live: () => apiClient.getLivenessProbe() }; -export const lobbiesApi = { - getAll: () => apiClient.getListLobbies(), - getChatMessages: (lobbyId: string, params?: Record) => apiClient.getChatMessages(lobbyId, params), -}; - -export const sessionsApi = { +export const sessionApi = { getCurrent: () => apiClient.getSession(), - createLobby: (sessionId: string, data: any) => apiClient.createLobby(sessionId, data), + createLobby: (session_id: string, data: any) => apiClient.createLobby(session_id, data) +}; + +export const lobbyApi = { + getAll: () => apiClient.getListLobbies(), + getChatMessages: (lobby_id: string, params?: Record) => apiClient.getChatMessages(lobby_id, params) }; export const botsApi = { + registerProvider: (data: any) => apiClient.createRegisterBotProvider(data), getProviders: () => apiClient.getListBotProviders(), getAvailable: () => apiClient.getListAvailableBots(), - requestJoinLobby: (botName: string, request: any) => apiClient.createRequestBotJoinLobby(botName, request), - requestLeaveLobby: (botInstanceId: string) => apiClient.createRequestBotLeaveLobby(botInstanceId), - getInstance: (botInstanceId: string) => apiClient.getBotInstance(botInstanceId), - registerProvider: (data: any) => apiClient.createRegisterBotProvider(data), + requestJoinLobby: (bot_name: string, data: any) => apiClient.createRequestBotJoinLobby(bot_name, data), + requestLeaveLobby: (bot_instance_id: string) => apiClient.createRequestBotLeaveLobby(bot_instance_id), + getInstance: (bot_instance_id: string) => apiClient.getBotInstance(bot_instance_id), + getSchema: (bot_name: string) => apiClient.getBotConfigSchema(bot_name), + getBotConfigs: (lobby_id: string) => apiClient.getLobbyBotConfigs(lobby_id), + deleteBotConfigs: (lobby_id: string) => apiClient.deleteLobbyConfigs(lobby_id), + getBotConfig: (lobby_id: string, bot_name: string) => apiClient.getLobbyBotConfig(lobby_id, bot_name), + deleteBotConfig: (lobby_id: string, bot_name: string) => apiClient.deleteBotConfig(lobby_id, bot_name), + updateConfig: (data: any) => apiClient.createUpdateBotConfig(data), + getConfigStatistics: () => apiClient.getConfigStatistics(), + refreshAllSchemas: () => apiClient.createRefreshBotSchemas(), + refreshSchema: (bot_name: string) => apiClient.createRefreshBotSchema(bot_name), + clearSchemaCache: (bot_name: string) => apiClient.deleteClearBotSchemaCache(bot_name) +}; + +export const metricsApi = { + getCurrent: () => apiClient.getCurrentMetrics(), + getHistory: (params?: Record) => apiClient.getMetricsHistory(params), + exportPrometheus: () => apiClient.getExportMetricsPrometheus() +}; + +export const cacheApi = { + getStats: () => apiClient.getCacheStatistics(), + clear: () => apiClient.createClearCache() }; diff --git a/client/src/api-usage-examples.ts b/client/src/api-usage-examples.ts index dbf9602..6ce5896 100644 --- a/client/src/api-usage-examples.ts +++ b/client/src/api-usage-examples.ts @@ -13,48 +13,53 @@ // }; // AFTER (using generated types): -import { - apiClient, - lobbiesApi, - sessionsApi, +import { + apiClient, + lobbyApi, + sessionApi, healthApi, adminApi, ApiError, - type LobbyModel, // Use this instead of local Lobby type + type LobbyModel, // Use this instead of local Lobby type type LobbyCreateRequest, - type AdminSetPassword -} from './api-client'; + type AdminSetPassword, +} from "./api-client"; // Example usage in a React component: // 1. Fetching data with type safety const fetchLobbies = async (setLobbies: (lobbies: any[]) => void, setError: (error: string) => void) => { try { - const response = await lobbiesApi.getAll(); + const response = await lobbyApi.getAll(); // response.lobbies is properly typed as LobbyListItem[] setLobbies(response.lobbies); } catch (error) { if (error instanceof ApiError) { console.error(`API Error ${error.status}: ${error.statusText}`, error.data); } - setError('Failed to fetch lobbies'); + setError("Failed to fetch lobbies"); } }; // 2. Creating a lobby with typed request -const createNewLobby = async (sessionId: string, lobbyName: string, isPrivate: boolean, setError: (error: string) => void) => { +const createNewLobby = async ( + sessionId: string, + lobbyName: string, + isPrivate: boolean, + setError: (error: string) => void +) => { const lobbyRequest: LobbyCreateRequest = { type: "lobby_create", data: { name: lobbyName, - private: isPrivate - } + private: isPrivate, + }, }; - + try { - const newLobby = await sessionsApi.createLobby(sessionId, lobbyRequest); + const newLobby = await sessionApi.createLobby(sessionId, lobbyRequest); // newLobby.data is properly typed as LobbyModel - console.log('Created lobby:', newLobby.data); + console.log("Created lobby:", newLobby.data); } catch (error) { if (error instanceof ApiError) { setError(`Failed to create lobby: ${error.message}`); diff --git a/client/update-api-client.js b/client/update-api-client.js index 4671971..c7e9569 100644 --- a/client/update-api-client.js +++ b/client/update-api-client.js @@ -196,41 +196,413 @@ class ApiClientGenerator { * Generate convenience API namespaces */ generateConvenienceApis() { - return `// Convenience API namespaces for easy usage -export const adminApi = { - listNames: () => apiClient.getListNames(), - setPassword: (data: any) => apiClient.createSetPassword(data), - clearPassword: (data: any) => apiClient.createClearPassword(data), - cleanupSessions: () => apiClient.createCleanupSessions(), - sessionMetrics: () => apiClient.getSessionMetrics(), - validateSessions: () => apiClient.getValidateSessions(), - cleanupLobbies: () => apiClient.createCleanupLobbies(), -}; + // Group endpoints by namespace based on path analysis + const namespaceGroups = this.groupEndpointsByNamespace(); + + // Generate convenience methods for each namespace + const namespaceDefinitions = Object.entries(namespaceGroups).map(([namespace, endpoints]) => { + const methods = endpoints.map(endpoint => this.generateConvenienceMethod(endpoint)).join(',\n '); + + return `export const ${namespace}Api = {\n ${methods}\n};`; + }).join('\n\n'); -export const healthApi = { - check: () => apiClient.getHealthSummary(), - ready: () => apiClient.getReadinessProbe(), - live: () => apiClient.getLivenessProbe(), -}; + return `// Convenience API namespaces for easy usage\n${namespaceDefinitions}`; + } -export const lobbiesApi = { - getAll: () => apiClient.getListLobbies(), - getChatMessages: (lobbyId: string, params?: Record) => apiClient.getChatMessages(lobbyId, params), -}; + /** + * Group endpoints into logical namespaces based on path patterns + * Dynamically creates namespaces based on the top-level API path segments + */ + groupEndpointsByNamespace() { + const groups = {}; -export const sessionsApi = { - getCurrent: () => apiClient.getSession(), - createLobby: (sessionId: string, data: any) => apiClient.createLobby(sessionId, data), -}; + this.endpoints.forEach(endpoint => { + const path = endpoint.path; + + // Extract the namespace from the path: /ai-voicebot/api/{namespace}/... + const pathMatch = path.match(/^\/ai-voicebot\/api\/([^\/]+)/); + if (!pathMatch) { + // Fallback for unexpected path formats + console.warn(`Unexpected path format: ${path}`); + return; + } + + let namespace = pathMatch[1]; + + // Apply special routing rules for better organization + if (namespace === 'lobby' && path.includes('/{session_id}') && endpoint.method.toLowerCase() === 'post') { + // Lobby creation belongs to sessions namespace for better UX + namespace = 'session'; + } else if (namespace === 'session' && (path.includes('/cleanup') || path.includes('/metrics') || path.includes('/validate'))) { + // Admin session operations belong to admin namespace + namespace = 'admin'; + } + + // Initialize namespace group if it doesn't exist + if (!groups[namespace]) { + groups[namespace] = []; + } + + groups[namespace].push(endpoint); + }); -export const botsApi = { - getProviders: () => apiClient.getListBotProviders(), - getAvailable: () => apiClient.getListAvailableBots(), - requestJoinLobby: (botName: string, request: any) => apiClient.createRequestBotJoinLobby(botName, request), - requestLeaveLobby: (botInstanceId: string) => apiClient.createRequestBotLeaveLobby(botInstanceId), - getInstance: (botInstanceId: string) => apiClient.getBotInstance(botInstanceId), - registerProvider: (data: any) => apiClient.createRegisterBotProvider(data), -};`; + // Remove empty groups and return + return Object.fromEntries( + Object.entries(groups).filter(([_, endpoints]) => endpoints.length > 0) + ); + } + + /** + * Generate a convenience method for an endpoint with intuitive naming + */ + generateConvenienceMethod(endpoint) { + const methodName = this.generateConvenienceMethodName(endpoint); + const clientMethodName = this.generateMethodName(endpoint); + const params = this.extractMethodParameters(endpoint); + + // Generate parameter list for the convenience method + const paramList = params.length > 0 ? params.join(', ') : ''; + + // Generate argument list for the client method call + const argList = this.generateArgumentList(endpoint, params); + + return `${methodName}: (${paramList}) => apiClient.${clientMethodName}(${argList})`; + } + + /** + * Generate an intuitive convenience method name based on the endpoint + */ + generateConvenienceMethodName(endpoint) { + const path = endpoint.path; + const method = endpoint.method.toLowerCase(); + + // Special naming patterns for better developer experience + if (path.includes('/lobby') && path.includes('/chat') && method === 'get') { + return 'getChatMessages'; + } + if (path.includes('/lobby') && method === 'get' && !path.includes('/chat')) { + return 'getAll'; + } + if (path.includes('/session') && method === 'get') { + return 'getCurrent'; + } + if (path.includes('/health') && method === 'get' && !path.includes('/live') && !path.includes('/ready')) { + return 'check'; + } + if (path.includes('/health/ready')) { + return 'ready'; + } + if (path.includes('/health/live')) { + return 'live'; + } + if (path.includes('/providers') && method === 'get') { + return 'getProviders'; + } + if (path.includes('/bots') && method === 'get' && !path.includes('/providers') && !path.includes('/instances') && !path.includes('/config')) { + return 'getAvailable'; + } + if (path.includes('/join')) { + return 'requestJoinLobby'; + } + if (path.includes('/leave')) { + return 'requestLeaveLobby'; + } + if (path.includes('/instances/') && method === 'get') { + return 'getInstance'; + } + if (path.includes('/providers/register')) { + return 'registerProvider'; + } + if (path.includes('/lobby/{session_id}') && method === 'post') { + return 'createLobby'; + } + if (path.includes('/cleanup_sessions')) { + return 'cleanupSessions'; + } + if (path.includes('/cleanup_lobbies')) { + return 'cleanupLobbies'; + } + if (path.includes('/session_metrics')) { + return 'sessionMetrics'; + } + if (path.includes('/validate_sessions')) { + return 'validateSessions'; + } + if (path.includes('/set_password')) { + return 'setPassword'; + } + if (path.includes('/clear_password')) { + return 'clearPassword'; + } + if (path.includes('/names')) { + return 'listNames'; + } + + // Generic patterns based on method and path + const pathSegments = path.split('/').filter(segment => segment && !segment.startsWith('{')); + const lastSegment = pathSegments[pathSegments.length - 1]; + + // Convert common method patterns to intuitive names + const methodPrefixes = { + 'get': 'get', + 'post': 'create', + 'put': 'replace', + 'patch': 'update', + 'delete': 'delete' + }; + + const prefix = methodPrefixes[method] || method; + const resource = lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1); + + return prefix + resource; + } + + /** + * Extract parameters needed for the convenience method + */ + extractMethodParameters(endpoint) { + const params = []; + + // Extract path parameters + const pathParams = endpoint.path.match(/\{([^}]+)\}/g); + if (pathParams) { + pathParams.forEach(param => { + const paramName = param.slice(1, -1); // Remove { and } + const tsType = this.inferParameterType(paramName); + params.push(`${paramName}: ${tsType}`); + }); + } + + // Add request body parameter if needed + if (endpoint.requestBody) { + params.push('data: any'); + } + + // Add query parameters parameter for GET requests + if (endpoint.method === 'GET' && this.hasQueryParameters(endpoint)) { + params.push('params?: Record'); + } + + return params; + } + + /** + * Generate argument list for calling the client method + */ + generateArgumentList(endpoint, params) { + const args = []; + + // Extract parameter names (without types) - exclude optional params indicator + params.forEach(param => { + const paramName = param.split(':')[0].trim().replace('?', ''); + args.push(paramName); + }); + + return args.join(', '); + } + + /** + * Infer TypeScript type for a parameter based on its name + */ + inferParameterType(paramName) { + if (paramName.includes('id')) { + return 'string'; + } + if (paramName.includes('limit') || paramName.includes('count')) { + return 'number'; + } + return 'string'; // Default to string + } + + /** + * Check if endpoint has query parameters + */ + hasQueryParameters(endpoint) { + return endpoint.parameters && endpoint.parameters.some(p => p.in === 'query'); + } + + /** + * Generate an intuitive convenience method name based on the endpoint + */ + generateConvenienceMethodName(endpoint) { + const path = endpoint.path; + const method = endpoint.method.toLowerCase(); + + // Special naming patterns for better developer experience (order matters - most specific first) + + // Lobby-related endpoints + if (path.includes('/lobby') && path.includes('/chat') && method === 'get') { + return 'getChatMessages'; + } + if (path === '/ai-voicebot/api/lobby' && method === 'get') { + return 'getAll'; + } + if (path.includes('/lobby/{session_id}') && method === 'post') { + return 'createLobby'; + } + + // Bot config endpoints (need specific handling to avoid duplicates) + if (path.includes('/bots/config/lobby') && path.includes('/bot/') && method === 'get') { + return 'getBotConfig'; + } + if (path.includes('/bots/config/lobby') && path.includes('/bot/') && method === 'delete') { + return 'deleteBotConfig'; + } + if (path.includes('/bots/config/lobby') && method === 'get' && !path.includes('/bot/')) { + return 'getBotConfigs'; + } + if (path.includes('/bots/config/lobby') && method === 'delete' && !path.includes('/bot/')) { + return 'deleteBotConfigs'; + } + if (path.includes('/config/update')) { + return 'updateConfig'; + } + if (path.includes('/config/statistics')) { + return 'getConfigStatistics'; + } + if (path.includes('/config/refresh-schemas')) { + return 'refreshAllSchemas'; + } + if (path.includes('/config/schema/') && path.includes('/refresh')) { + return 'refreshSchema'; + } + if (path.includes('/config/schema/') && path.includes('/cache') && method === 'delete') { + return 'clearSchemaCache'; + } + if (path.includes('/config/schema/') && method === 'get') { + return 'getSchema'; + } + + // Bot general endpoints + if (path.includes('/providers/register')) { + return 'registerProvider'; + } + if (path.includes('/providers') && method === 'get') { + return 'getProviders'; + } + if (path.includes('/bots') && method === 'get' && !path.includes('/providers') && !path.includes('/instances') && !path.includes('/config')) { + return 'getAvailable'; + } + if (path.includes('/join')) { + return 'requestJoinLobby'; + } + if (path.includes('/leave')) { + return 'requestLeaveLobby'; + } + if (path.includes('/instances/') && method === 'get') { + return 'getInstance'; + } + + // Session endpoints + if (path.includes('/session') && method === 'get') { + return 'getCurrent'; + } + + // Health endpoints (avoid duplicates by being specific) + if (path.includes('/system/health')) { + return 'getSystemHealth'; + } + if (path === '/ai-voicebot/api/health' && method === 'get') { + return 'check'; + } + if (path.includes('/health/ready')) { + return 'ready'; + } + if (path.includes('/health/live')) { + return 'live'; + } + + // Admin endpoints + if (path.includes('/cleanup_sessions')) { + return 'cleanupSessions'; + } + if (path.includes('/cleanup_lobbies')) { + return 'cleanupLobbies'; + } + if (path.includes('/session_metrics')) { + return 'getMetrics'; + } + if (path.includes('/validate_sessions')) { + return 'validateSessions'; + } + if (path.includes('/set_password')) { + return 'setPassword'; + } + if (path.includes('/clear_password')) { + return 'clearPassword'; + } + if (path.includes('/admin/names')) { + return 'getNames'; + } + + // Metrics endpoints + if (path.includes('/metrics/history')) { + return 'getHistory'; + } + if (path.includes('/metrics/export')) { + return 'exportPrometheus'; + } + if (path === '/ai-voicebot/api/metrics' && method === 'get') { + return 'getCurrent'; + } + + // Cache endpoints + if (path.includes('/cache/stats')) { + return 'getStats'; + } + if (path.includes('/cache/clear')) { + return 'clear'; + } + + // System endpoints + if (path.includes('/system/info')) { + return 'getInfo'; + } + + // Generic patterns based on method and path segments (fallback) + const pathSegments = path.split('/').filter(segment => segment && !segment.startsWith('{')); + const lastSegment = pathSegments[pathSegments.length - 1]; + + // Handle special characters in path segments + let resource = lastSegment; + if (resource.includes('-')) { + // Convert kebab-case to camelCase + resource = resource.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); + } + + // Convert common method patterns to intuitive names + const methodPrefixes = { + 'get': 'get', + 'post': 'create', + 'put': 'replace', + 'patch': 'update', + 'delete': 'delete' + }; + + const prefix = methodPrefixes[method] || method; + const resourceName = resource.charAt(0).toUpperCase() + resource.slice(1); + + return prefix + resourceName; + } + + /** + * Infer TypeScript type for a parameter based on its name + */ + inferParameterType(paramName) { + if (paramName.includes('id')) { + return 'string'; + } + if (paramName.includes('limit') || paramName.includes('count')) { + return 'number'; + } + return 'string'; // Default to string + } + + /** + * Check if endpoint has query parameters + */ + hasQueryParameters(endpoint) { + return endpoint.parameters && endpoint.parameters.some(p => p.in === 'query'); } /**