- 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
556 lines
18 KiB
TypeScript
556 lines
18 KiB
TypeScript
/**
|
|
* SustainabilityAgent
|
|
* Monitors and reports on environmental impact across the network
|
|
*
|
|
* Responsibilities:
|
|
* - Calculate network-wide carbon footprint
|
|
* - Track food miles reduction vs conventional
|
|
* - Monitor water usage in vertical farms
|
|
* - Generate sustainability reports
|
|
* - Identify improvement opportunities
|
|
*/
|
|
|
|
import { BaseAgent } from './BaseAgent';
|
|
import { AgentConfig, AgentTask, SustainabilityReport } from './types';
|
|
import { getTransportChain } from '../transport/tracker';
|
|
import { getBlockchain } from '../blockchain/manager';
|
|
|
|
interface CarbonMetrics {
|
|
totalEmissionsKg: number;
|
|
emissionsPerKgProduce: number;
|
|
savedVsConventionalKg: number;
|
|
percentageReduction: number;
|
|
byTransportMethod: Record<string, number>;
|
|
byEventType: Record<string, number>;
|
|
trend: 'improving' | 'stable' | 'worsening';
|
|
}
|
|
|
|
interface FoodMilesMetrics {
|
|
totalMiles: number;
|
|
avgMilesPerDelivery: number;
|
|
localDeliveryPercent: number; // < 50km
|
|
regionalDeliveryPercent: number; // 50-200km
|
|
longDistancePercent: number; // > 200km
|
|
conventionalComparison: number; // avg miles saved
|
|
}
|
|
|
|
interface WaterMetrics {
|
|
totalUsageLiters: number;
|
|
verticalFarmUsage: number;
|
|
traditionalUsage: number;
|
|
savedLiters: number;
|
|
recyclingRate: number;
|
|
efficiencyScore: number;
|
|
}
|
|
|
|
interface WasteMetrics {
|
|
totalWasteKg: number;
|
|
spoilageKg: number;
|
|
compostedKg: number;
|
|
wasteReductionPercent: number;
|
|
spoilageRate: number;
|
|
}
|
|
|
|
interface SustainabilityScore {
|
|
overall: number;
|
|
carbon: number;
|
|
water: number;
|
|
waste: number;
|
|
localFood: number;
|
|
biodiversity: number;
|
|
}
|
|
|
|
interface ImprovementOpportunity {
|
|
id: string;
|
|
category: 'carbon' | 'water' | 'waste' | 'transport' | 'energy';
|
|
title: string;
|
|
description: string;
|
|
potentialImpact: string;
|
|
difficulty: 'easy' | 'moderate' | 'challenging';
|
|
estimatedSavings: { value: number; unit: string };
|
|
priority: 'low' | 'medium' | 'high';
|
|
}
|
|
|
|
export class SustainabilityAgent extends BaseAgent {
|
|
private carbonMetrics: CarbonMetrics | null = null;
|
|
private foodMilesMetrics: FoodMilesMetrics | null = null;
|
|
private waterMetrics: WaterMetrics | null = null;
|
|
private wasteMetrics: WasteMetrics | null = null;
|
|
private sustainabilityScore: SustainabilityScore | null = null;
|
|
private opportunities: ImprovementOpportunity[] = [];
|
|
private historicalScores: { date: string; score: number }[] = [];
|
|
|
|
constructor() {
|
|
const config: AgentConfig = {
|
|
id: 'sustainability-agent',
|
|
name: 'Sustainability Agent',
|
|
description: 'Monitors environmental impact and sustainability metrics',
|
|
enabled: true,
|
|
intervalMs: 300000, // Run every 5 minutes
|
|
priority: 'medium',
|
|
maxRetries: 3,
|
|
timeoutMs: 60000
|
|
};
|
|
super(config);
|
|
}
|
|
|
|
/**
|
|
* Main execution cycle
|
|
*/
|
|
async runOnce(): Promise<AgentTask | null> {
|
|
// Calculate all metrics
|
|
this.carbonMetrics = this.calculateCarbonMetrics();
|
|
this.foodMilesMetrics = this.calculateFoodMilesMetrics();
|
|
this.waterMetrics = this.calculateWaterMetrics();
|
|
this.wasteMetrics = this.calculateWasteMetrics();
|
|
|
|
// Calculate overall sustainability score
|
|
this.sustainabilityScore = this.calculateSustainabilityScore();
|
|
|
|
// Track historical scores
|
|
this.historicalScores.push({
|
|
date: new Date().toISOString(),
|
|
score: this.sustainabilityScore.overall
|
|
});
|
|
if (this.historicalScores.length > 365) {
|
|
this.historicalScores = this.historicalScores.slice(-365);
|
|
}
|
|
|
|
// Identify improvement opportunities
|
|
this.opportunities = this.identifyOpportunities();
|
|
|
|
// Generate alerts for concerning metrics
|
|
this.checkMetricAlerts();
|
|
|
|
// Generate milestone alerts
|
|
this.checkMilestones();
|
|
|
|
return this.createTaskResult('sustainability_analysis', 'completed', {
|
|
overallScore: this.sustainabilityScore.overall,
|
|
carbonSavedKg: this.carbonMetrics.savedVsConventionalKg,
|
|
foodMilesSaved: this.foodMilesMetrics?.conventionalComparison || 0,
|
|
opportunitiesIdentified: this.opportunities.length
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate carbon metrics
|
|
*/
|
|
private calculateCarbonMetrics(): CarbonMetrics {
|
|
const transportChain = getTransportChain();
|
|
const events = transportChain.chain.slice(1).map(b => b.transportEvent);
|
|
|
|
let totalEmissions = 0;
|
|
let totalWeightKg = 0;
|
|
const byMethod: Record<string, number> = {};
|
|
const byEventType: Record<string, number> = {};
|
|
|
|
for (const event of events) {
|
|
totalEmissions += event.carbonFootprintKg;
|
|
totalWeightKg += 5; // Estimate avg weight
|
|
|
|
byMethod[event.transportMethod] = (byMethod[event.transportMethod] || 0) + event.carbonFootprintKg;
|
|
byEventType[event.eventType] = (byEventType[event.eventType] || 0) + event.carbonFootprintKg;
|
|
}
|
|
|
|
// Conventional comparison: 2.5 kg CO2 per kg produce avg
|
|
const conventionalEmissions = totalWeightKg * 2.5;
|
|
const saved = Math.max(0, conventionalEmissions - totalEmissions);
|
|
|
|
// Determine trend
|
|
const recentScores = this.historicalScores.slice(-10);
|
|
let trend: 'improving' | 'stable' | 'worsening' = 'stable';
|
|
|
|
if (recentScores.length >= 5) {
|
|
const firstHalf = recentScores.slice(0, 5).reduce((s, e) => s + e.score, 0) / 5;
|
|
const secondHalf = recentScores.slice(-5).reduce((s, e) => s + e.score, 0) / 5;
|
|
if (secondHalf > firstHalf + 2) trend = 'improving';
|
|
else if (secondHalf < firstHalf - 2) trend = 'worsening';
|
|
}
|
|
|
|
return {
|
|
totalEmissionsKg: Math.round(totalEmissions * 100) / 100,
|
|
emissionsPerKgProduce: totalWeightKg > 0
|
|
? Math.round((totalEmissions / totalWeightKg) * 1000) / 1000
|
|
: 0,
|
|
savedVsConventionalKg: Math.round(saved * 100) / 100,
|
|
percentageReduction: conventionalEmissions > 0
|
|
? Math.round((1 - totalEmissions / conventionalEmissions) * 100)
|
|
: 0,
|
|
byTransportMethod: byMethod,
|
|
byEventType,
|
|
trend
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate food miles metrics
|
|
*/
|
|
private calculateFoodMilesMetrics(): FoodMilesMetrics {
|
|
const transportChain = getTransportChain();
|
|
const events = transportChain.chain.slice(1).map(b => b.transportEvent);
|
|
|
|
let totalMiles = 0;
|
|
let localCount = 0;
|
|
let regionalCount = 0;
|
|
let longDistanceCount = 0;
|
|
|
|
for (const event of events) {
|
|
const km = event.distanceKm;
|
|
totalMiles += km;
|
|
|
|
if (km < 50) localCount++;
|
|
else if (km < 200) regionalCount++;
|
|
else longDistanceCount++;
|
|
}
|
|
|
|
const totalDeliveries = events.length;
|
|
|
|
// Conventional avg: 1500 miles per item
|
|
const conventionalMiles = totalDeliveries * 1500;
|
|
|
|
return {
|
|
totalMiles: Math.round(totalMiles),
|
|
avgMilesPerDelivery: totalDeliveries > 0
|
|
? Math.round(totalMiles / totalDeliveries)
|
|
: 0,
|
|
localDeliveryPercent: totalDeliveries > 0
|
|
? Math.round((localCount / totalDeliveries) * 100)
|
|
: 0,
|
|
regionalDeliveryPercent: totalDeliveries > 0
|
|
? Math.round((regionalCount / totalDeliveries) * 100)
|
|
: 0,
|
|
longDistancePercent: totalDeliveries > 0
|
|
? Math.round((longDistanceCount / totalDeliveries) * 100)
|
|
: 0,
|
|
conventionalComparison: Math.round(conventionalMiles - totalMiles)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate water metrics (simulated for demo)
|
|
*/
|
|
private calculateWaterMetrics(): WaterMetrics {
|
|
const blockchain = getBlockchain();
|
|
const plantCount = blockchain.chain.length - 1;
|
|
|
|
// Simulate water usage based on plant count
|
|
// Vertical farms use ~10% of traditional water
|
|
const traditionalUsagePerPlant = 500; // liters
|
|
const verticalFarmUsagePerPlant = 50; // liters
|
|
|
|
const verticalFarmRatio = 0.3; // 30% in vertical farms
|
|
const verticalFarmPlants = Math.floor(plantCount * verticalFarmRatio);
|
|
const traditionalPlants = plantCount - verticalFarmPlants;
|
|
|
|
const verticalUsage = verticalFarmPlants * verticalFarmUsagePerPlant;
|
|
const traditionalUsage = traditionalPlants * traditionalUsagePerPlant;
|
|
const totalUsage = verticalUsage + traditionalUsage;
|
|
|
|
const conventionalUsage = plantCount * traditionalUsagePerPlant;
|
|
const saved = conventionalUsage - totalUsage;
|
|
|
|
return {
|
|
totalUsageLiters: totalUsage,
|
|
verticalFarmUsage: verticalUsage,
|
|
traditionalUsage: traditionalUsage,
|
|
savedLiters: Math.max(0, saved),
|
|
recyclingRate: 85, // 85% water recycling in vertical farms
|
|
efficiencyScore: Math.round((saved / conventionalUsage) * 100)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate waste metrics (simulated for demo)
|
|
*/
|
|
private calculateWasteMetrics(): WasteMetrics {
|
|
const blockchain = getBlockchain();
|
|
const plants = blockchain.chain.slice(1);
|
|
|
|
const deceasedPlants = plants.filter(p => p.plant.status === 'deceased').length;
|
|
const totalPlants = plants.length;
|
|
|
|
// Conventional spoilage: 30-40%
|
|
const conventionalSpoilageRate = 0.35;
|
|
const localSpoilageRate = totalPlants > 0
|
|
? deceasedPlants / totalPlants
|
|
: 0;
|
|
|
|
const totalProduceKg = totalPlants * 2; // Estimate 2kg per plant
|
|
const spoilageKg = totalProduceKg * localSpoilageRate;
|
|
const compostedKg = spoilageKg * 0.8; // 80% composted
|
|
|
|
return {
|
|
totalWasteKg: Math.round(spoilageKg * 10) / 10,
|
|
spoilageKg: Math.round(spoilageKg * 10) / 10,
|
|
compostedKg: Math.round(compostedKg * 10) / 10,
|
|
wasteReductionPercent: Math.round((conventionalSpoilageRate - localSpoilageRate) / conventionalSpoilageRate * 100),
|
|
spoilageRate: Math.round(localSpoilageRate * 100)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate overall sustainability score
|
|
*/
|
|
private calculateSustainabilityScore(): SustainabilityScore {
|
|
const carbon = this.carbonMetrics
|
|
? Math.min(100, Math.max(0, this.carbonMetrics.percentageReduction + 20))
|
|
: 50;
|
|
|
|
const water = this.waterMetrics
|
|
? Math.min(100, this.waterMetrics.efficiencyScore + 30)
|
|
: 50;
|
|
|
|
const waste = this.wasteMetrics
|
|
? Math.min(100, 100 - this.wasteMetrics.spoilageRate * 2)
|
|
: 50;
|
|
|
|
const localFood = this.foodMilesMetrics
|
|
? Math.min(100, this.foodMilesMetrics.localDeliveryPercent + this.foodMilesMetrics.regionalDeliveryPercent)
|
|
: 50;
|
|
|
|
// Biodiversity: based on plant variety
|
|
const blockchain = getBlockchain();
|
|
const plants = blockchain.chain.slice(1);
|
|
const uniqueSpecies = new Set(plants.map(p => p.plant.commonName)).size;
|
|
const biodiversity = Math.min(100, 30 + uniqueSpecies * 5);
|
|
|
|
const overall = Math.round(
|
|
(carbon * 0.25 + water * 0.2 + waste * 0.15 + localFood * 0.25 + biodiversity * 0.15)
|
|
);
|
|
|
|
return {
|
|
overall,
|
|
carbon: Math.round(carbon),
|
|
water: Math.round(water),
|
|
waste: Math.round(waste),
|
|
localFood: Math.round(localFood),
|
|
biodiversity: Math.round(biodiversity)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Identify improvement opportunities
|
|
*/
|
|
private identifyOpportunities(): ImprovementOpportunity[] {
|
|
const opportunities: ImprovementOpportunity[] = [];
|
|
|
|
// Carbon opportunities
|
|
if (this.carbonMetrics) {
|
|
if (this.carbonMetrics.byTransportMethod['gasoline_vehicle'] > 10) {
|
|
opportunities.push({
|
|
id: `opp-${Date.now()}-ev`,
|
|
category: 'transport',
|
|
title: 'Switch to Electric Vehicles',
|
|
description: 'Replace gasoline vehicles with EVs for local deliveries',
|
|
potentialImpact: 'Reduce transport emissions by 60-80%',
|
|
difficulty: 'moderate',
|
|
estimatedSavings: {
|
|
value: this.carbonMetrics.byTransportMethod['gasoline_vehicle'] * 0.7,
|
|
unit: 'kg CO2'
|
|
},
|
|
priority: 'high'
|
|
});
|
|
}
|
|
|
|
if (this.carbonMetrics.byTransportMethod['air_freight'] > 0) {
|
|
opportunities.push({
|
|
id: `opp-${Date.now()}-air`,
|
|
category: 'transport',
|
|
title: 'Eliminate Air Freight',
|
|
description: 'Replace air freight with rail or local sourcing',
|
|
potentialImpact: 'Eliminate highest-carbon transport method',
|
|
difficulty: 'challenging',
|
|
estimatedSavings: {
|
|
value: this.carbonMetrics.byTransportMethod['air_freight'],
|
|
unit: 'kg CO2'
|
|
},
|
|
priority: 'high'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Food miles opportunities
|
|
if (this.foodMilesMetrics && this.foodMilesMetrics.longDistancePercent > 10) {
|
|
opportunities.push({
|
|
id: `opp-${Date.now()}-local`,
|
|
category: 'transport',
|
|
title: 'Increase Local Sourcing',
|
|
description: 'Partner with more local growers to reduce food miles',
|
|
potentialImpact: `Reduce ${this.foodMilesMetrics.longDistancePercent}% long-distance deliveries`,
|
|
difficulty: 'moderate',
|
|
estimatedSavings: {
|
|
value: Math.round(this.foodMilesMetrics.totalMiles * 0.3),
|
|
unit: 'miles'
|
|
},
|
|
priority: 'medium'
|
|
});
|
|
}
|
|
|
|
// Water opportunities
|
|
if (this.waterMetrics && this.waterMetrics.recyclingRate < 90) {
|
|
opportunities.push({
|
|
id: `opp-${Date.now()}-water`,
|
|
category: 'water',
|
|
title: 'Improve Water Recycling',
|
|
description: 'Upgrade water recycling systems in vertical farms',
|
|
potentialImpact: 'Increase water recycling from 85% to 95%',
|
|
difficulty: 'moderate',
|
|
estimatedSavings: {
|
|
value: Math.round(this.waterMetrics.totalUsageLiters * 0.1),
|
|
unit: 'liters'
|
|
},
|
|
priority: 'medium'
|
|
});
|
|
}
|
|
|
|
// Waste opportunities
|
|
if (this.wasteMetrics && this.wasteMetrics.spoilageRate > 10) {
|
|
opportunities.push({
|
|
id: `opp-${Date.now()}-waste`,
|
|
category: 'waste',
|
|
title: 'Reduce Spoilage Rate',
|
|
description: 'Implement better cold chain and demand forecasting',
|
|
potentialImpact: 'Reduce spoilage from ' + this.wasteMetrics.spoilageRate + '% to 5%',
|
|
difficulty: 'moderate',
|
|
estimatedSavings: {
|
|
value: Math.round(this.wasteMetrics.spoilageKg * 0.5),
|
|
unit: 'kg produce'
|
|
},
|
|
priority: 'high'
|
|
});
|
|
}
|
|
|
|
return opportunities.sort((a, b) =>
|
|
a.priority === 'high' ? -1 : b.priority === 'high' ? 1 : 0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check metric alerts
|
|
*/
|
|
private checkMetricAlerts(): void {
|
|
if (this.carbonMetrics && this.carbonMetrics.trend === 'worsening') {
|
|
this.createAlert('warning', 'Carbon Footprint Increasing',
|
|
'Network carbon emissions trending upward over the past week',
|
|
{ actionRequired: 'Review transport methods and route efficiency' }
|
|
);
|
|
}
|
|
|
|
if (this.wasteMetrics && this.wasteMetrics.spoilageRate > 15) {
|
|
this.createAlert('warning', 'High Spoilage Rate',
|
|
`Current spoilage rate of ${this.wasteMetrics.spoilageRate}% exceeds target of 10%`,
|
|
{ actionRequired: 'Improve cold chain or demand matching' }
|
|
);
|
|
}
|
|
|
|
if (this.foodMilesMetrics && this.foodMilesMetrics.localDeliveryPercent < 30) {
|
|
this.createAlert('info', 'Low Local Delivery Rate',
|
|
`Only ${this.foodMilesMetrics.localDeliveryPercent}% of deliveries are local (<50km)`,
|
|
{ actionRequired: 'Expand local grower network' }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for milestone achievements
|
|
*/
|
|
private checkMilestones(): void {
|
|
const milestones = [
|
|
{ carbon: 100, message: '100 kg CO2 saved!' },
|
|
{ carbon: 500, message: '500 kg CO2 saved!' },
|
|
{ carbon: 1000, message: '1 tonne CO2 saved!' },
|
|
{ carbon: 5000, message: '5 tonnes CO2 saved!' }
|
|
];
|
|
|
|
if (this.carbonMetrics) {
|
|
for (const milestone of milestones) {
|
|
const saved = this.carbonMetrics.savedVsConventionalKg;
|
|
if (saved >= milestone.carbon * 0.98 && saved <= milestone.carbon * 1.02) {
|
|
this.createAlert('info', 'Sustainability Milestone',
|
|
milestone.message,
|
|
{ relatedEntityType: 'network' }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate sustainability report
|
|
*/
|
|
generateReport(): SustainabilityReport {
|
|
const now = new Date();
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
return {
|
|
periodStart: weekAgo.toISOString(),
|
|
periodEnd: now.toISOString(),
|
|
totalCarbonSavedKg: this.carbonMetrics?.savedVsConventionalKg || 0,
|
|
totalFoodMilesSaved: this.foodMilesMetrics?.conventionalComparison || 0,
|
|
localProductionPercentage: this.foodMilesMetrics?.localDeliveryPercent || 0,
|
|
wasteReductionPercentage: this.wasteMetrics?.wasteReductionPercent || 0,
|
|
waterSavedLiters: this.waterMetrics?.savedLiters || 0,
|
|
recommendations: this.opportunities.slice(0, 3).map(o => o.title)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get carbon metrics
|
|
*/
|
|
getCarbonMetrics(): CarbonMetrics | null {
|
|
return this.carbonMetrics;
|
|
}
|
|
|
|
/**
|
|
* Get food miles metrics
|
|
*/
|
|
getFoodMilesMetrics(): FoodMilesMetrics | null {
|
|
return this.foodMilesMetrics;
|
|
}
|
|
|
|
/**
|
|
* Get water metrics
|
|
*/
|
|
getWaterMetrics(): WaterMetrics | null {
|
|
return this.waterMetrics;
|
|
}
|
|
|
|
/**
|
|
* Get waste metrics
|
|
*/
|
|
getWasteMetrics(): WasteMetrics | null {
|
|
return this.wasteMetrics;
|
|
}
|
|
|
|
/**
|
|
* Get sustainability score
|
|
*/
|
|
getSustainabilityScore(): SustainabilityScore | null {
|
|
return this.sustainabilityScore;
|
|
}
|
|
|
|
/**
|
|
* Get improvement opportunities
|
|
*/
|
|
getOpportunities(): ImprovementOpportunity[] {
|
|
return this.opportunities;
|
|
}
|
|
|
|
/**
|
|
* Get historical scores
|
|
*/
|
|
getHistoricalScores(): { date: string; score: number }[] {
|
|
return this.historicalScores;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let sustainabilityAgentInstance: SustainabilityAgent | null = null;
|
|
|
|
export function getSustainabilityAgent(): SustainabilityAgent {
|
|
if (!sustainabilityAgentInstance) {
|
|
sustainabilityAgentInstance = new SustainabilityAgent();
|
|
}
|
|
return sustainabilityAgentInstance;
|
|
}
|