Tripagra
Add api/index.py for Vercel serverless function and update frontend endpoint
6aa1546
Raw
History Blame Contribute Delete
4.02 kB
/**
* API client for Smart Escalation API
* Handles communication with the backend /ask endpoint
*/
// API Response Types
export interface ApiResponse {
response_type: 'answer' | 'escalation';
message: string;
confidence_explanation: string;
sources: string[] | null;
}
// API Request Types
export interface QuestionRequest {
question: string;
}
// Error Types
export class ApiError extends Error {
statusCode?: number;
originalError?: unknown;
constructor(
message: string,
statusCode?: number,
originalError?: unknown
) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.originalError = originalError;
}
}
/**
* Get API URL from environment variables
* For Vercel deployment, uses relative path /api
* For local development, uses localhost:8000
*/
const getApiUrl = (): string => {
// In production (Vercel), use relative path to serverless function
// In development, use environment variable or default to localhost
return import.meta.env.VITE_API_URL || '/api';
};
/**
* POST a question to the API endpoint
*
* @param question - The customer question to submit
* @returns Promise resolving to the API response
* @throws ApiError on network errors, timeouts, or invalid responses
*/
export const askQuestion = async (question: string): Promise<ApiResponse> => {
const apiUrl = getApiUrl();
const endpoint = apiUrl; // Use /api directly (not /api/ask)
// Validate input
if (!question || question.trim().length === 0) {
throw new ApiError('Question cannot be empty', 400);
}
const requestBody: QuestionRequest = {
question: question.trim()
};
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
signal: controller.signal,
});
clearTimeout(timeoutId);
// Handle HTTP errors
if (!response.ok) {
let errorMessage = `API request failed with status ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorData.message || errorMessage;
} catch {
// If error response is not JSON, use status text
errorMessage = response.statusText || errorMessage;
}
throw new ApiError(errorMessage, response.status);
}
// Parse response
const data = await response.json();
// Validate response structure
if (!isValidApiResponse(data)) {
throw new ApiError('Invalid response format from API', 500);
}
return data;
} catch (error) {
// Handle network errors
if (error instanceof ApiError) {
throw error;
}
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new ApiError('Request timeout - please try again', 408, error);
}
if (error.message.includes('fetch')) {
throw new ApiError(
'Unable to connect to the API. Please check your connection and try again.',
0,
error
);
}
throw new ApiError(
`Network error: ${error.message}`,
0,
error
);
}
throw new ApiError('An unexpected error occurred', 0, error);
}
};
/**
* Type guard to validate API response structure
*/
function isValidApiResponse(data: unknown): data is ApiResponse {
if (typeof data !== 'object' || data === null) {
return false;
}
const response = data as Record<string, unknown>;
return (
typeof response.response_type === 'string' &&
(response.response_type === 'answer' || response.response_type === 'escalation') &&
typeof response.message === 'string' &&
typeof response.confidence_explanation === 'string' &&
(response.sources === null || Array.isArray(response.sources))
);
}