/** * NetworkDiscoveryAgent * Analyzes geographic distribution and connections in the plant network * * Responsibilities: * - Map plant distribution across regions * - Identify network hotspots and clusters * - Suggest grower/consumer connections * - Track network growth patterns * - Detect coverage gaps */ import { BaseAgent } from './BaseAgent'; import { AgentConfig, AgentTask, NetworkAnalysis } from './types'; import { getBlockchain } from '../blockchain/manager'; import { PlantBlock } from '../blockchain/types'; interface NetworkNode { id: string; type: 'grower' | 'consumer' | 'plant'; location: { latitude: number; longitude: number }; connections: string[]; activityScore: number; species?: string[]; lastActive: string; } interface NetworkCluster { id: string; centroid: { latitude: number; longitude: number }; nodes: NetworkNode[]; radius: number; density: number; dominantSpecies: string[]; activityLevel: 'low' | 'medium' | 'high'; } interface CoverageGap { id: string; location: { latitude: number; longitude: number }; nearestCluster: string; distanceToNearest: number; populationDensity: 'urban' | 'suburban' | 'rural'; potentialDemand: number; recommendation: string; } interface ConnectionSuggestion { id: string; node1Id: string; node2Id: string; distance: number; reason: string; strength: number; // 0-100 mutualBenefits: string[]; } interface NetworkGrowth { date: string; totalNodes: number; totalConnections: number; newNodesWeek: number; newConnectionsWeek: number; geographicExpansion: number; // km radius growth } interface RegionalStats { region: string; centerLat: number; centerLon: number; nodeCount: number; plantCount: number; uniqueSpecies: number; avgActivityScore: number; connections: number; } export class NetworkDiscoveryAgent extends BaseAgent { private nodes: Map = new Map(); private clusters: NetworkCluster[] = []; private coverageGaps: CoverageGap[] = []; private connectionSuggestions: ConnectionSuggestion[] = []; private growthHistory: NetworkGrowth[] = []; private regionalStats: Map = new Map(); constructor() { const config: AgentConfig = { id: 'network-discovery-agent', name: 'Network Discovery Agent', description: 'Maps and analyzes the geographic plant network', enabled: true, intervalMs: 600000, // Run every 10 minutes priority: 'medium', maxRetries: 3, timeoutMs: 60000 }; super(config); } /** * Main execution cycle */ async runOnce(): Promise { const blockchain = getBlockchain(); const chain = blockchain.chain; const plants = chain.slice(1); // Build network from plant data this.buildNetworkNodes(plants); // Identify clusters this.identifyClusters(); // Find coverage gaps this.findCoverageGaps(); // Generate connection suggestions this.generateConnectionSuggestions(); // Update regional statistics this.updateRegionalStats(); // Track growth this.trackGrowth(); // Generate alerts for significant network events this.checkNetworkAlerts(); return this.createTaskResult('network_discovery', 'completed', { totalNodes: this.nodes.size, clustersIdentified: this.clusters.length, coverageGaps: this.coverageGaps.length, connectionSuggestions: this.connectionSuggestions.length, regions: this.regionalStats.size }); } /** * Build network nodes from plant data */ private buildNetworkNodes(plants: PlantBlock[]): void { // Group by owner to create nodes const ownerPlants = new Map(); for (const block of plants) { const ownerId = block.plant.owner?.id || 'unknown'; const ownerGroup = ownerPlants.get(ownerId) || []; ownerGroup.push(block); ownerPlants.set(ownerId, ownerGroup); } // Create nodes for each owner for (const [ownerId, ownerBlocks] of ownerPlants) { const latestBlock = ownerBlocks[ownerBlocks.length - 1]; const species = [...new Set(ownerBlocks.map(b => b.plant.commonName).filter(Boolean))]; // Calculate connections (plants from same lineage) const connections: string[] = []; for (const block of ownerBlocks) { if (block.plant.parentPlantId) { const parentOwner = plants.find(p => p.plant.id === block.plant.parentPlantId)?.plant.owner?.id; if (parentOwner && parentOwner !== ownerId && !connections.includes(parentOwner)) { connections.push(parentOwner); } } for (const childId of block.plant.childPlants || []) { const childOwner = plants.find(p => p.plant.id === childId)?.plant.owner?.id; if (childOwner && childOwner !== ownerId && !connections.includes(childOwner)) { connections.push(childOwner); } } } // Calculate activity score const recentActivity = ownerBlocks.filter(b => { const age = Date.now() - new Date(b.timestamp).getTime(); return age < 30 * 24 * 60 * 60 * 1000; // Last 30 days }).length; const node: NetworkNode = { id: ownerId, type: ownerBlocks.length > 5 ? 'grower' : 'consumer', location: latestBlock.plant.location, connections, activityScore: Math.min(100, 20 + recentActivity * 10 + connections.length * 5), species: species as string[], lastActive: latestBlock.timestamp }; this.nodes.set(ownerId, node); } // Also create plant nodes for visualization for (const block of plants) { const plantNode: NetworkNode = { id: `plant-${block.plant.id}`, type: 'plant', location: block.plant.location, connections: block.plant.childPlants?.map(c => `plant-${c}`) || [], activityScore: block.plant.status === 'growing' ? 80 : 40, species: [block.plant.commonName].filter(Boolean) as string[], lastActive: block.timestamp }; this.nodes.set(plantNode.id, plantNode); } } /** * Identify clusters using simplified DBSCAN-like algorithm */ private identifyClusters(): void { const nodes = Array.from(this.nodes.values()).filter(n => n.type !== 'plant'); const clusterRadius = 50; // km const minClusterSize = 2; const visited = new Set(); const clusters: NetworkCluster[] = []; for (const node of nodes) { if (visited.has(node.id)) continue; // Find all nodes within radius const neighborhood = nodes.filter(n => !visited.has(n.id) && this.calculateDistance(node.location, n.location) <= clusterRadius ); if (neighborhood.length >= minClusterSize) { // Create cluster const clusterNodes: NetworkNode[] = []; for (const neighbor of neighborhood) { visited.add(neighbor.id); clusterNodes.push(neighbor); } // Calculate centroid const centroid = { latitude: clusterNodes.reduce((sum, n) => sum + n.location.latitude, 0) / clusterNodes.length, longitude: clusterNodes.reduce((sum, n) => sum + n.location.longitude, 0) / clusterNodes.length }; // Calculate radius const maxDist = Math.max(...clusterNodes.map(n => this.calculateDistance(centroid, n.location) )); // Find dominant species const speciesCounts = new Map(); for (const n of clusterNodes) { for (const species of n.species || []) { speciesCounts.set(species, (speciesCounts.get(species) || 0) + 1); } } const dominantSpecies = Array.from(speciesCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([species]) => species); // Calculate activity level const avgActivity = clusterNodes.reduce((sum, n) => sum + n.activityScore, 0) / clusterNodes.length; const activityLevel = avgActivity > 70 ? 'high' : avgActivity > 40 ? 'medium' : 'low'; clusters.push({ id: `cluster-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, centroid, nodes: clusterNodes, radius: Math.round(maxDist * 10) / 10, density: clusterNodes.length / (Math.PI * maxDist * maxDist), dominantSpecies, activityLevel }); } } this.clusters = clusters; } /** * Find coverage gaps in the network */ private findCoverageGaps(): void { // Define key metropolitan areas that should have coverage const keyAreas = [ { name: 'Metro Center', lat: 40.7128, lon: -74.0060, pop: 'urban' }, { name: 'Suburban North', lat: 40.85, lon: -73.95, pop: 'suburban' }, { name: 'Suburban South', lat: 40.55, lon: -74.15, pop: 'suburban' }, { name: 'Rural West', lat: 40.7, lon: -74.4, pop: 'rural' } ]; const gaps: CoverageGap[] = []; for (const area of keyAreas) { // Find nearest cluster let nearestCluster: NetworkCluster | null = null; let nearestDistance = Infinity; for (const cluster of this.clusters) { const dist = this.calculateDistance( { latitude: area.lat, longitude: area.lon }, cluster.centroid ); if (dist < nearestDistance) { nearestDistance = dist; nearestCluster = cluster; } } // If no cluster within 30km, it's a gap if (nearestDistance > 30) { const potentialDemand = area.pop === 'urban' ? 1000 : area.pop === 'suburban' ? 500 : 100; gaps.push({ id: `gap-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, location: { latitude: area.lat, longitude: area.lon }, nearestCluster: nearestCluster?.id || 'none', distanceToNearest: Math.round(nearestDistance), populationDensity: area.pop as 'urban' | 'suburban' | 'rural', potentialDemand, recommendation: `Recruit growers in ${area.name} area to serve ${potentialDemand}+ potential consumers` }); } } this.coverageGaps = gaps; } /** * Generate connection suggestions */ private generateConnectionSuggestions(): void { const nodes = Array.from(this.nodes.values()).filter(n => n.type !== 'plant'); const suggestions: ConnectionSuggestion[] = []; for (const node1 of nodes) { for (const node2 of nodes) { if (node1.id >= node2.id) continue; // Avoid duplicates if (node1.connections.includes(node2.id)) continue; // Already connected const distance = this.calculateDistance(node1.location, node2.location); if (distance > 100) continue; // Too far // Calculate connection strength let strength = 50; const benefits: string[] = []; // Distance bonus if (distance < 10) { strength += 20; benefits.push('Very close proximity'); } else if (distance < 25) { strength += 10; benefits.push('Local connection'); } // Complementary types if (node1.type !== node2.type) { strength += 15; benefits.push('Grower-consumer match'); } // Shared species interest const sharedSpecies = node1.species?.filter(s => node2.species?.includes(s)) || []; if (sharedSpecies.length > 0) { strength += sharedSpecies.length * 5; benefits.push(`Shared interest: ${sharedSpecies.join(', ')}`); } // Activity match if (Math.abs(node1.activityScore - node2.activityScore) < 20) { strength += 10; benefits.push('Similar activity levels'); } if (strength >= 60) { suggestions.push({ id: `suggestion-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, node1Id: node1.id, node2Id: node2.id, distance: Math.round(distance * 10) / 10, reason: `${benefits[0] || 'Proximity match'}`, strength: Math.min(100, strength), mutualBenefits: benefits }); } } } // Sort by strength and keep top suggestions this.connectionSuggestions = suggestions .sort((a, b) => b.strength - a.strength) .slice(0, 50); } /** * Update regional statistics */ private updateRegionalStats(): void { // Define regions const regions = [ { name: 'Northeast', centerLat: 42, centerLon: -73, radius: 300 }, { name: 'Southeast', centerLat: 33, centerLon: -84, radius: 400 }, { name: 'Midwest', centerLat: 41, centerLon: -87, radius: 400 }, { name: 'Southwest', centerLat: 33, centerLon: -112, radius: 400 }, { name: 'West Coast', centerLat: 37, centerLon: -122, radius: 300 } ]; for (const region of regions) { const regionNodes = Array.from(this.nodes.values()).filter(n => { const dist = this.calculateDistance( n.location, { latitude: region.centerLat, longitude: region.centerLon } ); return dist <= region.radius; }); const plantNodes = regionNodes.filter(n => n.type === 'plant'); const otherNodes = regionNodes.filter(n => n.type !== 'plant'); const allSpecies = new Set(); regionNodes.forEach(n => n.species?.forEach(s => allSpecies.add(s))); const stats: RegionalStats = { region: region.name, centerLat: region.centerLat, centerLon: region.centerLon, nodeCount: otherNodes.length, plantCount: plantNodes.length, uniqueSpecies: allSpecies.size, avgActivityScore: otherNodes.length > 0 ? Math.round(otherNodes.reduce((sum, n) => sum + n.activityScore, 0) / otherNodes.length) : 0, connections: otherNodes.reduce((sum, n) => sum + n.connections.length, 0) }; this.regionalStats.set(region.name, stats); } } /** * Track network growth */ private trackGrowth(): void { const currentNodes = this.nodes.size; const currentConnections = Array.from(this.nodes.values()) .reduce((sum, n) => sum + n.connections.length, 0) / 2; const lastGrowth = this.growthHistory[this.growthHistory.length - 1]; const growth: NetworkGrowth = { date: new Date().toISOString(), totalNodes: currentNodes, totalConnections: Math.round(currentConnections), newNodesWeek: lastGrowth ? currentNodes - lastGrowth.totalNodes : currentNodes, newConnectionsWeek: lastGrowth ? Math.round(currentConnections) - lastGrowth.totalConnections : Math.round(currentConnections), geographicExpansion: this.calculateGeographicExpansion() }; this.growthHistory.push(growth); // Keep last year of data if (this.growthHistory.length > 52) { this.growthHistory = this.growthHistory.slice(-52); } } /** * Calculate geographic expansion */ private calculateGeographicExpansion(): number { if (this.nodes.size === 0) return 0; const nodes = Array.from(this.nodes.values()); let maxDistance = 0; // Find maximum distance between any two nodes (simplified) for (let i = 0; i < Math.min(nodes.length, 100); i++) { for (let j = i + 1; j < Math.min(nodes.length, 100); j++) { const dist = this.calculateDistance(nodes[i].location, nodes[j].location); maxDistance = Math.max(maxDistance, dist); } } return Math.round(maxDistance); } /** * Check for network alerts */ private checkNetworkAlerts(): void { // Alert for high-potential coverage gaps for (const gap of this.coverageGaps) { if (gap.populationDensity === 'urban' && gap.distanceToNearest > 50) { this.createAlert('warning', 'Coverage Gap in Urban Area', gap.recommendation, { relatedEntityId: gap.id, relatedEntityType: 'gap' } ); } } // Alert for network growth milestones const nodeCount = this.nodes.size; const milestones = [50, 100, 250, 500, 1000]; for (const milestone of milestones) { if (nodeCount >= milestone * 0.95 && nodeCount <= milestone * 1.05) { this.createAlert('info', 'Network Milestone', `Network has reached approximately ${milestone} participants!`, { relatedEntityType: 'network' } ); } } } /** * Calculate Haversine distance */ private calculateDistance( loc1: { latitude: number; longitude: number }, loc2: { latitude: number; longitude: number } ): number { const R = 6371; const dLat = (loc2.latitude - loc1.latitude) * Math.PI / 180; const dLon = (loc2.longitude - loc1.longitude) * Math.PI / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(loc1.latitude * Math.PI / 180) * Math.cos(loc2.latitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * Get network analysis */ getNetworkAnalysis(): NetworkAnalysis { return { totalNodes: this.nodes.size, totalConnections: Array.from(this.nodes.values()) .reduce((sum, n) => sum + n.connections.length, 0) / 2, clusters: this.clusters.map(c => ({ centroid: { lat: c.centroid.latitude, lon: c.centroid.longitude }, nodeCount: c.nodes.length, avgDistance: c.radius, dominantSpecies: c.dominantSpecies })), hotspots: this.clusters .filter(c => c.activityLevel === 'high') .map(c => ({ location: { lat: c.centroid.latitude, lon: c.centroid.longitude }, intensity: c.nodes.length * c.density, type: 'mixed' as const })), recommendations: this.coverageGaps.map(g => g.recommendation) }; } /** * Get clusters */ getClusters(): NetworkCluster[] { return this.clusters; } /** * Get coverage gaps */ getCoverageGaps(): CoverageGap[] { return this.coverageGaps; } /** * Get connection suggestions */ getConnectionSuggestions(): ConnectionSuggestion[] { return this.connectionSuggestions; } /** * Get growth history */ getGrowthHistory(): NetworkGrowth[] { return this.growthHistory; } /** * Get regional stats */ getRegionalStats(): RegionalStats[] { return Array.from(this.regionalStats.values()); } /** * Get node by ID */ getNode(nodeId: string): NetworkNode | null { return this.nodes.get(nodeId) || null; } } // Singleton instance let networkAgentInstance: NetworkDiscoveryAgent | null = null; export function getNetworkDiscoveryAgent(): NetworkDiscoveryAgent { if (!networkAgentInstance) { networkAgentInstance = new NetworkDiscoveryAgent(); } return networkAgentInstance; }