ai-voicebot/client/src/api-client.ts

484 lines
16 KiB
TypeScript

// TypeScript API client for AI Voicebot server
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"];
// Type aliases for API methods
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
export type HealthResponse = components["schemas"]["HealthResponse"];
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
export type SessionResponse = components["schemas"]["SessionResponse"];
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;
}
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 {
bot_name: string;
lobby_id: string;
nick?: string;
provider_id?: string;
}
export interface BotJoinLobbyResponse {
status: string;
bot_name: string;
run_id: string;
provider_id: string;
}
export interface BotLeaveLobbyRequest {
session_id: string;
}
export interface BotLeaveLobbyResponse {
status: string;
session_id: string;
run_id?: string;
}
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 = {};
}
/**
* 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);
}
private async request<T>(
path: string,
options: {
method: string;
body?: any;
params?: Record<string, string>;
}
): Promise<T> {
const url = new URL(path, this.baseURL);
if (options.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
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;
}
// Admin API methods
async adminListNames(): Promise<AdminNamesResponse> {
return this.request<AdminNamesResponse>(this.getApiPath("/ai-voicebot/api/admin/names"), { 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 adminClearPassword(data: AdminClearPassword): Promise<AdminActionResponse> {
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/clear_password"), {
method: "POST",
body: data,
});
}
// Health check
async healthCheck(): Promise<HealthResponse> {
return this.request<HealthResponse>(this.getApiPath("/ai-voicebot/api/health"), { method: "GET" });
}
// Session methods
async getSession(): Promise<SessionResponse> {
return this.request<SessionResponse>(this.getApiPath("/ai-voicebot/api/session"), { method: "GET" });
}
// Lobby methods
async getLobbies(): Promise<LobbiesResponse> {
return this.request<LobbiesResponse>(this.getApiPath("/ai-voicebot/api/lobby"), { 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,
});
}
// Bot Provider methods
async getBotProviders(): Promise<BotProviderListResponse> {
return this.request<BotProviderListResponse>(this.getApiPath("/ai-voicebot/api/bots/providers"), { method: "GET" });
}
async getAvailableBots(): Promise<BotListResponse> {
return this.request<BotListResponse>(this.getApiPath("/ai-voicebot/api/bots"), { 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 requestBotLeaveLobby(request: BotLeaveLobbyRequest): Promise<BotLeaveLobbyResponse> {
return this.request<BotLeaveLobbyResponse>(this.getApiPath("/ai-voicebot/api/bots/leave"), {
method: "POST",
body: request,
});
}
// Auto-generated endpoints will be added here by update-api-client.js
// DO NOT MANUALLY EDIT BELOW THIS LINE
// 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 });
}
}
// 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;
}
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',
'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}'
]);
}
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;
}
// 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;
}
checkForNewEndpoints(): void {
if (this.checkedOnce) {
return; // Only check once per session to avoid spam
}
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');
}
// Also check for potential parameter changes by looking at method signatures
this.checkForParameterChanges();
} catch (error) {
console.warn('Failed to check for API evolution:', error);
}
}
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');
}
// Method to manually trigger a fresh check (useful for development)
recheckEndpoints(): void {
this.checkedOnce = false;
this.checkForNewEndpoints();
}
}
// 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
export const adminApi = {
listNames: () => apiClient.adminListNames(),
setPassword: (data: AdminSetPassword) => apiClient.adminSetPassword(data),
clearPassword: (data: AdminClearPassword) => apiClient.adminClearPassword(data),
};
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),
};
export const botsApi = {
getProviders: () => apiClient.getBotProviders(),
getAvailable: () => apiClient.getAvailableBots(),
requestJoinLobby: (botName: string, request: BotJoinLobbyRequest) => apiClient.requestBotJoinLobby(botName, request),
};
// 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);
});
}