localgreenchain/lib/agents/NetworkDiscoveryAgent.ts
Claude 507df5912f
Deploy GrowerAdvisoryAgent (Agent 10) and fix type errors
- Add GrowerAdvisoryAgent test file
- Fix PlantChain constructor initialization order (plantIndex before genesis block)
- Fix blockchain.getChain() calls to use blockchain.chain property
- Add PropagationType export to blockchain types
- Fix SoilComposition.type property references (was soilType)
- Fix ClimateConditions.temperatureDay property references (was avgTemperature)
- Fix ClimateConditions.humidityAverage property references (was avgHumidity)
- Fix LightingConditions.naturalLight.hoursPerDay nested access
- Add 'critical' severity to QualityReport issues
- Add 'sqm' unit to PlantingRecommendation.quantityUnit
- Fix GrowerAdvisoryAgent growthMetrics property access
- Update TypeScript to v5 for react-hook-form compatibility
- Enable downlevelIteration in tsconfig for Map iteration
- Fix crypto Buffer type issues in anonymity.ts
- Fix zones.tsx status type comparison
- Fix next.config.js images.domains filter
- Rename [[...slug]].tsx to [...slug].tsx to resolve routing conflict
2025-11-23 00:44:58 +00:00

610 lines
19 KiB
TypeScript

/**
* 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<string, NetworkNode> = new Map();
private clusters: NetworkCluster[] = [];
private coverageGaps: CoverageGap[] = [];
private connectionSuggestions: ConnectionSuggestion[] = [];
private growthHistory: NetworkGrowth[] = [];
private regionalStats: Map<string, RegionalStats> = 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<AgentTask | null> {
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<string, PlantBlock[]>();
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<string>();
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<string, number>();
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<string>();
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;
}