Agents created: 1. PlantLineageAgent - Monitors plant ancestry and lineage integrity 2. TransportTrackerAgent - Tracks transport events and carbon footprint 3. DemandForecastAgent - Predicts consumer demand and market trends 4. VerticalFarmAgent - Manages vertical farm operations and optimization 5. EnvironmentAnalysisAgent - Analyzes growing conditions and recommendations 6. MarketMatchingAgent - Connects grower supply with consumer demand 7. SustainabilityAgent - Monitors environmental impact and sustainability 8. NetworkDiscoveryAgent - Maps geographic distribution and network analysis 9. QualityAssuranceAgent - Verifies blockchain integrity and data quality 10. GrowerAdvisoryAgent - Provides personalized growing recommendations Also includes: - BaseAgent abstract class for common functionality - AgentOrchestrator for centralized agent management - Comprehensive type definitions - Full documentation in docs/AGENTS.md
462 lines
14 KiB
TypeScript
462 lines
14 KiB
TypeScript
/**
|
|
* PlantLineageAgent
|
|
* Monitors and manages plant lineage tracking in the blockchain
|
|
*
|
|
* Responsibilities:
|
|
* - Validate new plant registrations
|
|
* - Track generation lineage and ancestry
|
|
* - Detect and alert on anomalies in plant data
|
|
* - Generate lineage reports and family trees
|
|
* - Monitor plant status transitions
|
|
*/
|
|
|
|
import { BaseAgent } from './BaseAgent';
|
|
import { AgentConfig, AgentTask } from './types';
|
|
import { getBlockchain } from '../blockchain/manager';
|
|
import { PlantData, PlantBlock, PropagationType } from '../blockchain/types';
|
|
|
|
interface LineageAnalysis {
|
|
plantId: string;
|
|
generation: number;
|
|
ancestors: string[];
|
|
descendants: string[];
|
|
totalLineageSize: number;
|
|
propagationChain: PropagationType[];
|
|
geographicSpread: number; // km
|
|
oldestAncestorDate: string;
|
|
healthScore: number;
|
|
}
|
|
|
|
interface LineageAnomaly {
|
|
type: 'orphan' | 'circular' | 'invalid_generation' | 'missing_parent' | 'suspicious_location';
|
|
plantId: string;
|
|
description: string;
|
|
severity: 'low' | 'medium' | 'high';
|
|
}
|
|
|
|
export class PlantLineageAgent extends BaseAgent {
|
|
private lineageCache: Map<string, LineageAnalysis> = new Map();
|
|
private anomalyLog: LineageAnomaly[] = [];
|
|
private lastFullScanAt: string | null = null;
|
|
|
|
constructor() {
|
|
const config: AgentConfig = {
|
|
id: 'plant-lineage-agent',
|
|
name: 'Plant Lineage Agent',
|
|
description: 'Monitors plant lineage integrity and generates ancestry reports',
|
|
enabled: true,
|
|
intervalMs: 60000, // Run every minute
|
|
priority: 'high',
|
|
maxRetries: 3,
|
|
timeoutMs: 30000
|
|
};
|
|
super(config);
|
|
}
|
|
|
|
/**
|
|
* Main execution cycle
|
|
*/
|
|
async runOnce(): Promise<AgentTask | null> {
|
|
const blockchain = getBlockchain();
|
|
const chain = blockchain.getChain();
|
|
|
|
// Skip genesis block
|
|
const plantBlocks = chain.slice(1);
|
|
let processedCount = 0;
|
|
let anomaliesFound = 0;
|
|
|
|
for (const block of plantBlocks) {
|
|
const plant = block.plant;
|
|
|
|
// Analyze lineage if not cached or stale
|
|
if (!this.lineageCache.has(plant.id)) {
|
|
const analysis = this.analyzeLineage(plant.id, chain);
|
|
this.lineageCache.set(plant.id, analysis);
|
|
}
|
|
|
|
// Check for anomalies
|
|
const anomalies = this.detectAnomalies(block, chain);
|
|
for (const anomaly of anomalies) {
|
|
this.anomalyLog.push(anomaly);
|
|
anomaliesFound++;
|
|
|
|
if (anomaly.severity === 'high') {
|
|
this.createAlert('warning', `Lineage Anomaly: ${anomaly.type}`,
|
|
anomaly.description,
|
|
{ relatedEntityId: anomaly.plantId, relatedEntityType: 'plant' }
|
|
);
|
|
}
|
|
}
|
|
|
|
processedCount++;
|
|
}
|
|
|
|
// Monitor for plants needing attention
|
|
this.monitorPlantHealth(chain);
|
|
|
|
// Update scan timestamp
|
|
this.lastFullScanAt = new Date().toISOString();
|
|
|
|
// Keep anomaly log manageable
|
|
if (this.anomalyLog.length > 1000) {
|
|
this.anomalyLog = this.anomalyLog.slice(-500);
|
|
}
|
|
|
|
return this.createTaskResult('lineage_scan', 'completed', {
|
|
plantsScanned: processedCount,
|
|
anomaliesFound,
|
|
cacheSize: this.lineageCache.size,
|
|
timestamp: this.lastFullScanAt
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Analyze complete lineage for a plant
|
|
*/
|
|
private analyzeLineage(plantId: string, chain: PlantBlock[]): LineageAnalysis {
|
|
const plant = chain.find(b => b.plant.id === plantId)?.plant;
|
|
if (!plant) {
|
|
return this.createEmptyLineageAnalysis(plantId);
|
|
}
|
|
|
|
const ancestors = this.findAncestors(plantId, chain);
|
|
const descendants = this.findDescendants(plantId, chain);
|
|
const propagationChain = this.buildPropagationChain(plantId, chain);
|
|
const geographicSpread = this.calculateGeographicSpread(plantId, chain);
|
|
const oldestAncestor = this.findOldestAncestor(plantId, chain);
|
|
|
|
return {
|
|
plantId,
|
|
generation: plant.generation,
|
|
ancestors,
|
|
descendants,
|
|
totalLineageSize: ancestors.length + descendants.length + 1,
|
|
propagationChain,
|
|
geographicSpread,
|
|
oldestAncestorDate: oldestAncestor?.timestamp || plant.dateAcquired,
|
|
healthScore: this.calculateHealthScore(plant, chain)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Find all ancestors recursively
|
|
*/
|
|
private findAncestors(plantId: string, chain: PlantBlock[], visited: Set<string> = new Set()): string[] {
|
|
if (visited.has(plantId)) return [];
|
|
visited.add(plantId);
|
|
|
|
const plant = chain.find(b => b.plant.id === plantId)?.plant;
|
|
if (!plant || !plant.parentPlantId) return [];
|
|
|
|
const ancestors = [plant.parentPlantId];
|
|
const parentAncestors = this.findAncestors(plant.parentPlantId, chain, visited);
|
|
return [...ancestors, ...parentAncestors];
|
|
}
|
|
|
|
/**
|
|
* Find all descendants recursively
|
|
*/
|
|
private findDescendants(plantId: string, chain: PlantBlock[], visited: Set<string> = new Set()): string[] {
|
|
if (visited.has(plantId)) return [];
|
|
visited.add(plantId);
|
|
|
|
const plant = chain.find(b => b.plant.id === plantId)?.plant;
|
|
if (!plant) return [];
|
|
|
|
const descendants: string[] = [...(plant.childPlants || [])];
|
|
|
|
for (const childId of plant.childPlants || []) {
|
|
const childDescendants = this.findDescendants(childId, chain, visited);
|
|
descendants.push(...childDescendants);
|
|
}
|
|
|
|
return descendants;
|
|
}
|
|
|
|
/**
|
|
* Build propagation type chain from origin to plant
|
|
*/
|
|
private buildPropagationChain(plantId: string, chain: PlantBlock[]): PropagationType[] {
|
|
const result: PropagationType[] = [];
|
|
let currentId: string | undefined = plantId;
|
|
|
|
while (currentId) {
|
|
const plant = chain.find(b => b.plant.id === currentId)?.plant;
|
|
if (!plant) break;
|
|
|
|
result.unshift(plant.propagationType);
|
|
currentId = plant.parentPlantId;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate geographic spread of lineage in km
|
|
*/
|
|
private calculateGeographicSpread(plantId: string, chain: PlantBlock[]): number {
|
|
const lineagePlantIds = [
|
|
plantId,
|
|
...this.findAncestors(plantId, chain),
|
|
...this.findDescendants(plantId, chain)
|
|
];
|
|
|
|
const locations = lineagePlantIds
|
|
.map(id => chain.find(b => b.plant.id === id)?.plant.location)
|
|
.filter(loc => loc !== undefined);
|
|
|
|
if (locations.length < 2) return 0;
|
|
|
|
let maxDistance = 0;
|
|
for (let i = 0; i < locations.length; i++) {
|
|
for (let j = i + 1; j < locations.length; j++) {
|
|
const distance = this.calculateHaversine(
|
|
locations[i]!.latitude, locations[i]!.longitude,
|
|
locations[j]!.latitude, locations[j]!.longitude
|
|
);
|
|
maxDistance = Math.max(maxDistance, distance);
|
|
}
|
|
}
|
|
|
|
return Math.round(maxDistance * 100) / 100;
|
|
}
|
|
|
|
/**
|
|
* Find oldest ancestor
|
|
*/
|
|
private findOldestAncestor(plantId: string, chain: PlantBlock[]): PlantBlock | null {
|
|
const ancestors = this.findAncestors(plantId, chain);
|
|
if (ancestors.length === 0) return null;
|
|
|
|
let oldest: PlantBlock | null = null;
|
|
let oldestDate = new Date();
|
|
|
|
for (const ancestorId of ancestors) {
|
|
const block = chain.find(b => b.plant.id === ancestorId);
|
|
if (block) {
|
|
const date = new Date(block.timestamp);
|
|
if (date < oldestDate) {
|
|
oldestDate = date;
|
|
oldest = block;
|
|
}
|
|
}
|
|
}
|
|
|
|
return oldest;
|
|
}
|
|
|
|
/**
|
|
* Calculate health score for a plant (0-100)
|
|
*/
|
|
private calculateHealthScore(plant: PlantData, chain: PlantBlock[]): number {
|
|
let score = 100;
|
|
|
|
// Deduct for status issues
|
|
if (plant.status === 'deceased') score -= 50;
|
|
if (plant.status === 'dormant') score -= 10;
|
|
|
|
// Deduct for missing data
|
|
if (!plant.environment) score -= 10;
|
|
if (!plant.growthMetrics) score -= 10;
|
|
|
|
// Deduct for high generation (genetic drift risk)
|
|
if (plant.generation > 10) score -= Math.min(20, plant.generation - 10);
|
|
|
|
// Bonus for having offspring (successful propagation)
|
|
if (plant.childPlants && plant.childPlants.length > 0) {
|
|
score += Math.min(10, plant.childPlants.length * 2);
|
|
}
|
|
|
|
return Math.max(0, Math.min(100, score));
|
|
}
|
|
|
|
/**
|
|
* Detect anomalies in plant data
|
|
*/
|
|
private detectAnomalies(block: PlantBlock, chain: PlantBlock[]): LineageAnomaly[] {
|
|
const anomalies: LineageAnomaly[] = [];
|
|
const plant = block.plant;
|
|
|
|
// Check for orphan plants (non-original with missing parent)
|
|
if (plant.propagationType !== 'original' && plant.parentPlantId) {
|
|
const parent = chain.find(b => b.plant.id === plant.parentPlantId);
|
|
if (!parent) {
|
|
anomalies.push({
|
|
type: 'missing_parent',
|
|
plantId: plant.id,
|
|
description: `Plant ${plant.id} references parent ${plant.parentPlantId} which doesn't exist`,
|
|
severity: 'medium'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for invalid generation numbers
|
|
if (plant.parentPlantId) {
|
|
const parent = chain.find(b => b.plant.id === plant.parentPlantId);
|
|
if (parent && plant.generation !== parent.plant.generation + 1) {
|
|
anomalies.push({
|
|
type: 'invalid_generation',
|
|
plantId: plant.id,
|
|
description: `Generation ${plant.generation} doesn't match parent generation ${parent.plant.generation}`,
|
|
severity: 'medium'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for suspicious location jumps
|
|
if (plant.parentPlantId) {
|
|
const parent = chain.find(b => b.plant.id === plant.parentPlantId);
|
|
if (parent) {
|
|
const distance = this.calculateHaversine(
|
|
plant.location.latitude, plant.location.longitude,
|
|
parent.plant.location.latitude, parent.plant.location.longitude
|
|
);
|
|
// Flag if offspring is more than 1000km from parent
|
|
if (distance > 1000) {
|
|
anomalies.push({
|
|
type: 'suspicious_location',
|
|
plantId: plant.id,
|
|
description: `Plant is ${Math.round(distance)}km from parent - verify transport records`,
|
|
severity: 'low'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for circular references
|
|
const ancestors = this.findAncestors(plant.id, chain);
|
|
if (ancestors.includes(plant.id)) {
|
|
anomalies.push({
|
|
type: 'circular',
|
|
plantId: plant.id,
|
|
description: 'Circular lineage reference detected',
|
|
severity: 'high'
|
|
});
|
|
}
|
|
|
|
return anomalies;
|
|
}
|
|
|
|
/**
|
|
* Monitor overall plant health across network
|
|
*/
|
|
private monitorPlantHealth(chain: PlantBlock[]): void {
|
|
const statusCounts: Record<string, number> = {};
|
|
let deceasedCount = 0;
|
|
let recentDeaths = 0;
|
|
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
|
|
for (const block of chain.slice(1)) {
|
|
const status = block.plant.status;
|
|
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
|
|
if (status === 'deceased') {
|
|
deceasedCount++;
|
|
if (new Date(block.timestamp).getTime() > oneWeekAgo) {
|
|
recentDeaths++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Alert if death rate is high
|
|
const totalPlants = chain.length - 1;
|
|
if (totalPlants > 10 && recentDeaths / totalPlants > 0.1) {
|
|
this.createAlert('warning', 'High Plant Mortality Rate',
|
|
`${recentDeaths} plants marked as deceased in the last week (${Math.round(recentDeaths / totalPlants * 100)}% of network)`,
|
|
{ actionRequired: 'Investigate potential environmental or disease issues' }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate Haversine distance
|
|
*/
|
|
private calculateHaversine(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
|
const R = 6371; // km
|
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
|
const dLon = (lon2 - lon1) * Math.PI / 180;
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * 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;
|
|
}
|
|
|
|
/**
|
|
* Create empty lineage analysis
|
|
*/
|
|
private createEmptyLineageAnalysis(plantId: string): LineageAnalysis {
|
|
return {
|
|
plantId,
|
|
generation: 0,
|
|
ancestors: [],
|
|
descendants: [],
|
|
totalLineageSize: 1,
|
|
propagationChain: [],
|
|
geographicSpread: 0,
|
|
oldestAncestorDate: new Date().toISOString(),
|
|
healthScore: 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get lineage analysis for a specific plant
|
|
*/
|
|
getLineageAnalysis(plantId: string): LineageAnalysis | null {
|
|
return this.lineageCache.get(plantId) || null;
|
|
}
|
|
|
|
/**
|
|
* Get all detected anomalies
|
|
*/
|
|
getAnomalies(): LineageAnomaly[] {
|
|
return this.anomalyLog;
|
|
}
|
|
|
|
/**
|
|
* Get network statistics
|
|
*/
|
|
getNetworkStats(): {
|
|
totalPlants: number;
|
|
totalLineages: number;
|
|
avgGenerationDepth: number;
|
|
avgLineageSize: number;
|
|
geographicSpread: number;
|
|
} {
|
|
const analyses = Array.from(this.lineageCache.values());
|
|
const totalPlants = analyses.length;
|
|
|
|
if (totalPlants === 0) {
|
|
return {
|
|
totalPlants: 0,
|
|
totalLineages: 0,
|
|
avgGenerationDepth: 0,
|
|
avgLineageSize: 0,
|
|
geographicSpread: 0
|
|
};
|
|
}
|
|
|
|
const rootPlants = analyses.filter(a => a.ancestors.length === 0);
|
|
const avgGen = analyses.reduce((sum, a) => sum + a.generation, 0) / totalPlants;
|
|
const avgSize = analyses.reduce((sum, a) => sum + a.totalLineageSize, 0) / totalPlants;
|
|
const maxSpread = Math.max(...analyses.map(a => a.geographicSpread));
|
|
|
|
return {
|
|
totalPlants,
|
|
totalLineages: rootPlants.length,
|
|
avgGenerationDepth: Math.round(avgGen * 10) / 10,
|
|
avgLineageSize: Math.round(avgSize * 10) / 10,
|
|
geographicSpread: maxSpread
|
|
};
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let lineageAgentInstance: PlantLineageAgent | null = null;
|
|
|
|
export function getPlantLineageAgent(): PlantLineageAgent {
|
|
if (!lineageAgentInstance) {
|
|
lineageAgentInstance = new PlantLineageAgent();
|
|
}
|
|
return lineageAgentInstance;
|
|
}
|