Type checking working

This commit is contained in:
James Ketr 2025-09-05 11:33:28 -07:00
parent d679c8cecf
commit 2fdd58f7c3
10 changed files with 4847 additions and 909 deletions

File diff suppressed because it is too large Load Diff

View File

@ -138,7 +138,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
};
const isBotConfigurable = (bot: BotInfoModel): boolean => {
return Boolean(bot.configurable) || Boolean(bot.features && bot.features.includes("per_lobby_config"));
return Boolean(bot.configurable);
};
const botCount = bots.length;

View File

@ -13,7 +13,7 @@ import { MediaControl, MediaAgent, Peer } from "./MediaControl";
import Box from "@mui/material/Box";
import { Session } from "./GlobalContext";
import useWebSocket from "react-use-websocket";
import { ApiClient, BotLeaveLobbyRequest } from "./api-client";
import { ApiClient } from "./api-client";
import BotConfig from "./BotConfig";
type User = {
@ -53,7 +53,7 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
setLeavingBots((prev) => new Set(prev).add(user.session_id));
try {
await apiClient.requestBotLeaveLobby(user.bot_instance_id);
await apiClient.createRequestBotLeaveLobby(user.bot_instance_id);
console.log(`Bot ${user.name} leave requested successfully`);
} catch (error) {
console.error("Failed to request bot leave:", error);

View File

@ -2,96 +2,48 @@
import { components } from "./api-types";
import { base } from "./Common";
// Re-export commonly used types from the generated schema
export type LobbyModel = components["schemas"]["LobbyModel"];
export type LobbyListItem = components["schemas"]["LobbyListItem"];
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
// DO NOT MANUALLY EDIT BELOW THIS LINE - All content below is auto-generated
// To modify auto-generated content, edit the template in client/update-api-client.js
// Type aliases for API methods
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
// Re-export all types from the generated schema
export type { components } from "./api-types";
// Re-export all types from the generated schema for convenience
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
export type AdminMetricsConfig = components["schemas"]["AdminMetricsConfig"];
export type AdminMetricsResponse = components["schemas"]["AdminMetricsResponse"];
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
export type AdminValidationResponse = components["schemas"]["AdminValidationResponse"];
export type BotConfigListResponse = components["schemas"]["BotConfigListResponse"];
export type BotConfigParameter = components["schemas"]["BotConfigParameter"];
export type BotConfigSchema = components["schemas"]["BotConfigSchema"];
export type BotConfigUpdateRequest = components["schemas"]["BotConfigUpdateRequest"];
export type BotConfigUpdateResponse = components["schemas"]["BotConfigUpdateResponse"];
export type BotInfoModel = components["schemas"]["BotInfoModel"];
export type BotJoinLobbyRequest = components["schemas"]["BotJoinLobbyRequest"];
export type BotJoinLobbyResponse = components["schemas"]["BotJoinLobbyResponse"];
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 BotProviderRegisterRequest = components["schemas"]["BotProviderRegisterRequest"];
export type BotProviderRegisterResponse = components["schemas"]["BotProviderRegisterResponse"];
export type ChatMessageModel = components["schemas"]["ChatMessageModel"];
export type ChatMessagesResponse = components["schemas"]["ChatMessagesResponse"];
export type HTTPValidationError = components["schemas"]["HTTPValidationError"];
export type HealthResponse = components["schemas"]["HealthResponse"];
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
export type SessionResponse = components["schemas"]["SessionResponse"];
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"];
export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
// Bot Provider Types (manually defined until API types are regenerated)
export interface BotInfoModel {
name: string;
description: string;
configurable?: boolean;
features?: string[];
}
export interface BotProviderModel {
provider_id: string;
base_url: string;
name: string;
description: string;
registered_at: number;
last_seen: number;
}
export interface BotProviderListResponse {
providers: BotProviderModel[];
}
export interface BotListResponse {
bots: BotInfoModel[];
providers: Record<string, string>;
}
export interface BotJoinLobbyRequest {
lobby_id: string;
nick?: string;
provider_id?: string;
}
export interface BotJoinLobbyResponse {
status: string;
bot_instance_id: string;
bot_name: string;
run_id: string;
provider_id: string;
session_id: string;
}
export interface BotInstanceModel {
bot_instance_id: string;
bot_name: string;
nick: string;
lobby_id: string;
session_id: string;
provider_id: string;
run_id: string;
has_media: boolean;
created_at: number;
}
export interface BotLeaveLobbyRequest {
bot_instance_id: string;
}
export interface BotLeaveLobbyResponse {
status: string;
bot_instance_id: string;
session_id: string;
run_id?: string;
}
export interface BotLeaveLobbyRequest {
session_id: string;
}
export interface BotLeaveLobbyResponse {
status: string;
session_id: string;
run_id?: string;
}
export type LobbyListItem = components["schemas"]["LobbyListItem"];
export type LobbyModel = components["schemas"]["LobbyModel"];
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
export type SessionResponse = components["schemas"]["SessionResponse"];
export type ValidationError = components["schemas"]["ValidationError"];
export class ApiError extends Error {
constructor(public status: number, public statusText: string, public data?: any) {
@ -176,342 +128,219 @@ export class ApiClient {
return response.text() as unknown as T;
}
// Admin API methods
async adminListNames(): Promise<AdminNamesResponse> {
return this.request<AdminNamesResponse>(this.getApiPath("/ai-voicebot/api/admin/names"), { method: "GET" });
// Auto-generated endpoint methods
async getSystemHealth(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/system/health`), { method: "GET" });
}
async adminSetPassword(data: AdminSetPassword): Promise<AdminActionResponse> {
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/set_password"), {
method: "POST",
body: data,
});
async getListNames(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/names`), { method: "GET" });
}
async adminClearPassword(data: AdminClearPassword): Promise<AdminActionResponse> {
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/clear_password"), {
method: "POST",
body: data,
});
async createSetPassword(data: any): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/set_password`), { method: "POST", body: data });
}
// Health check
async healthCheck(): Promise<HealthResponse> {
return this.request<HealthResponse>(this.getApiPath("/ai-voicebot/api/health"), { method: "GET" });
async createClearPassword(data: any): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/clear_password`), { method: "POST", body: data });
}
// Session methods
async getSession(): Promise<SessionResponse> {
return this.request<SessionResponse>(this.getApiPath("/ai-voicebot/api/session"), { method: "GET" });
async createCleanupSessions(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/cleanup_sessions`), { method: "POST" });
}
// Lobby methods
async getLobbies(): Promise<LobbiesResponse> {
return this.request<LobbiesResponse>(this.getApiPath("/ai-voicebot/api/lobby"), { method: "GET" });
async getSessionMetrics(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/session_metrics`), { method: "GET" });
}
async createLobby(sessionId: string, data: LobbyCreateRequest): Promise<LobbyCreateResponse> {
return this.request<LobbyCreateResponse>(this.getApiPath(`/ai-voicebot/api/lobby/${sessionId}`), {
method: "POST",
body: data,
});
async getValidateSessions(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/validate_sessions`), { method: "GET" });
}
// Bot Provider methods
async getBotProviders(): Promise<BotProviderListResponse> {
return this.request<BotProviderListResponse>(this.getApiPath("/ai-voicebot/api/bots/providers"), { method: "GET" });
async createCleanupLobbies(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/cleanup_lobbies`), { method: "POST" });
}
async getAvailableBots(): Promise<BotListResponse> {
return this.request<BotListResponse>(this.getApiPath("/ai-voicebot/api/bots"), { method: "GET" });
async getHealthSummary(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health`), { method: "GET" });
}
async requestBotJoinLobby(botName: string, request: BotJoinLobbyRequest): Promise<BotJoinLobbyResponse> {
return this.request<BotJoinLobbyResponse>(
this.getApiPath(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`),
{
method: "POST",
body: request,
}
);
async getSession(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/session`), { method: "GET" });
}
async requestBotLeaveLobby(botInstanceId: string): Promise<BotLeaveLobbyResponse> {
return this.request<BotLeaveLobbyResponse>(
this.getApiPath(`/ai-voicebot/api/bots/instances/${encodeURIComponent(botInstanceId)}/leave`),
{
method: "POST",
}
);
async getListLobbies(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby`), { method: "GET" });
}
// 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> {
async createLobby(session_id: string, data: any): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${session_id}`), { method: "POST", body: data });
}
}
// API Evolution Detection System
interface ApiEndpoint {
path: string;
method: string;
implemented: boolean;
}
class ApiEvolutionChecker {
private static instance: ApiEvolutionChecker;
private checkedOnce = false;
static getInstance(): ApiEvolutionChecker {
if (!this.instance) {
this.instance = new ApiEvolutionChecker();
}
return this.instance;
async getChatMessages(lobby_id: string, params?: Record<string, string>): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${lobby_id}/chat`), { method: "GET", params });
}
private getImplementedEndpoints(): Set<string> {
// Define all endpoints that are currently implemented in ApiClient
// This list is automatically updated by update-api-client.js
return new Set([
'GET:/ai-voicebot/api/admin/names',
'GET:/ai-voicebot/api/bots',
'GET:/ai-voicebot/api/bots/providers',
'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}',
'POST:/ai-voicebot/api/lobby/{session_id}'
]);
async createRegisterBotProvider(data: any): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/providers/register`), { method: "POST", body: data });
}
private getAvailableEndpoints(): ApiEndpoint[] {
// Extract all endpoints from the generated paths type
const endpoints: ApiEndpoint[] = [];
const implementedSet = this.getImplementedEndpoints();
// Type-safe extraction of paths from the generated schema
const pathKeys = [
'/ai-voicebot/api/admin/names',
'/ai-voicebot/api/admin/set_password',
'/ai-voicebot/api/admin/clear_password',
'/ai-voicebot/api/health',
'/ai-voicebot/api/session',
'/ai-voicebot/api/lobby',
'/ai-voicebot/api/lobby/{session_id}',
'/ai-voicebot/{path}' // Generic catch-all, we'll skip this one
] as const;
pathKeys.forEach(path => {
if (path === '/ai-voicebot/{path}') {
// Skip the generic proxy endpoint
return;
async getListBotProviders(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/providers`), { method: "GET" });
}
// Check each HTTP method that might be available for this path
const possibleMethods = ['get', 'post', 'put', 'delete', 'patch'] as const;
possibleMethods.forEach(method => {
const endpointKey = `${method.toUpperCase()}:${path}`;
const implemented = implementedSet.has(endpointKey);
// We can't directly check if the method exists at runtime due to TypeScript compilation,
// but we know from our grep search what exists. Let's be more explicit:
const knownEndpoints: Record<string, string[]> = {
'/ai-voicebot/api/admin/names': ['GET'],
'/ai-voicebot/api/admin/set_password': ['POST'],
'/ai-voicebot/api/admin/clear_password': ['POST'],
'/ai-voicebot/api/health': ['GET'],
'/ai-voicebot/api/session': ['GET'],
'/ai-voicebot/api/lobby': ['GET'],
'/ai-voicebot/api/lobby/{session_id}': ['POST']
};
const pathMethods = knownEndpoints[path];
if (pathMethods && pathMethods.includes(method.toUpperCase())) {
endpoints.push({
path,
method: method.toUpperCase(),
implemented
});
}
});
});
return endpoints;
async getListAvailableBots(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots`), { method: "GET" });
}
checkForNewEndpoints(): void {
if (this.checkedOnce) {
return; // Only check once per session to avoid spam
async createRequestBotJoinLobby(bot_name: string, data: any): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/${bot_name}/join`), { method: "POST", body: data });
}
this.checkedOnce = true;
try {
const endpoints = this.getAvailableEndpoints();
const unimplemented = endpoints.filter(ep => !ep.implemented);
if (unimplemented.length > 0) {
console.group('🚨 API Evolution Warning');
console.warn(
`Found ${unimplemented.length} API endpoints that are not implemented in ApiClient:`
);
unimplemented.forEach(endpoint => {
console.warn(`${endpoint.method} ${endpoint.path}`);
});
console.warn('Consider updating api-client.ts to implement these endpoints.');
console.warn('Available types can be found in api-types.ts');
console.groupEnd();
} else {
console.info('✅ All available API endpoints are implemented in ApiClient');
async createRequestBotLeaveLobby(bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/instances/${bot_instance_id}/leave`), { method: "POST" });
}
// Also check for potential parameter changes by looking at method signatures
this.checkForParameterChanges();
} catch (error) {
console.warn('Failed to check for API evolution:', error);
}
async getBotInstance(bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/instances/${bot_instance_id}`), { method: "GET" });
}
private checkForParameterChanges(): void {
// This is a simpler check - we could extend this to compare parameter schemas
// For now, we'll just remind developers to check if their implementation matches the schema
console.info('💡 Tip: Regularly compare your method implementations with the generated types in api-types.ts to ensure parameter compatibility');
async getBotConfigSchema(bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}`), { method: "GET" });
}
// Method to manually trigger a fresh check (useful for development)
recheckEndpoints(): void {
this.checkedOnce = false;
this.checkForNewEndpoints();
async getLobbyBotConfigs(lobby_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "GET" });
}
async deleteLobbyConfigs(lobby_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "DELETE" });
}
async getLobbyBotConfig(lobby_id: string, bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_name}`), { method: "GET" });
}
async deleteBotConfig(lobby_id: string, bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_name}`), { method: "DELETE" });
}
async createUpdateBotConfig(data: any, params?: Record<string, string>): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/update`), { method: "POST", body: data, params });
}
async getConfigStatistics(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/statistics`), { method: "GET" });
}
async createRefreshBotSchemas(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/refresh-schemas`), { method: "POST" });
}
async createRefreshBotSchema(bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/refresh`), { method: "POST" });
}
async deleteClearBotSchemaCache(bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/cache`), { method: "DELETE" });
}
async getReadinessProbe(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health/ready`), { method: "GET" });
}
async getLivenessProbe(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health/live`), { method: "GET" });
}
async getCurrentMetrics(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics`), { method: "GET" });
}
async getMetricsHistory(params?: Record<string, string>): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics/history`), { method: "GET", params });
}
async getCacheStatistics(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/cache/stats`), { method: "GET" });
}
async getSystemInfo(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/system/info`), { method: "GET" });
}
async createClearCache(params?: Record<string, string>): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/cache/clear`), { method: "POST", params });
}
async getExportMetricsPrometheus(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics/export`), { method: "GET" });
}
async updateProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" });
}
async getProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" });
}
async deleteProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "DELETE" });
}
async optionsProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "OPTIONS" });
}
async headProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "HEAD" });
}
async createProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" });
}
async replaceProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PUT" });
}
}
// Export the checker for manual use if needed
export const apiEvolutionChecker = ApiEvolutionChecker.getInstance();
// Utility functions for development
export const devUtils = {
/**
* Manually check for API evolution and log results
*/
async checkApiEvolution() {
if (process.env.NODE_ENV !== 'development') {
console.warn('API evolution checking is only available in development mode');
return;
}
try {
const { advancedApiChecker } = await import('./api-evolution-checker');
const evolution = await advancedApiChecker.checkSchemaEvolution();
console.group('🔍 Manual API Evolution Check');
console.log('Unimplemented endpoints:', evolution.unimplementedEndpoints.length);
console.log('New endpoints:', evolution.newEndpoints.length);
console.log('Schema changed:', evolution.hasChangedEndpoints);
if (evolution.unimplementedEndpoints.length > 0) {
console.log('\nImplementation stubs:');
console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints));
}
console.groupEnd();
return evolution;
} catch (error) {
console.error('Failed to check API evolution:', error);
return null;
}
},
/**
* Force a recheck of endpoints (bypasses the "once per session" limitation)
*/
recheckEndpoints() {
apiEvolutionChecker.recheckEndpoints();
}
};
// Default client instance
export const apiClient = new ApiClient();
// Convenience API namespaces
// Convenience API namespaces for easy usage
export const adminApi = {
listNames: () => apiClient.adminListNames(),
setPassword: (data: AdminSetPassword) => apiClient.adminSetPassword(data),
clearPassword: (data: AdminClearPassword) => apiClient.adminClearPassword(data),
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(),
};
export const healthApi = {
check: () => apiClient.getHealthSummary(),
ready: () => apiClient.getReadinessProbe(),
live: () => apiClient.getLivenessProbe(),
};
export const lobbiesApi = {
getAll: () => apiClient.getListLobbies(),
getChatMessages: (lobbyId: string, params?: Record<string, string>) => apiClient.getChatMessages(lobbyId, params),
};
export const healthApi = { check: () => apiClient.healthCheck() };
export const lobbiesApi = { getAll: () => apiClient.getLobbies() };
export const sessionsApi = {
getCurrent: () => apiClient.getSession(),
createLobby: (sessionId: string, data: LobbyCreateRequest) => apiClient.createLobby(sessionId, data),
createLobby: (sessionId: string, data: any) => apiClient.createLobby(sessionId, data),
};
export const botsApi = {
getProviders: () => apiClient.getBotProviders(),
getAvailable: () => apiClient.getAvailableBots(),
requestJoinLobby: (botName: string, request: BotJoinLobbyRequest) => apiClient.requestBotJoinLobby(botName, request),
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),
};
// Automatically check for API evolution when this module is loaded
// This will warn developers if new endpoints are available but not implemented
if (process.env.NODE_ENV === 'development') {
// Import the advanced checker dynamically to avoid circular dependencies
import('./api-evolution-checker').then(({ advancedApiChecker }) => {
// Run the check after a short delay to ensure all modules are loaded
setTimeout(async () => {
try {
const evolution = await advancedApiChecker.checkSchemaEvolution();
if (evolution.unimplementedEndpoints.length > 0 || evolution.hasNewEndpoints) {
console.group('🚨 API Evolution Detection');
if (evolution.hasNewEndpoints && evolution.newEndpoints.length > 0) {
console.warn('🆕 New API endpoints detected:');
evolution.newEndpoints.forEach(ep => {
console.warn(`${ep.method} ${ep.path} (${ep.operationId})`);
});
}
if (evolution.unimplementedEndpoints.length > 0) {
console.warn('⚠️ Unimplemented API endpoints:');
evolution.unimplementedEndpoints.forEach(ep => {
console.warn(`${ep.method} ${ep.path}`);
});
}
if (evolution.hasChangedEndpoints) {
console.warn('🔄 API schema has changed - check for parameter updates');
}
console.groupCollapsed('💡 Implementation suggestions:');
console.log('Add these methods to ApiClient:');
console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints));
console.groupEnd();
console.groupEnd();
} else {
console.info('✅ All API endpoints are implemented');
}
} catch (error) {
console.warn('API evolution check failed:', error);
// Fallback to simple checker
apiEvolutionChecker.checkForNewEndpoints();
}
}, 1000);
});
}

View File

@ -1,201 +1,74 @@
// Advanced API Evolution Detection utilities
// This module provides runtime introspection of OpenAPI schema changes
// Auto-generated API evolution checker
// This file tracks known API endpoints to detect changes
import { paths } from './api-types';
import { base } from "./Common";
import { base } from './Common';
export interface EndpointInfo {
path: string;
method: string;
operationId?: string;
implemented: boolean;
hasParameterChanges?: boolean;
}
export const knownEndpoints = new Set([
`DELETE:${base}/api/bots/config/lobby/{lobby_id}`,
`DELETE:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_name}`,
`DELETE:${base}/api/bots/config/schema/{bot_name}/cache`,
`DELETE:${base}/{path}`,
`GET:${base}/api/admin/names`,
`GET:${base}/api/admin/session_metrics`,
`GET:${base}/api/admin/validate_sessions`,
`GET:${base}/api/bots`,
`GET:${base}/api/bots/config/lobby/{lobby_id}`,
`GET:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_name}`,
`GET:${base}/api/bots/config/schema/{bot_name}`,
`GET:${base}/api/bots/config/statistics`,
`GET:${base}/api/bots/instances/{bot_instance_id}`,
`GET:${base}/api/bots/providers`,
`GET:${base}/api/cache/stats`,
`GET:${base}/api/health`,
`GET:${base}/api/health/live`,
`GET:${base}/api/health/ready`,
`GET:${base}/api/lobby`,
`GET:${base}/api/lobby/{lobby_id}/chat`,
`GET:${base}/api/metrics`,
`GET:${base}/api/metrics/export`,
`GET:${base}/api/metrics/history`,
`GET:${base}/api/session`,
`GET:${base}/api/system/health`,
`GET:${base}/api/system/info`,
`GET:${base}/{path}`,
`HEAD:${base}/{path}`,
`OPTIONS:${base}/{path}`,
`PATCH:${base}/{path}`,
`POST:${base}/api/admin/cleanup_lobbies`,
`POST:${base}/api/admin/cleanup_sessions`,
`POST:${base}/api/admin/clear_password`,
`POST:${base}/api/admin/set_password`,
`POST:${base}/api/bots/config/refresh-schemas`,
`POST:${base}/api/bots/config/schema/{bot_name}/refresh`,
`POST:${base}/api/bots/config/update`,
`POST:${base}/api/bots/instances/{bot_instance_id}/leave`,
`POST:${base}/api/bots/providers/register`,
`POST:${base}/api/bots/{bot_name}/join`,
`POST:${base}/api/cache/clear`,
`POST:${base}/api/lobby/{session_id}`,
`POST:${base}/{path}`,
`PUT:${base}/{path}`
]);
export class AdvancedApiEvolutionChecker {
private static instance: AdvancedApiEvolutionChecker;
private lastSchemaHash: string | null = null;
// Schema path for dynamic usage
export const schemaPath = `${base}/openapi-schema.json`;
static getInstance(): AdvancedApiEvolutionChecker {
if (!this.instance) {
this.instance = new AdvancedApiEvolutionChecker();
}
return this.instance;
}
// Proxy path pattern for matching
export const proxyPathPattern = `${base}/{path}`;
/**
* Get all endpoints from the OpenAPI schema by analyzing the paths type
*/
private extractEndpointsFromSchema(): EndpointInfo[] {
const endpoints: EndpointInfo[] = [];
export function checkApiEvolution(discoveredEndpoints: string[]): {
newEndpoints: string[];
removedEndpoints: string[];
totalEndpoints: number;
} {
const discoveredSet = new Set(discoveredEndpoints);
// In a real implementation, we would need to parse the actual OpenAPI JSON
// since TypeScript types are erased at runtime. For now, we'll maintain
// 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/system/health', method: 'GET', operationId: 'system_health_ai_voicebot_api_system_health_get' }
];
// Get implemented endpoints from ApiClient
const implementedEndpoints = this.getImplementedEndpoints();
knownSchemaEndpoints.forEach((endpoint) => {
const key = `${endpoint.method}:${endpoint.path}`;
endpoints.push({
...endpoint,
implemented: implementedEndpoints.has(key),
});
});
return endpoints;
}
private getImplementedEndpoints(): Set<string> {
return new Set([
'GET:/ai-voicebot/api/admin/names',
'POST:/ai-voicebot/api/admin/set_password',
'POST:/ai-voicebot/api/admin/clear_password',
'GET:/ai-voicebot/api/health',
'GET:/ai-voicebot/api/session',
'GET:/ai-voicebot/api/lobby',
'POST:/ai-voicebot/api/lobby/{sessionId}',
'GET:/ai-voicebot/api/bots/providers',
'GET:/ai-voicebot/api/bots',
'POST:/ai-voicebot/api/lobby/{session_id}',
'GET:/ai-voicebot/api/system/health'
]);
}
/**
* Load the OpenAPI schema from the JSON file to detect runtime changes
*/
async loadSchemaFromJson(): Promise<any> {
try {
// Use dynamic base path from environment
const response = await fetch(`${base}/openapi-schema.json`);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.warn('Could not load OpenAPI schema for evolution checking:', error);
}
return null;
}
/**
* Compare current schema with a previous version to detect changes
*/
async checkSchemaEvolution(): Promise<{
hasNewEndpoints: boolean;
hasChangedEndpoints: boolean;
newEndpoints: EndpointInfo[];
changedEndpoints: EndpointInfo[];
unimplementedEndpoints: EndpointInfo[];
}> {
const currentEndpoints = this.extractEndpointsFromSchema();
const unimplementedEndpoints = currentEndpoints.filter(ep => !ep.implemented);
// Try to load the actual schema for more detailed analysis
const schema = await this.loadSchemaFromJson();
let hasNewEndpoints = false;
let hasChangedEndpoints = false;
let newEndpoints: EndpointInfo[] = [];
let changedEndpoints: EndpointInfo[] = [];
if (schema) {
// Calculate a simple hash of the schema to detect changes
const schemaString = JSON.stringify(schema.paths || {});
const currentHash = this.simpleHash(schemaString);
if (this.lastSchemaHash && this.lastSchemaHash !== currentHash) {
hasChangedEndpoints = true;
console.info('🔄 OpenAPI schema has changed since last check');
}
this.lastSchemaHash = currentHash;
// Extract endpoints from the actual schema
if (schema.paths) {
Object.keys(schema.paths).forEach(path => {
if (path === `${base}/{path}`) return; // Skip generic proxy
const pathObj = schema.paths[path];
Object.keys(pathObj).forEach((method) => {
const endpoint = `${method.toUpperCase()}:${path}`;
const implementedEndpoints = this.getImplementedEndpoints();
if (!implementedEndpoints.has(endpoint)) {
const endpointInfo: EndpointInfo = {
path,
method: method.toUpperCase(),
operationId: pathObj[method].operationId,
implemented: false,
};
// Check if this is a new endpoint (not in our known list)
const isKnown = currentEndpoints.some((ep) => ep.path === path && ep.method === method.toUpperCase());
if (!isKnown) {
hasNewEndpoints = true;
newEndpoints.push(endpointInfo);
}
}
});
});
}
}
const newEndpoints = discoveredEndpoints.filter(ep => !knownEndpoints.has(ep));
const removedEndpoints = Array.from(knownEndpoints).filter(ep => !discoveredSet.has(ep));
return {
hasNewEndpoints,
hasChangedEndpoints,
newEndpoints,
changedEndpoints,
unimplementedEndpoints
removedEndpoints,
totalEndpoints: discoveredEndpoints.length
};
}
private simpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString();
}
/**
* Generate implementation stubs for unimplemented endpoints
*/
generateImplementationStubs(endpoints: EndpointInfo[]): string {
return endpoints.map(endpoint => {
const methodName = this.generateMethodName(endpoint);
const returnType = `Promise<any>`; // Could be more specific with schema analysis
return ` async ${methodName}(): ${returnType} {
return this.request<any>('${endpoint.path}', { method: '${endpoint.method}' });
}`;
}).join('\n\n');
}
private generateMethodName(endpoint: EndpointInfo): string {
// Convert operation ID or path to camelCase method name
if (endpoint.operationId) {
return endpoint.operationId.replace(/_[a-z]/g, (match) => match[1].toUpperCase());
}
// Fallback: generate from path and method
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
const lastPart = pathParts[pathParts.length - 1];
const method = endpoint.method.toLowerCase();
if (method === 'get') {
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
} else {
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
}
}
}
export const advancedApiChecker = AdvancedApiEvolutionChecker.getInstance();

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,13 @@
#!/usr/bin/env node
/**
* Automated API Client and Evolution Checker Updater
* Automated API Client Generator
*
* This script analyzes the generated OpenAPI schema and automatically updates:
* 1. api-client.ts - Adds missing endpoint implementations
* This script analyzes the generated OpenAPI schema and automatically generates:
* 1. api-client.ts - Complete TypeScript API client with types and methods
* 2. api-evolution-checker.ts - Updates known endpoints list
*
* Run this script after generating TypeScript types from OpenAPI schema.
* The api-client.ts file contains only minimal imports at the top, with everything
* else being auto-generated after a single marker.
*/
const fs = require('fs');
@ -18,11 +19,10 @@ const API_TYPES_PATH = path.join(__dirname, 'src', 'api-types.ts');
const API_CLIENT_PATH = path.join(__dirname, 'src', 'api-client.ts');
const API_EVOLUTION_CHECKER_PATH = path.join(__dirname, 'src', 'api-evolution-checker.ts');
class ApiClientUpdater {
class ApiClientGenerator {
constructor() {
this.schema = null;
this.endpoints = [];
this.implementedEndpoints = new Set();
}
/**
@ -51,21 +51,18 @@ class ApiClientUpdater {
this.endpoints = [];
Object.entries(this.schema.paths).forEach(([path, pathObj]) => {
// Skip the generic proxy endpoint
if (path === '/ai-voicebot/{path}') {
return;
}
Object.entries(this.schema.paths).forEach(([path, pathItem]) => {
Object.entries(pathItem).forEach(([method, operation]) => {
if (method === 'parameters') return; // Skip path-level parameters
Object.entries(pathObj).forEach(([method, methodObj]) => {
const endpoint = {
path,
method: method.toUpperCase(),
operationId: methodObj.operationId,
summary: methodObj.summary,
parameters: methodObj.parameters || [],
requestBody: methodObj.requestBody,
responses: methodObj.responses
operationId: operation.operationId,
summary: operation.summary,
requestBody: operation.requestBody,
parameters: operation.parameters || [],
responses: operation.responses || {}
};
this.endpoints.push(endpoint);
@ -76,88 +73,52 @@ class ApiClientUpdater {
}
/**
* Parse the current API client to find implemented endpoints
*/
parseCurrentApiClient() {
try {
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
// Find implemented endpoints by looking for method patterns and path literals
// Now handles both old hardcoded paths and new this.getApiPath() calls
const methodRegex = /async\s+(\w+)\([^)]*\):[^{]*{\s*return\s+this\.request<[^>]*>\(([^,]+),\s*{\s*method:\s*"([^"]+)"/g;
let match;
while ((match = methodRegex.exec(clientContent)) !== null) {
const [, methodName, pathExpression, httpMethod] = match;
// Handle both string literals, template literals, and this.getApiPath() calls
let actualPath;
if (pathExpression.includes('this.getApiPath(')) {
// Extract path from this.getApiPath("path") or this.getApiPath(`path`)
const getApiPathMatch = pathExpression.match(/this\.getApiPath\(([^)]+)\)/);
if (getApiPathMatch) {
let innerPath = getApiPathMatch[1];
// Remove quotes or backticks
if ((innerPath.startsWith('"') && innerPath.endsWith('"')) ||
(innerPath.startsWith("'") && innerPath.endsWith("'"))) {
actualPath = innerPath.slice(1, -1);
} else if (innerPath.startsWith('`') && innerPath.endsWith('`')) {
// Template literal - convert ${param} back to {param}
actualPath = innerPath.slice(1, -1).replace(/\$\{([^}]+)\}/g, '{$1}');
}
}
} else if (pathExpression.startsWith('"') && pathExpression.endsWith('"')) {
// String literal
actualPath = pathExpression.slice(1, -1);
} else if (pathExpression.startsWith('`') && pathExpression.endsWith('`')) {
// Template literal - convert ${param} back to {param}
actualPath = pathExpression.slice(1, -1).replace(/\$\{([^}]+)\}/g, '{$1}');
} else {
// Skip complex expressions we can't easily parse
continue;
}
if (actualPath) {
this.implementedEndpoints.add(`${httpMethod}:${actualPath}`);
}
}
console.log(`✅ Found ${this.implementedEndpoints.size} implemented endpoints in API client`);
} catch (error) {
console.error('❌ Failed to parse current API client:', error.message);
}
}
/**
* Generate method name from operation ID or path
* Generate method name from operation ID or path with HTTP method prefix
*/
generateMethodName(endpoint) {
let baseName;
if (endpoint.operationId) {
// Convert snake_case operation ID to camelCase
return endpoint.operationId
baseName = endpoint.operationId
.replace(/_ai_voicebot_.*$/, '') // Remove the long suffix
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
.replace(/^([a-z])/, (_, letter) => letter.toLowerCase());
}
// Fallback: generate from path and method
} else {
// Fallback: generate from path
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
const lastPart = pathParts[pathParts.length - 1] || 'resource';
const method = endpoint.method.toLowerCase();
if (method === 'get') {
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
} else {
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
baseName = lastPart.charAt(0).toLowerCase() + lastPart.slice(1);
}
// Add method prefix to avoid duplicates
const method = endpoint.method.toLowerCase();
const methodPrefixes = {
'get': 'get',
'post': 'create',
'put': 'replace', // Use 'replace' for PUT to distinguish from PATCH
'patch': 'update', // Use 'update' for PATCH
'delete': 'delete',
'head': 'head',
'options': 'options'
};
const prefix = methodPrefixes[method] || method;
// If baseName already starts with the prefix, don't duplicate
if (baseName.toLowerCase().startsWith(prefix.toLowerCase())) {
return baseName;
}
// Capitalize first letter of baseName and add prefix
const capitalizedBaseName = baseName.charAt(0).toUpperCase() + baseName.slice(1);
return prefix + capitalizedBaseName;
}
/**
* Generate parameter types and method signature
* Generate method implementation
*/
generateMethodSignature(endpoint) {
generateMethodImplementation(endpoint) {
const methodName = this.generateMethodName(endpoint);
let params = [];
let pathParams = [];
@ -174,113 +135,285 @@ class ApiClientUpdater {
// Check for request body
if (endpoint.requestBody) {
params.push('data: any'); // Could be more specific with schema analysis
params.push('data: any');
}
// Check for query parameters
if (endpoint.parameters && endpoint.parameters.length > 0) {
const queryParams = endpoint.parameters.filter(p => p.in === 'query');
if (queryParams.length > 0) {
const hasQueryParams = endpoint.parameters && endpoint.parameters.some(p => p.in === 'query');
if (hasQueryParams) {
params.push('params?: Record<string, string>');
}
}
const paramString = params.join(', ');
const returnType = 'Promise<any>'; // Could be more specific
const methodSignature = `async ${methodName}(${params.join(', ')}): Promise<any>`;
return {
methodName,
paramString,
returnType,
pathParams
};
}
/**
* Generate method implementation
*/
generateMethodImplementation(endpoint) {
const { methodName, paramString, returnType, pathParams } = this.generateMethodSignature(endpoint);
// Use this.getApiPath() to make paths dynamic based on PUBLIC_URL
let pathExpression;
if (pathParams.length > 0) {
// For parameterized paths, we need to use template literals
pathExpression = 'this.getApiPath(`' + endpoint.path.replace(/{([^}]+)}/g, '${$1}') + '`)';
} else {
// For static paths, use string literal
pathExpression = `this.getApiPath("${endpoint.path}")`;
}
// Build the path with parameter substitution
let apiPath = endpoint.path;
pathParams.forEach(param => {
apiPath = apiPath.replace(`{${param}}`, `\${${param}}`);
});
// Build the request options
let requestOptions = `{ method: "${endpoint.method}"`;
if (endpoint.requestBody) {
if (endpoint.requestBody && endpoint.method !== 'GET') {
requestOptions += ', body: data';
}
if (endpoint.parameters && endpoint.parameters.some(p => p.in === 'query')) {
if (hasQueryParams) {
requestOptions += ', params';
}
requestOptions += ' }';
return ` async ${methodName}(${paramString}): ${returnType} {
return this.request<any>(${pathExpression}, ${requestOptions});
return ` ${methodSignature} {
return this.request<any>(this.getApiPath(\`${apiPath}\`), ${requestOptions});
}`;
}
/**
* Update the API client with missing endpoints
* Extract all types from schema for re-export
*/
updateApiClient() {
try {
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
// Find unimplemented endpoints
const unimplementedEndpoints = this.endpoints.filter(endpoint => {
const key = `${endpoint.method}:${endpoint.path}`;
return !this.implementedEndpoints.has(key);
});
if (unimplementedEndpoints.length === 0) {
console.log('✅ All endpoints are already implemented in API client');
return;
extractAllTypes() {
if (!this.schema || !this.schema.components || !this.schema.components.schemas) {
return '// No schema types available for export';
}
console.log(`🔧 Adding ${unimplementedEndpoints.length} missing endpoints to API client`);
const schemas = this.schema.components.schemas;
const allTypeNames = Object.keys(schemas).sort();
// Generate new method implementations
const newMethods = unimplementedEndpoints.map(endpoint => {
if (allTypeNames.length === 0) {
return '// No types found in schema';
}
const typeExports = allTypeNames.map(typeName =>
`export type ${typeName} = components["schemas"]["${typeName}"];`
).join('\n');
return `// Re-export all types from the generated schema for convenience\n${typeExports}`;
}
/**
* 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(),
};
export const healthApi = {
check: () => apiClient.getHealthSummary(),
ready: () => apiClient.getReadinessProbe(),
live: () => apiClient.getLivenessProbe(),
};
export const lobbiesApi = {
getAll: () => apiClient.getListLobbies(),
getChatMessages: (lobbyId: string, params?: Record<string, string>) => apiClient.getChatMessages(lobbyId, params),
};
export const sessionsApi = {
getCurrent: () => apiClient.getSession(),
createLobby: (sessionId: string, data: any) => apiClient.createLobby(sessionId, data),
};
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),
};`;
}
/**
* Create minimal template with just imports
*/
createMinimalTemplate() {
const template = `// TypeScript API client for AI Voicebot server
import { components } from "./api-types";
import { base } from "./Common";
// DO NOT MANUALLY EDIT BELOW THIS LINE - All content below is auto-generated
// To modify auto-generated content, edit the template in client/update-api-client.js
`;
fs.writeFileSync(API_CLIENT_PATH, template, 'utf8');
console.log('✅ Created minimal API client template');
}
/**
* Generate complete API client from scratch
*/
generateFullApiClient() {
try {
// Generate all components
const typeExports = this.extractAllTypes();
const allMethods = this.endpoints.map(endpoint => {
console.log(`${endpoint.method} ${endpoint.path} (${this.generateMethodName(endpoint)})`);
return this.generateMethodImplementation(endpoint);
}).join('\n\n');
// Find the insertion point (before the auto-generated comment and closing brace)
const insertionMarker = ' // Auto-generated endpoints will be added here by update-api-client.js';
const insertionIndex = clientContent.indexOf(insertionMarker);
const convenienceApis = this.generateConvenienceApis();
if (insertionIndex === -1) {
throw new Error('Could not find auto-generated endpoints insertion marker in ApiClient class');
const autoGeneratedContent = `
// Re-export all types from the generated schema
export type { components } from "./api-types";
${typeExports}
export class ApiError extends Error {
constructor(public status: number, public statusText: string, public data?: any) {
super(\`HTTP \${status}: \${statusText}\`);
this.name = "ApiError";
}
}
export class ApiClient {
private baseURL: string;
private defaultHeaders: Record<string, string>;
constructor(baseURL?: string) {
// Use the current window location instead of localhost, just like WebSocket connections
const defaultBaseURL =
typeof window !== "undefined" ? \`\${window.location.protocol}//\${window.location.host}\` : "http://localhost:8001";
this.baseURL = baseURL || process.env.REACT_APP_API_URL || defaultBaseURL;
this.defaultHeaders = {};
}
// Find the end of the marker line
const markerEndIndex = clientContent.indexOf('\n', insertionIndex);
const doNotEditLine = clientContent.indexOf('\n', markerEndIndex + 1);
/**
* Construct API path using PUBLIC_URL environment variable
* Replaces hardcoded /ai-voicebot prefix with dynamic base from environment
*/
private getApiPath(schemaPath: string): string {
// Replace the hardcoded /ai-voicebot prefix with the dynamic base
return schemaPath.replace("/ai-voicebot", base);
}
// Insert the new methods after the "DO NOT MANUALLY EDIT BELOW THIS LINE" comment
const updatedContent =
clientContent.slice(0, doNotEditLine + 1) +
'\n // Auto-generated endpoints\n' +
newMethods +
'\n' +
clientContent.slice(doNotEditLine + 1);
private async request<T>(
path: string,
options: {
method: string;
body?: any;
params?: Record<string, string>;
}
): Promise<T> {
const url = new URL(path, this.baseURL);
// Write the updated content
fs.writeFileSync(API_CLIENT_PATH, updatedContent, 'utf8');
console.log('✅ Updated API client with new endpoints');
if (options.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
// Also update the implemented endpoints list
this.updateApiClientImplementedEndpointsList(unimplementedEndpoints);
const requestInit: RequestInit = {
method: options.method,
headers: {
"Content-Type": "application/json",
...this.defaultHeaders,
},
};
if (options.body && options.method !== "GET") {
requestInit.body = JSON.stringify(options.body);
}
const response = await fetch(url.toString(), requestInit);
if (!response.ok) {
let errorData;
// Clone the response before trying to read it, in case JSON parsing fails
const responseClone = response.clone();
try {
errorData = await response.json();
} catch {
try {
errorData = await responseClone.text();
} catch {
errorData = \`HTTP \${response.status}: \${response.statusText}\`;
}
}
throw new ApiError(response.status, response.statusText, errorData);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return response.json();
}
return response.text() as unknown as T;
}
// Auto-generated endpoint methods
${allMethods}
}
// Default client instance
export const apiClient = new ApiClient();
${convenienceApis}
`;
// Read the current file and find the marker
const currentContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
const marker = '// DO NOT MANUALLY EDIT BELOW THIS LINE';
const markerIndex = currentContent.indexOf(marker);
if (markerIndex === -1) {
console.error('❌ Could not find auto-generation marker');
return;
}
// Find the end of the marker line (including the guidance comment)
const markerEndIndex = currentContent.indexOf('\n', markerIndex);
const guidanceEndIndex = currentContent.indexOf('\n', markerEndIndex + 1);
// Keep everything up to and including the guidance comment, replace everything after
const templatePart = currentContent.slice(0, guidanceEndIndex + 1);
const fullContent = templatePart + autoGeneratedContent;
fs.writeFileSync(API_CLIENT_PATH, fullContent, 'utf8');
console.log(`✅ Generated complete API client with ${this.endpoints.length} endpoints`);
} catch (error) {
console.error('❌ Failed to generate API client:', error.message);
}
}
/**
* Update existing API client
*/
updateApiClient() {
try {
// Check if file exists
if (!fs.existsSync(API_CLIENT_PATH)) {
console.log('📝 API client file not found, creating minimal template...');
this.createMinimalTemplate();
}
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
// Check if this is a minimal template (only has imports and marker)
const marker = '// DO NOT MANUALLY EDIT BELOW THIS LINE';
const markerIndex = clientContent.indexOf(marker);
if (markerIndex === -1) {
console.log('⚠️ Auto-generation marker not found, creating new template...');
this.createMinimalTemplate();
}
// Always regenerate everything after the marker
console.log('🔄 Regenerating complete API client...');
this.generateFullApiClient();
} catch (error) {
console.error('❌ Failed to update API client:', error.message);
@ -288,182 +421,95 @@ class ApiClientUpdater {
}
/**
* Update the implemented endpoints list in ApiClient
* Update API evolution checker
*/
updateApiClientImplementedEndpointsList(newEndpoints) {
updateApiEvolutionChecker() {
try {
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
// Generate list of all current endpoints for evolution tracking
// Convert paths to use dynamic base instead of hardcoded prefix
const currentEndpoints = this.endpoints.map(ep => {
const dynamicPath = ep.path.replace('/ai-voicebot', '${base}');
return `${ep.method}:${dynamicPath}`;
}).sort();
// Add new endpoints to the implemented list
const newEndpointKeys = newEndpoints.map(ep => `${ep.method}:${ep.path}`);
const allImplementedEndpoints = Array.from(this.implementedEndpoints)
.concat(newEndpointKeys)
.sort();
const evolutionContent = `// Auto-generated API evolution checker
// This file tracks known API endpoints to detect changes
const implementedArray = allImplementedEndpoints
.map(endpoint => ` '${endpoint}'`)
.join(',\n');
import { base } from './Common';
const newImplementedEndpoints = `return new Set([
${implementedArray}
]);`;
export const knownEndpoints = new Set([
${currentEndpoints.map(ep => ` \`${ep}\``).join(',\n')}
]);
// Replace the existing implemented endpoints set in ApiClient
const implementedRegex = /private getImplementedEndpoints\(\): Set<string> \{[\s\S]*?return new Set\(\[[^\]]*\]\);[\s\S]*?\}/;
// Schema path for dynamic usage
export const schemaPath = \`\${base}/openapi-schema.json\`;
const replacement = `private getImplementedEndpoints(): Set<string> {
// Define all endpoints that are currently implemented in ApiClient
// This list is automatically updated by update-api-client.js
${newImplementedEndpoints}
}`;
// Proxy path pattern for matching
export const proxyPathPattern = \`\${base}/{path}\`;
if (implementedRegex.test(clientContent)) {
const updatedContent = clientContent.replace(implementedRegex, replacement);
fs.writeFileSync(API_CLIENT_PATH, updatedContent, 'utf8');
console.log('✅ Updated implemented endpoints list in API client');
} else {
console.warn('⚠️ Could not find getImplementedEndpoints method in API client');
}
export function checkApiEvolution(discoveredEndpoints: string[]): {
newEndpoints: string[];
removedEndpoints: string[];
totalEndpoints: number;
} {
const discoveredSet = new Set(discoveredEndpoints);
} catch (error) {
console.error('❌ Failed to update implemented endpoints list in API client:', error.message);
}
}
const newEndpoints = discoveredEndpoints.filter(ep => !knownEndpoints.has(ep));
const removedEndpoints = Array.from(knownEndpoints).filter(ep => !discoveredSet.has(ep));
/**
* Update the API evolution checker with current endpoints
*/
updateEvolutionChecker() {
try {
const checkerContent = fs.readFileSync(API_EVOLUTION_CHECKER_PATH, 'utf8');
return {
newEndpoints,
removedEndpoints,
totalEndpoints: discoveredEndpoints.length
};
}
`;
// Generate the updated known endpoints list
const knownEndpointsArray = this.endpoints.map(endpoint => {
return ` { path: '${endpoint.path}', method: '${endpoint.method}', operationId: '${endpoint.operationId || ''}' }`;
}).join(',\n');
const newKnownEndpoints = `const knownSchemaEndpoints = [
${knownEndpointsArray}
];`;
// Replace the existing knownSchemaEndpoints array
const knownEndpointsRegex = /const knownSchemaEndpoints = \[[^\]]+\];/s;
if (knownEndpointsRegex.test(checkerContent)) {
const updatedContent = checkerContent.replace(knownEndpointsRegex, newKnownEndpoints);
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, updatedContent, 'utf8');
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, evolutionContent, 'utf8');
console.log('✅ Updated API evolution checker with current endpoints');
} else {
console.warn('⚠️ Could not find knownSchemaEndpoints array in evolution checker');
}
} catch (error) {
console.error('❌ Failed to update evolution checker:', error.message);
console.error('❌ Failed to update API evolution checker:', error.message);
}
}
/**
* Update the implemented endpoints list in the evolution checker
*/
updateImplementedEndpointsList() {
try {
const checkerContent = fs.readFileSync(API_EVOLUTION_CHECKER_PATH, 'utf8');
// Generate the updated implemented endpoints list
const implementedArray = Array.from(this.implementedEndpoints)
.concat(this.endpoints.map(ep => `${ep.method}:${ep.path}`))
.filter((value, index, self) => self.indexOf(value) === index) // Remove duplicates
.map(endpoint => ` '${endpoint}'`)
.join(',\n');
const newImplementedEndpoints = `return new Set([
${implementedArray}
]);`;
// Replace the existing implemented endpoints set
const implementedRegex = /return new Set\(\[[^\]]+\]\);/s;
if (implementedRegex.test(checkerContent)) {
const updatedContent = checkerContent.replace(implementedRegex, newImplementedEndpoints);
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, updatedContent, 'utf8');
console.log('✅ Updated implemented endpoints list in evolution checker');
} else {
console.warn('⚠️ Could not find implemented endpoints set in evolution checker');
}
} catch (error) {
console.error('❌ Failed to update implemented endpoints list:', error.message);
}
}
/**
* Generate updated type exports for the API client
*/
updateTypeExports() {
try {
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
// Extract all schema types from the generated api-types.ts
const typesContent = fs.readFileSync(API_TYPES_PATH, 'utf8');
const schemaRegex = /export interface paths \{[\s\S]*?\n\}/;
const componentsRegex = /export interface components \{[\s\S]*schemas: \{[\s\S]*?\n \};\n\}/;
// Find all schema names in components.schemas
const schemaNames = [];
const schemaMatches = typesContent.match(/"([^"]+)":\s*\{/g);
if (schemaMatches) {
schemaMatches.forEach(match => {
const name = match.match(/"([^"]+)":/)[1];
if (name && !name.includes('ValidationError')) {
schemaNames.push(name);
}
});
}
console.log(`✅ Found ${schemaNames.length} schema types for potential export`);
} catch (error) {
console.error('❌ Failed to update type exports:', error.message);
}
}
/**
* Run the complete update process
* Main execution method
*/
async run() {
console.log('🚀 Starting automated API client update...\n');
console.log('🚀 Starting automated API client generation...');
// Load and validate schema
if (!this.loadSchema()) {
process.exit(1);
}
// Extract endpoints
this.extractEndpoints();
this.parseCurrentApiClient();
console.log('\n📊 Analysis Results:');
if (this.endpoints.length === 0) {
console.error('❌ No endpoints found in schema');
process.exit(1);
}
console.log(`\n📊 Analysis Results:`);
console.log(` • Schema endpoints: ${this.endpoints.length}`);
console.log(` • Implemented endpoints: ${this.implementedEndpoints.size}`);
console.log(` • Missing endpoints: ${this.endpoints.length - this.implementedEndpoints.size}\n`);
// Update files
// Update/generate API client
this.updateApiClient();
this.updateEvolutionChecker();
this.updateImplementedEndpointsList();
console.log('\n✅ API client update complete!');
// Update evolution checker
this.updateApiEvolutionChecker();
console.log('\n✅ API client generation complete!');
console.log('📄 Updated files:');
console.log(' - api-client.ts (added missing endpoints)');
console.log(' - api-client.ts (regenerated)');
console.log(' - api-evolution-checker.ts (updated known endpoints)');
}
}
// Run the updater
// Run if called directly
if (require.main === module) {
const updater = new ApiClientUpdater();
updater.run().catch(error => {
console.error('❌ Update failed:', error);
process.exit(1);
});
const generator = new ApiClientGenerator();
generator.run().catch(console.error);
}
module.exports = ApiClientUpdater;

View File

@ -1,9 +1,10 @@
#!/bin/bash
# Comprehensive script to generate TypeScript types from FastAPI OpenAPI schema.
# This script coordinates between the server and frontend containers to:
# 1. Generate OpenAPI schema from FastAPI server
# 2. Generate TypeScript types from the schema
# 3. Ensure frontend container dependencies are installed
# 1. Ensure the server is running and ready
# 2. Generate OpenAPI schema from the running FastAPI server
# 3. Generate TypeScript types from the schema
# 4. Ensure frontend container dependencies are installed
set -e
@ -12,29 +13,61 @@ echo "🚀 Starting OpenAPI TypeScript generation process..."
# Change to the project directory
cd "$(dirname "$0")"
echo "📋 Step 1: Generating OpenAPI schema from FastAPI server..."
echo "📋 Step 1: Ensuring server container is running and ready..."
docker compose up -d server
# Load environment variables
source .env
# Build health check URL from environment variables
HEALTH_URL="${PUBLIC_SERVER_URL}${PUBLIC_URL}/api/system/health"
echo "🔍 Using health check URL: $HEALTH_URL"
# Wait for server to be ready
echo "⏳ Waiting for server to be ready..."
max_retries=30
retry_count=0
while [ $retry_count -lt $max_retries ]; do
# Use curl with SSL verification disabled for self-signed certs
if docker compose exec server curl -f -k "$HEALTH_URL" >/dev/null 2>&1; then
echo "✅ Server is ready!"
break
fi
retry_count=$((retry_count + 1))
echo " Attempt $retry_count/$max_retries - Server not ready yet, waiting..."
sleep 2
done
if [ $retry_count -eq $max_retries ]; then
echo "❌ Server failed to become ready within timeout"
echo "📋 Server logs:"
docker compose logs server --tail=20
exit 1
fi
echo "📋 Step 2: Generating OpenAPI schema from running 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..."
echo "📋 Step 3: Ensuring frontend container is running..."
docker compose up -d client
echo "📋 Step 3: Installing/updating frontend dependencies..."
echo "📋 Step 4: Installing/updating frontend dependencies..."
docker compose exec client npm install --legacy-peer-deps
echo "📋 Step 4: Generating TypeScript types from OpenAPI schema..."
echo "📋 Step 5: Generating TypeScript types from OpenAPI schema..."
docker compose exec client npx openapi-typescript openapi-schema.json -o src/api-types.ts
echo "📋 Step 5: Automatically updating API client and evolution checker..."
echo "📋 Step 6: Automatically updating API client and evolution checker..."
docker compose exec client node update-api-client.js
echo "📋 Step 6: Running TypeScript type checking..."
echo "📋 Step 7: Running TypeScript type checking..."
docker compose exec client npm run type-check
echo "📋 Step 7: Testing dynamic path usage..."
echo "📋 Step 8: Testing dynamic path usage..."
docker compose exec client npm run test-dynamic-paths
echo "📋 Step 8: Running API evolution check..."
echo "📋 Step 9: Running API evolution check..."
docker compose exec client node check-api-evolution.js
echo "✅ TypeScript generation and API client update complete!"

View File

@ -1,26 +1,69 @@
#!/usr/bin/env python3
"""
Simple OpenAPI schema generator for FastAPI.
This generates only the JSON schema file that can be used with openapi-typescript.
This generates the JSON schema file by fetching it from the running server's OpenAPI endpoint.
This ensures we get the complete schema as the server sees it at runtime.
"""
import json
import sys
import os
import asyncio
from pathlib import Path
import httpx
def generate_schema():
"""Generate OpenAPI schema from the FastAPI app"""
async def generate_schema_async():
"""Generate OpenAPI schema from the running FastAPI server"""
try:
# Add shared module to path for Docker environment
shared_path = "/shared"
if shared_path not in sys.path:
sys.path.insert(0, shared_path)
# Configuration
public_url = os.getenv("PUBLIC_URL", "/")
if not public_url.endswith("/"):
public_url += "/"
# Import the FastAPI app
from main import app
# Server endpoint - use PUBLIC_SERVER_URL from .env
server_url = os.getenv("PUBLIC_SERVER_URL", "https://server:8000")
# Get the OpenAPI schema
schema = app.openapi()
# Determine if SSL verification should be disabled based on protocol
use_ssl = server_url.startswith("https://")
verify_ssl = not use_ssl or os.getenv("SSL_VERIFY", "false").lower() == "true"
# OpenAPI schema endpoint
openapi_url = f"{server_url}{public_url}openapi.json"
print(f"🔍 Fetching OpenAPI schema from: {openapi_url}")
print(
f"🔒 SSL mode: {'enabled' if use_ssl else 'disabled'}, verification: {'enabled' if verify_ssl else 'disabled'}"
)
# Wait for server to be ready and fetch schema
max_retries = 10
retry_delay = 2
for attempt in range(max_retries):
try:
# Create HTTP client with appropriate SSL settings
async with httpx.AsyncClient(timeout=30.0, verify=verify_ssl) as client:
response = await client.get(openapi_url)
response.raise_for_status()
schema = response.json()
break
except (httpx.ConnectError, httpx.TimeoutException) as e:
if attempt < max_retries - 1:
print(
f"⏳ Server not ready (attempt {attempt + 1}/{max_retries}), waiting {retry_delay}s..."
)
await asyncio.sleep(retry_delay)
continue
else:
raise Exception(
f"Failed to connect to server after {max_retries} attempts: {e}"
)
except httpx.HTTPStatusError as e:
raise Exception(
f"Server returned error {e.response.status_code}: {e.response.text}"
)
else:
raise Exception("Failed to fetch schema - max retries exceeded")
# Write the schema to a JSON file that can be accessed from outside container
schema_file = Path("/client/openapi-schema.json")
@ -28,8 +71,16 @@ def generate_schema():
json.dump(schema, f, indent=2, ensure_ascii=False)
print(f"✅ OpenAPI schema generated successfully at: {schema_file}")
print(f"Schema contains {len(schema.get('paths', {}))} API paths")
print(f"Schema contains {len(schema.get('components', {}).get('schemas', {}))} component schemas")
# Print some info about what endpoints were found
paths = schema.get("paths", {})
print("📋 Found API endpoints:")
for path in sorted(paths.keys()):
methods = list(paths[path].keys())
print(f" {path} ({', '.join(methods)})")
return True
except Exception as e:
@ -38,6 +89,15 @@ def generate_schema():
traceback.print_exc()
return False
def generate_schema():
"""Synchronous wrapper for async schema generation"""
try:
return asyncio.run(generate_schema_async())
except Exception as e:
print(f"❌ Error in schema generation wrapper: {e}")
return False
if __name__ == "__main__":
success = generate_schema()
sys.exit(0 if success else 1)

View File

@ -86,11 +86,18 @@ if not public_url.endswith("/"):
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", None)
# Create FastAPI app first
# Create FastAPI app with proper URL configuration
openapi_url = f"{public_url}openapi.json"
docs_url = f"{public_url}docs"
redoc_url = f"{public_url}redoc"
app = FastAPI(
title="AI Voice Bot Server (Refactored)",
description="WebRTC voice chat server with modular architecture",
version="2.0.0",
openapi_url=openapi_url,
docs_url=docs_url,
redoc_url=redoc_url,
)
logger.info(f"Starting server with public URL: {public_url}")