import type { Episode, Observation, Action, Reward, Agent, MemoryState, MCPTool, SystemSettings, APIResponse, StepRequest, ResetRequest, EpisodeStats, } from '@/types'; const API_BASE = '/api'; class APIError extends Error { constructor( message: string, public status: number, public data?: unknown ) { super(message); this.name = 'APIError'; } } async function request( endpoint: string, options: RequestInit = {} ): Promise { const url = `${API_BASE}${endpoint}`; const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, }; const response = await fetch(url, { ...options, headers, }); const data = await response.json() as APIResponse; if (!response.ok) { throw new APIError( data.error ?? 'An error occurred', response.status, data ); } if (!data.success) { throw new APIError(data.error ?? 'Request failed', response.status, data); } return data.data as T; } // Scraping types export interface ScrapeRequest { assets: string[]; instructions: string; output_instructions: string; output_format: 'json' | 'csv' | 'markdown' | 'text'; complexity: 'low' | 'medium' | 'high'; model: string; provider: string; enable_memory: boolean; enable_plugins: string[]; selected_agents: string[]; max_steps: number; python_code?: string; } export interface ScrapeStep { step_number: number; action: string; url: string | null; status: string; message: string; reward: number; extracted_data: Record | null; duration_ms: number | null; timestamp: string; } export interface ScrapeResponse { session_id: string; status: string; total_steps: number; total_reward: number; extracted_data: Record; output: string; output_format: string; duration_seconds: number; urls_processed: number; errors: string[]; selected_agents?: string[]; sandbox_artifacts?: string[]; } export interface StreamEvent { type: 'init' | 'url_start' | 'step' | 'url_complete' | 'complete' | 'error'; session_id?: string; url?: string; index?: number; total?: number; data?: ScrapeStep | ScrapeResponse | { url: string; error: string }; } export const apiClient = { // Episode Management async resetEpisode(params: ResetRequest): Promise { return request('/episode/reset', { method: 'POST', body: JSON.stringify(params), }); }, async getEpisode(episodeId: string): Promise { return request(`/episode/${episodeId}`); }, async getCurrentEpisode(): Promise { try { return await request('/episode/current'); } catch (error) { if (error instanceof APIError && error.status === 404) { return null; } throw error; } }, async stepEpisode(episodeId: string, step: StepRequest): Promise<{ observation: Observation; reward: Reward; done: boolean; info: Record; }> { return request(`/episode/${episodeId}/step`, { method: 'POST', body: JSON.stringify(step), }); }, async terminateEpisode(episodeId: string): Promise { return request(`/episode/${episodeId}/terminate`, { method: 'POST', }); }, // State Queries async getState(episodeId: string): Promise<{ observation: Observation; agents: Agent[]; memory: MemoryState; }> { return request(`/episode/${episodeId}/state`); }, async getObservation(episodeId: string, step?: number): Promise { const query = step !== undefined ? `?step=${step}` : ''; return request(`/episode/${episodeId}/observation${query}`); }, async getActions(episodeId: string): Promise { return request(`/episode/${episodeId}/actions`); }, async getRewards(episodeId: string): Promise { return request(`/episode/${episodeId}/rewards`); }, // Agent Management async getAgents(episodeId: string): Promise { return request(`/episode/${episodeId}/agents`); }, async getAgent(episodeId: string, agentId: string): Promise { return request(`/episode/${episodeId}/agents/${agentId}`); }, async updateAgent( episodeId: string, agentId: string, updates: Partial ): Promise { return request(`/episode/${episodeId}/agents/${agentId}`, { method: 'PATCH', body: JSON.stringify(updates), }); }, // Memory Operations async getMemory(episodeId: string): Promise { return request(`/episode/${episodeId}/memory`); }, async queryMemory( episodeId: string, query: string, layer?: string, limit?: number ): Promise { const params = new URLSearchParams({ query }); if (layer) params.set('layer', layer); if (limit) params.set('limit', limit.toString()); return request(`/episode/${episodeId}/memory/query?${params}`); }, async addMemory( episodeId: string, entry: Omit ): Promise { return request(`/episode/${episodeId}/memory`, { method: 'POST', body: JSON.stringify(entry), }); }, async clearMemory(episodeId: string, layer?: string): Promise { const query = layer ? `?layer=${layer}` : ''; return request(`/episode/${episodeId}/memory${query}`, { method: 'DELETE', }); }, // Tools async getTools(): Promise { return request('/tools'); }, async getTool(name: string): Promise { return request(`/tools/${name}`); }, async executeTool( name: string, parameters: Record ): Promise { return request(`/tools/${name}/execute`, { method: 'POST', body: JSON.stringify(parameters), }); }, async toggleTool(name: string, enabled: boolean): Promise { return request(`/tools/${name}`, { method: 'PATCH', body: JSON.stringify({ enabled }), }); }, // Settings async getSettings(): Promise { return request('/settings'); }, async updateSettings(settings: Partial): Promise { return request('/settings', { method: 'PATCH', body: JSON.stringify(settings), }); }, // Stats async getStats(): Promise { return request('/stats'); }, // Health Check async healthCheck(): Promise<{ status: string; version: string }> { const response = await fetch(`${API_BASE}/health`, { cache: 'no-store' }); if (!response.ok) { throw new APIError('Health check failed', response.status); } const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { return response.json(); } const text = await response.text(); try { return JSON.parse(text) as { status: string; version: string }; } catch { return { status: 'healthy', version: 'unknown' }; } }, // Scraping with streaming streamScrape( scrapeRequest: ScrapeRequest, onInit?: (sessionId: string) => void, onUrlStart?: (url: string, index: number, total: number) => void, onStep?: (step: ScrapeStep) => void, onUrlComplete?: (url: string, index: number) => void, onComplete?: (response: ScrapeResponse) => void, onError?: (error: string, url?: string) => void ): { abort: () => void } { const abortController = new AbortController(); fetch(`${API_BASE}/scrape/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(scrapeRequest), signal: abortController.signal, }) .then(async (response) => { if (!response.ok) { const errorData = await response.json().catch(() => ({})); onError?.(errorData.detail || 'Stream failed'); return; } const reader = response.body?.getReader(); if (!reader) { onError?.('No response body'); return; } const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { try { const event: StreamEvent = JSON.parse(line.slice(6)); switch (event.type) { case 'init': onInit?.(event.session_id!); break; case 'url_start': onUrlStart?.(event.url!, event.index!, event.total!); break; case 'step': onStep?.(event.data as ScrapeStep); break; case 'url_complete': onUrlComplete?.(event.url!, event.index!); break; case 'complete': onComplete?.(event.data as ScrapeResponse); break; case 'error': const errData = event.data as { url: string; error: string }; onError?.(errData.error, errData.url); break; } } catch { // Ignore parse errors } } } } }) .catch((err) => { if (err.name !== 'AbortError') { onError?.(err.message || 'Stream failed'); } }); return { abort: () => abortController.abort() }; }, // Get scrape session status async getScrapeStatus(sessionId: string): Promise<{ session_id: string; status: string; current_url_index: number; total_urls: number; total_reward: number; extracted_count: number; errors: string[]; duration: number; }> { const response = await fetch(`${API_BASE}/scrape/${sessionId}/status`); if (!response.ok) { throw new APIError('Failed to get scrape status', response.status); } return response.json(); }, // Get scrape result async getScrapeResult(sessionId: string): Promise { const response = await fetch(`${API_BASE}/scrape/${sessionId}/result`); if (!response.ok) { throw new APIError('Failed to get scrape result', response.status); } return response.json(); }, }; export { APIError }; export default apiClient;