| |
| |
| |
| |
| |
|
|
| const API = { |
| BASE_URL: 'http://localhost:8000', |
| API_PREFIX: '/api/v1', |
|
|
| |
| POLL_INTERVAL: 10000, |
| FAST_POLL_INTERVAL: 3000, |
|
|
| |
| _pollTimer: null, |
|
|
| |
| _currentAllocationRun: null, |
| _lastAgentTimeline: null, |
| |
| |
| _lastKnownRunId: null, |
| _lastPollTime: 0, |
| _lastRunCount: 0, |
| |
| |
| _selectedDate: null, |
| |
| |
| getSelectedDate() { |
| if (!this._selectedDate) { |
| this._selectedDate = this.getTodayDate(); |
| } |
| return this._selectedDate; |
| }, |
|
|
| |
| _callbacks: { |
| onStatusUpdate: null, |
| onWorkflowUpdate: null, |
| onError: null |
| }, |
|
|
| |
| |
| |
| setBaseUrl(url) { |
| this.BASE_URL = url; |
| }, |
|
|
| |
| |
| |
| async request(endpoint, options = {}) { |
| const url = `${this.BASE_URL}${this.API_PREFIX}${endpoint}`; |
|
|
| |
| const cacheBuster = options.method === 'GET' || !options.method |
| ? (endpoint.includes('?') ? '&' : '?') + `_t=${new Date().getTime()}` |
| : ''; |
|
|
| try { |
| const response = await fetch(url + cacheBuster, { |
| cache: 'no-store', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Pragma': 'no-cache', |
| 'Cache-Control': 'no-cache, no-store, must-revalidate', |
| ...options.headers |
| }, |
| ...options |
| }); |
|
|
| if (!response.ok) { |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| } |
|
|
| return await response.json(); |
| } catch (error) { |
| console.error(`API Error (${endpoint}):`, error); |
| if (this._callbacks.onError) { |
| this._callbacks.onError(error); |
| } |
| throw error; |
| } |
| }, |
|
|
| |
| |
| |
| async checkHealth() { |
| try { |
| const response = await fetch(`${this.BASE_URL}/health`); |
| return await response.json(); |
| } catch (error) { |
| return { status: 'disconnected', error: error.message }; |
| } |
| }, |
|
|
| |
| |
| |
| async getAdminHealth() { |
| try { |
| return await this.request('/admin/health'); |
| } catch (error) { |
| return null; |
| } |
| }, |
|
|
| |
| |
| |
| async getAllocationRuns(date) { |
| return this.request(`/admin/allocation_runs?date=${date}`); |
| }, |
|
|
| |
| |
| |
| async getAgentTimeline(allocationRunId) { |
| return this.request(`/admin/agent_timeline?allocation_run_id=${allocationRunId}`); |
| }, |
|
|
| |
| |
| |
| async getFairnessMetrics(startDate, endDate) { |
| return this.request(`/admin/metrics/fairness?start_date=${startDate}&end_date=${endDate}`); |
| }, |
|
|
| |
| |
| |
| async triggerAllocation(data) { |
| return this.request('/allocate', { |
| method: 'POST', |
| body: JSON.stringify(data) |
| }); |
| }, |
|
|
| |
| |
| |
| async getDriver(driverId) { |
| return this.request(`/drivers/${driverId}`); |
| }, |
|
|
| |
| |
| |
| async getRoute(routeId) { |
| return this.request(`/routes/${routeId}`); |
| }, |
|
|
| |
| |
| |
| async getAssignments(date) { |
| return this.request(`/admin/assignments?date=${date}`); |
| }, |
|
|
| |
| |
| |
| onStatusUpdate(callback) { |
| this._callbacks.onStatusUpdate = callback; |
| }, |
|
|
| |
| |
| |
| onWorkflowUpdate(callback) { |
| this._callbacks.onWorkflowUpdate = callback; |
| }, |
|
|
| |
| |
| |
| onError(callback) { |
| this._callbacks.onError = callback; |
| }, |
|
|
| |
| |
| |
| startPolling() { |
| if (this._pollTimer) { |
| clearInterval(this._pollTimer); |
| } |
|
|
| |
| this._pollTimer = setInterval(async () => { |
| await this._pollStatus(); |
| }, this.POLL_INTERVAL); |
|
|
| |
| this._pollStatus(); |
| }, |
|
|
| |
| |
| |
| stopPolling() { |
| if (this._pollTimer) { |
| clearInterval(this._pollTimer); |
| this._pollTimer = null; |
| } |
| }, |
| |
| |
| |
| |
| setSelectedDate(date) { |
| this._selectedDate = date; |
| |
| this._lastKnownRunId = null; |
| this._lastRunCount = 0; |
| }, |
|
|
| |
| |
| |
| |
| async _pollStatus() { |
| try { |
| const health = await this.checkHealth(); |
| const connected = health.status === 'healthy'; |
|
|
| if (this._callbacks.onStatusUpdate) { |
| this._callbacks.onStatusUpdate({ |
| connected, |
| health |
| }); |
| } |
|
|
| |
| if (connected && this._callbacks.onWorkflowUpdate) { |
| const queryDate = this.getSelectedDate(); |
| |
| |
| const runsResponse = await this.getAllocationRuns(queryDate); |
| const runs = runsResponse.runs || []; |
| const runCount = runs.length; |
| const latestRunId = runs.length > 0 ? runs[runs.length - 1].id : null; |
| |
| |
| |
| |
| |
| const needsFullFetch = !this._currentAllocationRun || |
| runCount !== this._lastRunCount || |
| latestRunId !== this._lastKnownRunId; |
| |
| if (needsFullFetch && runs.length > 0) { |
| console.log(`[Poll] New data detected, fetching full workflow state...`); |
| const workflowState = await this.getRealWorkflowState(queryDate); |
| this._lastRunCount = runCount; |
| this._callbacks.onWorkflowUpdate(workflowState); |
| } |
| |
| } |
| } catch (error) { |
| if (this._callbacks.onStatusUpdate) { |
| this._callbacks.onStatusUpdate({ |
| connected: false, |
| error: error.message |
| }); |
| } |
| } |
| }, |
|
|
| |
| |
| |
| getTodayDate() { |
| const now = new Date(); |
| const year = now.getFullYear(); |
| const month = String(now.getMonth() + 1).padStart(2, '0'); |
| const day = String(now.getDate()).padStart(2, '0'); |
| return `${year}-${month}-${day}`; |
| }, |
|
|
| |
| |
| |
| |
| |
| async getRealWorkflowState(date = null, runId = null) { |
| try { |
| |
| if (runId) { |
| return await this.getWorkflowStateForRun(runId); |
| } |
|
|
| |
| const queryDate = date || this.getSelectedDate(); |
| const runsResponse = await this.getAllocationRuns(queryDate); |
|
|
| if (runsResponse.runs && runsResponse.runs.length > 0) { |
| |
| const latestRun = runsResponse.runs[runsResponse.runs.length - 1]; |
| this._currentAllocationRun = latestRun; |
| |
| |
| this._lastKnownRunId = latestRun.id; |
|
|
| |
| const timeline = await this.getAgentTimeline(latestRun.id); |
| this._lastAgentTimeline = timeline; |
|
|
| |
| return this.transformTimelineToWorkflowState(timeline, latestRun); |
| } |
| } catch (error) { |
| console.log('Using mock data - no allocations found:', error.message); |
| } |
|
|
| |
| return this.getMockWorkflowState(); |
| }, |
|
|
| |
| |
| |
| |
| async getWorkflowStateForRun(runId) { |
| try { |
| |
| const summaryResponse = await fetch(`${this.BASE_URL}${this.API_PREFIX}/runs/${runId}/summary`); |
| if (!summaryResponse.ok) { |
| throw new Error(`Failed to fetch run summary: ${summaryResponse.status}`); |
| } |
| const summary = await summaryResponse.json(); |
|
|
| |
| const allocationRun = { |
| id: summary.allocation_run_id, |
| date: summary.date, |
| num_drivers: summary.num_drivers, |
| num_routes: summary.num_routes, |
| num_packages: summary.num_packages, |
| gini_index: summary.global_gini_index, |
| std_dev: summary.global_std_dev, |
| status: summary.status, |
| }; |
| this._currentAllocationRun = allocationRun; |
| this._lastKnownRunId = runId; |
|
|
| |
| const timeline = await this.getAgentTimeline(runId); |
| this._lastAgentTimeline = timeline; |
|
|
| |
| return this.transformTimelineToWorkflowState(timeline, allocationRun); |
| } catch (error) { |
| console.error('Error fetching workflow state for run:', runId, error); |
| throw error; |
| } |
| }, |
|
|
| |
| |
| |
| transformTimelineToWorkflowState(timeline, allocationRun) { |
| |
| |
| const agentNameMap = { |
| |
| 'ML_EFFORT': { id: 'route_planner', name: 'Route Planner Agent', type: 'agent' }, |
| 'ML_EFFORT_AGENT': { id: 'route_planner', name: 'Route Planner Agent', type: 'agent' }, |
| 'ROUTE_PLANNER': { id: 'route_planner', name: 'Route Planner Agent', type: 'agent' }, |
| 'FAIRNESS_MANAGER': { id: 'fairness_manager', name: 'Fairness Manager Agent', type: 'agent' }, |
| 'DRIVER_LIAISON': { id: 'driver_liaison', name: 'Driver Liaison Agent', type: 'agent' }, |
| 'EXPLAINABILITY': { id: 'explainability', name: 'Explainability Agent', type: 'agent' }, |
| 'LEARNING': { id: 'learning', name: 'Learning Agent', type: 'agent' }, |
| |
| 'MLEffortAgent': { id: 'route_planner', name: 'Route Planner Agent', type: 'agent' }, |
| 'RoutePlannerAgent': { id: 'route_planner', name: 'Route Planner Agent', type: 'agent' }, |
| 'FairnessManagerAgent': { id: 'fairness_manager', name: 'Fairness Manager Agent', type: 'agent' }, |
| 'DriverLiaisonAgent': { id: 'driver_liaison', name: 'Driver Liaison Agent', type: 'agent' }, |
| 'ExplainabilityAgent': { id: 'explainability', name: 'Explainability Agent', type: 'agent' }, |
| 'LearningAgent': { id: 'learning', name: 'Learning Agent', type: 'agent' }, |
| }; |
|
|
|
|
| |
| |
| const agentStatus = {}; |
| const agentLogs = {}; |
| let totalSteps = 0; |
|
|
| |
| const steps = timeline.timeline || timeline.steps || []; |
|
|
| if (steps && steps.length > 0) { |
| steps.forEach(step => { |
| const mapped = agentNameMap[step.agent_name]; |
| if (mapped) { |
| agentStatus[mapped.id] = 'active'; |
| if (!agentLogs[mapped.id]) { |
| agentLogs[mapped.id] = []; |
| } |
| agentLogs[mapped.id].push(step); |
| totalSteps++; |
| } |
| }); |
| } |
|
|
| |
| this._agentLogs = agentLogs; |
|
|
| const agents = [ |
| { |
| id: 'central_orchestrator', |
| name: 'Central Orchestrator Agent', |
| description: 'Central Intelligence Hub', |
| status: 'active', |
| type: 'orchestrator', |
| meta: `${Object.keys(agentStatus).length} Active` |
| }, |
| { |
| id: 'route_database', |
| name: 'Route Database', |
| description: `Run: ${allocationRun.id.substring(0, 8)}...`, |
| status: 'active', |
| type: 'database', |
| meta: 'Connected' |
| }, |
| { |
| id: 'route_planner', |
| name: 'Route Planner Agent', |
| description: agentLogs['route_planner'] ? |
| `${agentLogs['route_planner'].length} decisions` : 'Optimizing routes', |
| status: agentStatus['route_planner'] || 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'fairness_manager', |
| name: 'Fairness Manager Agent', |
| description: agentLogs['fairness_manager'] ? |
| `Gini: ${allocationRun.gini_index?.toFixed(3) || 'N/A'}` : 'Ensuring equitable distribution', |
| status: agentStatus['fairness_manager'] || 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'driver_liaison', |
| name: 'Driver Liaison Agent', |
| description: agentLogs['driver_liaison'] ? |
| `${agentLogs['driver_liaison'].length} interactions` : 'Driver communication hub', |
| status: agentStatus['driver_liaison'] || 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'explainability', |
| name: 'Explainability Agent', |
| description: agentLogs['explainability'] ? |
| `${agentLogs['explainability'].length} explanations` : 'Generating explanations', |
| status: agentStatus['explainability'] || 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'learning', |
| name: 'Learning Agent', |
| description: 'Continuous improvement loop', |
| status: agentStatus['learning'] || 'idle', |
| type: 'agent', |
| meta: null |
| } |
| ]; |
|
|
| |
| const connections = [ |
| { from: 'central_orchestrator', to: 'route_database', active: true }, |
| { from: 'central_orchestrator', to: 'route_planner', active: !!agentStatus['route_planner'] }, |
| { from: 'central_orchestrator', to: 'fairness_manager', active: !!agentStatus['fairness_manager'] }, |
| { from: 'central_orchestrator', to: 'driver_liaison', active: !!agentStatus['driver_liaison'] }, |
| { from: 'central_orchestrator', to: 'explainability', active: !!agentStatus['explainability'] }, |
| { from: 'central_orchestrator', to: 'learning', active: !!agentStatus['learning'] }, |
| { from: 'route_database', to: 'route_planner', active: !!agentStatus['route_planner'] }, |
| { from: 'route_planner', to: 'fairness_manager', active: !!agentStatus['fairness_manager'] }, |
| { from: 'fairness_manager', to: 'driver_liaison', active: !!agentStatus['driver_liaison'] }, |
| { from: 'driver_liaison', to: 'explainability', active: !!agentStatus['explainability'] }, |
| { from: 'explainability', to: 'learning', active: !!agentStatus['learning'] } |
| ]; |
|
|
| const activeConnections = connections.filter(c => c.active).length; |
|
|
| return { |
| agents, |
| connections, |
| stats: { |
| processing: 0, |
| dataFlows: activeConnections, |
| totalAgents: Object.keys(agentStatus).length |
| }, |
| allocationRun, |
| timeline, |
| isRealData: true |
| }; |
| }, |
|
|
| |
| |
| |
| getAgentLogs(agentId) { |
| if (this._agentLogs && this._agentLogs[agentId]) { |
| return this._agentLogs[agentId]; |
| } |
| return null; |
| }, |
|
|
| |
| |
| |
| getCurrentAllocationRun() { |
| return this._currentAllocationRun; |
| }, |
|
|
| |
| |
| |
| getMockWorkflowState() { |
| return { |
| agents: [ |
| { |
| id: 'central_orchestrator', |
| name: 'Central Orchestrator Agent', |
| description: 'Central Intelligence Hub', |
| status: 'active', |
| type: 'orchestrator', |
| meta: '6 Agents' |
| }, |
| { |
| id: 'route_database', |
| name: 'Route Database', |
| description: '@ 18.9K records', |
| status: 'active', |
| type: 'database', |
| meta: 'Connected' |
| }, |
| { |
| id: 'route_planner', |
| name: 'Route Planner Agent', |
| description: 'Optimizing delivery routes', |
| status: 'processing', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'fairness_manager', |
| name: 'Fairness Manager Agent', |
| description: 'Ensuring equitable distribution', |
| status: 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'driver_liaison', |
| name: 'Driver Liaison Agent', |
| description: 'Driver communication hub', |
| status: 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'explainability', |
| name: 'Explainability Agent', |
| description: 'Generating detailed explanations', |
| status: 'idle', |
| type: 'agent', |
| meta: null |
| }, |
| { |
| id: 'learning', |
| name: 'Learning Agent', |
| description: 'Continuous improvement loop', |
| status: 'idle', |
| type: 'agent', |
| meta: null |
| } |
| ], |
| connections: [ |
| { from: 'central_orchestrator', to: 'route_database', active: true }, |
| { from: 'central_orchestrator', to: 'route_planner', active: true }, |
| { from: 'central_orchestrator', to: 'fairness_manager', active: true }, |
| { from: 'central_orchestrator', to: 'driver_liaison', active: true }, |
| { from: 'central_orchestrator', to: 'explainability', active: true }, |
| { from: 'central_orchestrator', to: 'learning', active: true }, |
| { from: 'route_database', to: 'route_planner', active: false }, |
| { from: 'route_planner', to: 'fairness_manager', active: false }, |
| { from: 'fairness_manager', to: 'driver_liaison', active: false }, |
| { from: 'driver_liaison', to: 'explainability', active: false }, |
| { from: 'explainability', to: 'learning', active: false } |
| ], |
| stats: { |
| processing: 1, |
| dataFlows: 6, |
| totalAgents: 6 |
| }, |
| isRealData: false |
| }; |
| } |
| }; |
|
|
| |
| window.API = API; |
|
|