Add comprehensive analytics system with: - Analytics data layer (aggregator, metrics, trends, cache) - 6 API endpoints (overview, plants, transport, farms, sustainability, export) - 6 chart components (LineChart, BarChart, PieChart, AreaChart, Gauge, Heatmap) - 5 dashboard widgets (KPICard, TrendIndicator, DataTable, DateRangePicker, FilterPanel) - 5 dashboard pages (overview, plants, transport, farms, sustainability) - Export functionality (CSV, JSON) Dependencies added: recharts, d3, date-fns Also includes minor fixes: - Fix EnvironmentalForm spread type error - Fix AgentOrchestrator Map iteration issues - Fix next.config.js image domains undefined error - Add downlevelIteration to tsconfig
568 lines
16 KiB
TypeScript
568 lines
16 KiB
TypeScript
/**
|
|
* AgentOrchestrator
|
|
* Central management system for all LocalGreenChain agents
|
|
*
|
|
* Responsibilities:
|
|
* - Start/stop all agents
|
|
* - Coordinate inter-agent communication
|
|
* - Aggregate metrics and alerts
|
|
* - Handle agent failures and restarts
|
|
* - Provide unified status dashboard
|
|
*/
|
|
|
|
import { BaseAgent } from './BaseAgent';
|
|
import { AgentMetrics, AgentAlert, AgentConfig, AgentStatus } from './types';
|
|
|
|
import { getPlantLineageAgent, PlantLineageAgent } from './PlantLineageAgent';
|
|
import { getTransportTrackerAgent, TransportTrackerAgent } from './TransportTrackerAgent';
|
|
import { getDemandForecastAgent, DemandForecastAgent } from './DemandForecastAgent';
|
|
import { getVerticalFarmAgent, VerticalFarmAgent } from './VerticalFarmAgent';
|
|
import { getEnvironmentAnalysisAgent, EnvironmentAnalysisAgent } from './EnvironmentAnalysisAgent';
|
|
import { getMarketMatchingAgent, MarketMatchingAgent } from './MarketMatchingAgent';
|
|
import { getSustainabilityAgent, SustainabilityAgent } from './SustainabilityAgent';
|
|
import { getNetworkDiscoveryAgent, NetworkDiscoveryAgent } from './NetworkDiscoveryAgent';
|
|
import { getQualityAssuranceAgent, QualityAssuranceAgent } from './QualityAssuranceAgent';
|
|
import { getGrowerAdvisoryAgent, GrowerAdvisoryAgent } from './GrowerAdvisoryAgent';
|
|
|
|
interface OrchestratorConfig {
|
|
autoStart: boolean;
|
|
enabledAgents: string[];
|
|
alertAggregationIntervalMs: number;
|
|
healthCheckIntervalMs: number;
|
|
maxAlertsPerAgent: number;
|
|
}
|
|
|
|
interface AgentHealth {
|
|
agentId: string;
|
|
agentName: string;
|
|
status: AgentStatus;
|
|
lastRunAt: string | null;
|
|
tasksCompleted: number;
|
|
tasksFailed: number;
|
|
errorRate: number;
|
|
avgExecutionMs: number;
|
|
isHealthy: boolean;
|
|
}
|
|
|
|
interface OrchestratorStatus {
|
|
isRunning: boolean;
|
|
startedAt: string | null;
|
|
uptime: number;
|
|
totalAgents: number;
|
|
runningAgents: number;
|
|
healthyAgents: number;
|
|
totalTasksCompleted: number;
|
|
totalTasksFailed: number;
|
|
activeAlerts: number;
|
|
}
|
|
|
|
interface AgentSummary {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
status: AgentStatus;
|
|
priority: string;
|
|
intervalMs: number;
|
|
metrics: AgentMetrics;
|
|
alertCount: number;
|
|
}
|
|
|
|
export class AgentOrchestrator {
|
|
private static instance: AgentOrchestrator;
|
|
|
|
private config: OrchestratorConfig;
|
|
private agents: Map<string, BaseAgent> = new Map();
|
|
private isRunning: boolean = false;
|
|
private startedAt: string | null = null;
|
|
private healthCheckInterval: NodeJS.Timeout | null = null;
|
|
private alertAggregationInterval: NodeJS.Timeout | null = null;
|
|
private aggregatedAlerts: AgentAlert[] = [];
|
|
private eventHandlers: Map<string, ((event: any) => void)[]> = new Map();
|
|
|
|
private constructor(config?: Partial<OrchestratorConfig>) {
|
|
this.config = {
|
|
autoStart: false,
|
|
enabledAgents: [
|
|
'plant-lineage-agent',
|
|
'transport-tracker-agent',
|
|
'demand-forecast-agent',
|
|
'vertical-farm-agent',
|
|
'environment-analysis-agent',
|
|
'market-matching-agent',
|
|
'sustainability-agent',
|
|
'network-discovery-agent',
|
|
'quality-assurance-agent',
|
|
'grower-advisory-agent'
|
|
],
|
|
alertAggregationIntervalMs: 60000,
|
|
healthCheckIntervalMs: 30000,
|
|
maxAlertsPerAgent: 50,
|
|
...config
|
|
};
|
|
|
|
this.initializeAgents();
|
|
}
|
|
|
|
public static getInstance(config?: Partial<OrchestratorConfig>): AgentOrchestrator {
|
|
if (!AgentOrchestrator.instance) {
|
|
AgentOrchestrator.instance = new AgentOrchestrator(config);
|
|
}
|
|
return AgentOrchestrator.instance;
|
|
}
|
|
|
|
/**
|
|
* Initialize all agents
|
|
*/
|
|
private initializeAgents(): void {
|
|
const agentFactories: Record<string, () => BaseAgent> = {
|
|
'plant-lineage-agent': getPlantLineageAgent,
|
|
'transport-tracker-agent': getTransportTrackerAgent,
|
|
'demand-forecast-agent': getDemandForecastAgent,
|
|
'vertical-farm-agent': getVerticalFarmAgent,
|
|
'environment-analysis-agent': getEnvironmentAnalysisAgent,
|
|
'market-matching-agent': getMarketMatchingAgent,
|
|
'sustainability-agent': getSustainabilityAgent,
|
|
'network-discovery-agent': getNetworkDiscoveryAgent,
|
|
'quality-assurance-agent': getQualityAssuranceAgent,
|
|
'grower-advisory-agent': getGrowerAdvisoryAgent
|
|
};
|
|
|
|
for (const agentId of this.config.enabledAgents) {
|
|
const factory = agentFactories[agentId];
|
|
if (factory) {
|
|
const agent = factory();
|
|
this.agents.set(agentId, agent);
|
|
console.log(`[Orchestrator] Registered agent: ${agent.config.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start all agents
|
|
*/
|
|
async startAll(): Promise<void> {
|
|
if (this.isRunning) {
|
|
console.log('[Orchestrator] Already running');
|
|
return;
|
|
}
|
|
|
|
console.log('[Orchestrator] Starting all agents...');
|
|
this.isRunning = true;
|
|
this.startedAt = new Date().toISOString();
|
|
|
|
// Start agents in priority order
|
|
const sortedAgents = this.getSortedAgentsByPriority();
|
|
|
|
for (const agent of sortedAgents) {
|
|
if (agent.config.enabled) {
|
|
try {
|
|
await agent.start();
|
|
console.log(`[Orchestrator] Started: ${agent.config.name}`);
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Failed to start ${agent.config.name}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start health check interval
|
|
this.healthCheckInterval = setInterval(() => {
|
|
this.performHealthCheck();
|
|
}, this.config.healthCheckIntervalMs);
|
|
|
|
// Start alert aggregation interval
|
|
this.alertAggregationInterval = setInterval(() => {
|
|
this.aggregateAlerts();
|
|
}, this.config.alertAggregationIntervalMs);
|
|
|
|
this.emit('orchestrator_started', { agentCount: this.agents.size });
|
|
console.log(`[Orchestrator] All agents started (${this.agents.size} total)`);
|
|
}
|
|
|
|
/**
|
|
* Stop all agents
|
|
*/
|
|
async stopAll(): Promise<void> {
|
|
if (!this.isRunning) {
|
|
console.log('[Orchestrator] Not running');
|
|
return;
|
|
}
|
|
|
|
console.log('[Orchestrator] Stopping all agents...');
|
|
|
|
// Clear intervals
|
|
if (this.healthCheckInterval) {
|
|
clearInterval(this.healthCheckInterval);
|
|
this.healthCheckInterval = null;
|
|
}
|
|
|
|
if (this.alertAggregationInterval) {
|
|
clearInterval(this.alertAggregationInterval);
|
|
this.alertAggregationInterval = null;
|
|
}
|
|
|
|
// Stop all agents
|
|
for (const agent of Array.from(this.agents.values())) {
|
|
try {
|
|
await agent.stop();
|
|
console.log(`[Orchestrator] Stopped: ${agent.config.name}`);
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Error stopping ${agent.config.name}:`, error);
|
|
}
|
|
}
|
|
|
|
this.isRunning = false;
|
|
this.emit('orchestrator_stopped', {});
|
|
console.log('[Orchestrator] All agents stopped');
|
|
}
|
|
|
|
/**
|
|
* Start a specific agent
|
|
*/
|
|
async startAgent(agentId: string): Promise<boolean> {
|
|
const agent = this.agents.get(agentId);
|
|
if (!agent) {
|
|
console.error(`[Orchestrator] Agent not found: ${agentId}`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await agent.start();
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Failed to start ${agentId}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop a specific agent
|
|
*/
|
|
async stopAgent(agentId: string): Promise<boolean> {
|
|
const agent = this.agents.get(agentId);
|
|
if (!agent) {
|
|
console.error(`[Orchestrator] Agent not found: ${agentId}`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await agent.stop();
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Failed to stop ${agentId}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart a specific agent
|
|
*/
|
|
async restartAgent(agentId: string): Promise<boolean> {
|
|
await this.stopAgent(agentId);
|
|
return this.startAgent(agentId);
|
|
}
|
|
|
|
/**
|
|
* Get sorted agents by priority
|
|
*/
|
|
private getSortedAgentsByPriority(): BaseAgent[] {
|
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
return Array.from(this.agents.values()).sort((a, b) =>
|
|
priorityOrder[a.config.priority] - priorityOrder[b.config.priority]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Perform health check on all agents
|
|
*/
|
|
private performHealthCheck(): void {
|
|
for (const [agentId, agent] of Array.from(this.agents.entries())) {
|
|
const health = this.getAgentHealth(agentId);
|
|
|
|
if (!health.isHealthy) {
|
|
console.warn(`[Orchestrator] Unhealthy agent detected: ${agent.config.name}`);
|
|
|
|
// Auto-restart if error rate is high
|
|
if (health.errorRate > 50 && agent.status === 'running') {
|
|
console.log(`[Orchestrator] Auto-restarting: ${agent.config.name}`);
|
|
this.restartAgent(agentId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aggregate alerts from all agents
|
|
*/
|
|
private aggregateAlerts(): void {
|
|
this.aggregatedAlerts = [];
|
|
|
|
for (const agent of Array.from(this.agents.values())) {
|
|
const alerts = agent.getAlerts()
|
|
.filter(a => !a.acknowledged)
|
|
.slice(-this.config.maxAlertsPerAgent);
|
|
|
|
this.aggregatedAlerts.push(...alerts);
|
|
}
|
|
|
|
// Sort by severity and timestamp
|
|
const severityOrder = { critical: 0, error: 1, warning: 2, info: 3 };
|
|
this.aggregatedAlerts.sort((a, b) => {
|
|
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
if (severityDiff !== 0) return severityDiff;
|
|
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
|
|
});
|
|
|
|
// Emit event if critical alerts
|
|
const criticalAlerts = this.aggregatedAlerts.filter(a => a.severity === 'critical');
|
|
if (criticalAlerts.length > 0) {
|
|
this.emit('critical_alerts', { count: criticalAlerts.length, alerts: criticalAlerts });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get agent health
|
|
*/
|
|
getAgentHealth(agentId: string): AgentHealth {
|
|
const agent = this.agents.get(agentId);
|
|
if (!agent) {
|
|
return {
|
|
agentId,
|
|
agentName: 'Unknown',
|
|
status: 'idle',
|
|
lastRunAt: null,
|
|
tasksCompleted: 0,
|
|
tasksFailed: 0,
|
|
errorRate: 0,
|
|
avgExecutionMs: 0,
|
|
isHealthy: false
|
|
};
|
|
}
|
|
|
|
const metrics = agent.getMetrics();
|
|
const totalTasks = metrics.tasksCompleted + metrics.tasksFailed;
|
|
const errorRate = totalTasks > 0
|
|
? (metrics.tasksFailed / totalTasks) * 100
|
|
: 0;
|
|
|
|
// Check if agent is responding (last run within 2x interval)
|
|
const lastRunTime = metrics.lastRunAt ? new Date(metrics.lastRunAt).getTime() : 0;
|
|
const maxTimeSinceRun = agent.config.intervalMs * 2;
|
|
const isResponding = agent.status !== 'running' ||
|
|
(Date.now() - lastRunTime < maxTimeSinceRun);
|
|
|
|
const isHealthy = agent.status !== 'error' &&
|
|
errorRate < 30 &&
|
|
isResponding;
|
|
|
|
return {
|
|
agentId,
|
|
agentName: agent.config.name,
|
|
status: agent.status,
|
|
lastRunAt: metrics.lastRunAt,
|
|
tasksCompleted: metrics.tasksCompleted,
|
|
tasksFailed: metrics.tasksFailed,
|
|
errorRate: Math.round(errorRate),
|
|
avgExecutionMs: Math.round(metrics.averageExecutionMs),
|
|
isHealthy
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get orchestrator status
|
|
*/
|
|
getStatus(): OrchestratorStatus {
|
|
const healthStatuses = Array.from(this.agents.keys()).map(id => this.getAgentHealth(id));
|
|
const runningAgents = healthStatuses.filter(h => h.status === 'running').length;
|
|
const healthyAgents = healthStatuses.filter(h => h.isHealthy).length;
|
|
const totalCompleted = healthStatuses.reduce((sum, h) => sum + h.tasksCompleted, 0);
|
|
const totalFailed = healthStatuses.reduce((sum, h) => sum + h.tasksFailed, 0);
|
|
|
|
return {
|
|
isRunning: this.isRunning,
|
|
startedAt: this.startedAt,
|
|
uptime: this.startedAt
|
|
? Date.now() - new Date(this.startedAt).getTime()
|
|
: 0,
|
|
totalAgents: this.agents.size,
|
|
runningAgents,
|
|
healthyAgents,
|
|
totalTasksCompleted: totalCompleted,
|
|
totalTasksFailed: totalFailed,
|
|
activeAlerts: this.aggregatedAlerts.filter(a => !a.acknowledged).length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all agent summaries
|
|
*/
|
|
getAgentSummaries(): AgentSummary[] {
|
|
return Array.from(this.agents.values()).map(agent => ({
|
|
id: agent.config.id,
|
|
name: agent.config.name,
|
|
description: agent.config.description,
|
|
status: agent.status,
|
|
priority: agent.config.priority,
|
|
intervalMs: agent.config.intervalMs,
|
|
metrics: agent.getMetrics(),
|
|
alertCount: agent.getAlerts().filter(a => !a.acknowledged).length
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get all alerts
|
|
*/
|
|
getAlerts(severity?: string): AgentAlert[] {
|
|
if (severity) {
|
|
return this.aggregatedAlerts.filter(a => a.severity === severity);
|
|
}
|
|
return this.aggregatedAlerts;
|
|
}
|
|
|
|
/**
|
|
* Acknowledge an alert
|
|
*/
|
|
acknowledgeAlert(alertId: string): boolean {
|
|
const alert = this.aggregatedAlerts.find(a => a.id === alertId);
|
|
if (alert) {
|
|
alert.acknowledged = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get specific agent
|
|
*/
|
|
getAgent<T extends BaseAgent>(agentId: string): T | null {
|
|
return this.agents.get(agentId) as T || null;
|
|
}
|
|
|
|
/**
|
|
* Get typed agent instances
|
|
*/
|
|
getPlantLineageAgent(): PlantLineageAgent | null {
|
|
return this.getAgent<PlantLineageAgent>('plant-lineage-agent');
|
|
}
|
|
|
|
getTransportTrackerAgent(): TransportTrackerAgent | null {
|
|
return this.getAgent<TransportTrackerAgent>('transport-tracker-agent');
|
|
}
|
|
|
|
getDemandForecastAgent(): DemandForecastAgent | null {
|
|
return this.getAgent<DemandForecastAgent>('demand-forecast-agent');
|
|
}
|
|
|
|
getVerticalFarmAgent(): VerticalFarmAgent | null {
|
|
return this.getAgent<VerticalFarmAgent>('vertical-farm-agent');
|
|
}
|
|
|
|
getEnvironmentAnalysisAgent(): EnvironmentAnalysisAgent | null {
|
|
return this.getAgent<EnvironmentAnalysisAgent>('environment-analysis-agent');
|
|
}
|
|
|
|
getMarketMatchingAgent(): MarketMatchingAgent | null {
|
|
return this.getAgent<MarketMatchingAgent>('market-matching-agent');
|
|
}
|
|
|
|
getSustainabilityAgent(): SustainabilityAgent | null {
|
|
return this.getAgent<SustainabilityAgent>('sustainability-agent');
|
|
}
|
|
|
|
getNetworkDiscoveryAgent(): NetworkDiscoveryAgent | null {
|
|
return this.getAgent<NetworkDiscoveryAgent>('network-discovery-agent');
|
|
}
|
|
|
|
getQualityAssuranceAgent(): QualityAssuranceAgent | null {
|
|
return this.getAgent<QualityAssuranceAgent>('quality-assurance-agent');
|
|
}
|
|
|
|
getGrowerAdvisoryAgent(): GrowerAdvisoryAgent | null {
|
|
return this.getAgent<GrowerAdvisoryAgent>('grower-advisory-agent');
|
|
}
|
|
|
|
/**
|
|
* Register event handler
|
|
*/
|
|
on(event: string, handler: (data: any) => void): void {
|
|
const handlers = this.eventHandlers.get(event) || [];
|
|
handlers.push(handler);
|
|
this.eventHandlers.set(event, handlers);
|
|
}
|
|
|
|
/**
|
|
* Emit event
|
|
*/
|
|
private emit(event: string, data: any): void {
|
|
const handlers = this.eventHandlers.get(event) || [];
|
|
for (const handler of handlers) {
|
|
try {
|
|
handler(data);
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Event handler error for ${event}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get dashboard data
|
|
*/
|
|
getDashboard(): {
|
|
status: OrchestratorStatus;
|
|
agents: AgentSummary[];
|
|
health: AgentHealth[];
|
|
recentAlerts: AgentAlert[];
|
|
metrics: {
|
|
totalTasksToday: number;
|
|
avgErrorRate: number;
|
|
avgExecutionTime: number;
|
|
};
|
|
} {
|
|
const status = this.getStatus();
|
|
const agents = this.getAgentSummaries();
|
|
const health = Array.from(this.agents.keys()).map(id => this.getAgentHealth(id));
|
|
const recentAlerts = this.aggregatedAlerts.slice(0, 10);
|
|
|
|
const avgErrorRate = health.length > 0
|
|
? health.reduce((sum, h) => sum + h.errorRate, 0) / health.length
|
|
: 0;
|
|
|
|
const avgExecutionTime = health.length > 0
|
|
? health.reduce((sum, h) => sum + h.avgExecutionMs, 0) / health.length
|
|
: 0;
|
|
|
|
return {
|
|
status,
|
|
agents,
|
|
health,
|
|
recentAlerts,
|
|
metrics: {
|
|
totalTasksToday: status.totalTasksCompleted,
|
|
avgErrorRate: Math.round(avgErrorRate),
|
|
avgExecutionTime: Math.round(avgExecutionTime)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get orchestrator singleton
|
|
*/
|
|
export function getOrchestrator(config?: Partial<OrchestratorConfig>): AgentOrchestrator {
|
|
return AgentOrchestrator.getInstance(config);
|
|
}
|
|
|
|
/**
|
|
* Start all agents
|
|
*/
|
|
export async function startAllAgents(): Promise<void> {
|
|
const orchestrator = getOrchestrator();
|
|
await orchestrator.startAll();
|
|
}
|
|
|
|
/**
|
|
* Stop all agents
|
|
*/
|
|
export async function stopAllAgents(): Promise<void> {
|
|
const orchestrator = getOrchestrator();
|
|
await orchestrator.stopAll();
|
|
}
|