localgreenchain/lib/agents/GrowerAdvisoryAgent.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

657 lines
22 KiB
TypeScript

/**
* GrowerAdvisoryAgent
* Provides personalized recommendations to growers
*
* Responsibilities:
* - Generate planting recommendations based on demand
* - Provide crop rotation advice
* - Alert on optimal planting windows
* - Analyze market opportunities
* - Track grower performance metrics
*/
import { BaseAgent } from './BaseAgent';
import { AgentConfig, AgentTask, PlantingRecommendation } from './types';
import { getDemandForecaster } from '../demand/forecaster';
import { getBlockchain } from '../blockchain/manager';
interface GrowerProfile {
growerId: string;
growerName: string;
location: { latitude: number; longitude: number };
availableSpaceSqm: number;
specializations: string[];
certifications: string[];
experienceLevel: 'beginner' | 'intermediate' | 'expert';
preferredCrops: string[];
growingHistory: {
cropType: string;
successRate: number;
avgYield: number;
}[];
}
interface CropRecommendation {
id: string;
growerId: string;
cropType: string;
recommendedQuantity: number;
quantityUnit: 'sqm' | 'plants' | 'trays';
projectedYieldKg: number;
projectedRevenueUsd: number;
demandScore: number;
competitionLevel: 'low' | 'medium' | 'high';
riskLevel: 'low' | 'medium' | 'high';
plantingWindow: { start: string; end: string; optimal: string };
harvestWindow: { start: string; end: string };
reasoning: string[];
tips: string[];
priority: 'low' | 'medium' | 'high' | 'critical';
}
interface RotationAdvice {
growerId: string;
currentCrops: string[];
recommendedNext: string[];
avoidCrops: string[];
soilRestPeriod: number; // days
reasoning: string;
}
interface GrowingOpportunity {
id: string;
cropType: string;
demandGapKg: number;
currentSupplyKg: number;
pricePerKg: number;
windowCloses: string;
estimatedRevenue: number;
competitorCount: number;
successProbability: number;
}
interface GrowerPerformance {
growerId: string;
totalPlantsGrown: number;
successRate: number;
avgYieldPerSqm: number;
topCrops: { crop: string; count: number; successRate: number }[];
carbonFootprintKg: number;
localDeliveryPercent: number;
customerSatisfaction: number;
trend: 'improving' | 'stable' | 'declining';
}
interface SeasonalAlert {
id: string;
alertType: 'planting_window' | 'harvest_time' | 'frost_warning' | 'demand_spike' | 'price_change';
cropType: string;
message: string;
actionRequired: string;
deadline?: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
}
export class GrowerAdvisoryAgent extends BaseAgent {
private growerProfiles: Map<string, GrowerProfile> = new Map();
private recommendations: Map<string, CropRecommendation[]> = new Map();
private rotationAdvice: Map<string, RotationAdvice> = new Map();
private opportunities: GrowingOpportunity[] = [];
private performance: Map<string, GrowerPerformance> = new Map();
private seasonalAlerts: SeasonalAlert[] = [];
// Crop knowledge base
private cropData: Record<string, {
growingDays: number;
yieldPerSqm: number;
seasons: string[];
companions: string[];
avoid: string[];
difficulty: 'easy' | 'moderate' | 'challenging';
}> = {
'lettuce': { growingDays: 45, yieldPerSqm: 4, seasons: ['spring', 'fall'], companions: ['carrot', 'radish'], avoid: ['celery'], difficulty: 'easy' },
'tomato': { growingDays: 80, yieldPerSqm: 8, seasons: ['summer'], companions: ['basil', 'carrot'], avoid: ['brassicas'], difficulty: 'moderate' },
'spinach': { growingDays: 40, yieldPerSqm: 3, seasons: ['spring', 'fall', 'winter'], companions: ['strawberry', 'pea'], avoid: [], difficulty: 'easy' },
'kale': { growingDays: 55, yieldPerSqm: 3.5, seasons: ['spring', 'fall', 'winter'], companions: ['onion', 'beet'], avoid: ['strawberry'], difficulty: 'easy' },
'basil': { growingDays: 30, yieldPerSqm: 2, seasons: ['spring', 'summer'], companions: ['tomato', 'pepper'], avoid: ['sage'], difficulty: 'easy' },
'pepper': { growingDays: 75, yieldPerSqm: 6, seasons: ['summer'], companions: ['basil', 'carrot'], avoid: ['fennel'], difficulty: 'moderate' },
'cucumber': { growingDays: 60, yieldPerSqm: 10, seasons: ['summer'], companions: ['bean', 'pea'], avoid: ['potato'], difficulty: 'moderate' },
'carrot': { growingDays: 70, yieldPerSqm: 5, seasons: ['spring', 'fall'], companions: ['onion', 'lettuce'], avoid: ['dill'], difficulty: 'easy' },
'microgreens': { growingDays: 14, yieldPerSqm: 1.5, seasons: ['spring', 'summer', 'fall', 'winter'], companions: [], avoid: [], difficulty: 'easy' },
'strawberry': { growingDays: 90, yieldPerSqm: 3, seasons: ['spring', 'summer'], companions: ['spinach', 'lettuce'], avoid: ['brassicas'], difficulty: 'moderate' }
};
constructor() {
const config: AgentConfig = {
id: 'grower-advisory-agent',
name: 'Grower Advisory Agent',
description: 'Provides personalized growing recommendations',
enabled: true,
intervalMs: 300000, // Run every 5 minutes
priority: 'high',
maxRetries: 3,
timeoutMs: 60000
};
super(config);
}
/**
* Main execution cycle
*/
async runOnce(): Promise<AgentTask | null> {
// Load/update grower profiles from blockchain
this.updateGrowerProfiles();
// Generate recommendations for each grower
for (const [growerId] of this.growerProfiles) {
const recs = this.generateRecommendations(growerId);
this.recommendations.set(growerId, recs);
// Generate rotation advice
const rotation = this.generateRotationAdvice(growerId);
this.rotationAdvice.set(growerId, rotation);
// Update performance metrics
const perf = this.calculatePerformance(growerId);
this.performance.set(growerId, perf);
}
// Identify market opportunities
this.opportunities = this.findOpportunities();
// Generate seasonal alerts
this.seasonalAlerts = this.generateSeasonalAlerts();
// Alert growers about urgent opportunities
this.notifyUrgentOpportunities();
return this.createTaskResult('grower_advisory', 'completed', {
growersAdvised: this.growerProfiles.size,
recommendationsGenerated: Array.from(this.recommendations.values()).flat().length,
opportunitiesIdentified: this.opportunities.length,
alertsGenerated: this.seasonalAlerts.length
});
}
/**
* Update grower profiles from blockchain data
*/
private updateGrowerProfiles(): void {
const blockchain = getBlockchain();
const chain = blockchain.chain.slice(1);
const ownerPlants = new Map<string, typeof chain>();
for (const block of chain) {
const ownerId = block.plant.owner?.id;
if (!ownerId) continue;
const plants = ownerPlants.get(ownerId) || [];
plants.push(block);
ownerPlants.set(ownerId, plants);
}
for (const [ownerId, plants] of ownerPlants) {
// Only consider active growers (>2 plants)
if (plants.length < 3) continue;
const latestPlant = plants[plants.length - 1];
const cropTypes = [...new Set(plants.map(p => p.plant.commonName).filter(Boolean))];
// Calculate success rate
const healthyPlants = plants.filter(p =>
['growing', 'mature', 'flowering', 'fruiting'].includes(p.plant.status)
).length;
const successRate = (healthyPlants / plants.length) * 100;
// Determine experience level
let experienceLevel: GrowerProfile['experienceLevel'];
if (plants.length > 50 && successRate > 80) experienceLevel = 'expert';
else if (plants.length > 10 && successRate > 60) experienceLevel = 'intermediate';
else experienceLevel = 'beginner';
// Build growing history
const historyMap = new Map<string, { total: number; healthy: number; yield: number }>();
for (const plant of plants) {
const crop = plant.plant.commonName || 'unknown';
const existing = historyMap.get(crop) || { total: 0, healthy: 0, yield: 0 };
existing.total++;
if (['growing', 'mature', 'flowering', 'fruiting'].includes(plant.plant.status)) {
existing.healthy++;
}
// Estimate yield based on health score, or use default of 2kg
const healthMultiplier = plant.plant.growthMetrics?.healthScore
? plant.plant.growthMetrics.healthScore / 50
: 1;
existing.yield += 2 * healthMultiplier;
historyMap.set(crop, existing);
}
const growingHistory = Array.from(historyMap.entries()).map(([cropType, data]) => ({
cropType,
successRate: Math.round((data.healthy / data.total) * 100),
avgYield: Math.round((data.yield / data.total) * 10) / 10
}));
const profile: GrowerProfile = {
growerId: ownerId,
growerName: latestPlant.plant.owner?.name || 'Unknown',
location: latestPlant.plant.location,
availableSpaceSqm: Math.max(10, plants.length * 2), // Estimate
specializations: cropTypes.slice(0, 3) as string[],
certifications: [],
experienceLevel,
preferredCrops: cropTypes.slice(0, 5) as string[],
growingHistory
};
this.growerProfiles.set(ownerId, profile);
}
}
/**
* Generate personalized recommendations
*/
private generateRecommendations(growerId: string): CropRecommendation[] {
const profile = this.growerProfiles.get(growerId);
if (!profile) return [];
const recommendations: CropRecommendation[] = [];
const currentSeason = this.getCurrentSeason();
const forecaster = getDemandForecaster();
// Get demand signal for grower's region
const signal = forecaster.generateDemandSignal(
profile.location.latitude,
profile.location.longitude,
50, // 50km radius
'Local Region',
currentSeason
);
// Find crops with demand gaps
const demandGaps = signal.demandItems.filter(item => item.gapKg > 10);
for (const demandItem of demandGaps.slice(0, 5)) {
const cropData = this.cropData[demandItem.produceType.toLowerCase()];
if (!cropData) continue;
// Check if in season
if (!cropData.seasons.includes(currentSeason)) continue;
// Check grower's history with this crop
const history = profile.growingHistory.find(h =>
h.cropType.toLowerCase() === demandItem.produceType.toLowerCase()
);
// Calculate recommended quantity
const spaceForCrop = Math.min(
profile.availableSpaceSqm * 0.3, // Max 30% of space per crop
demandItem.gapKg / cropData.yieldPerSqm
);
// Calculate risk level
let riskLevel: 'low' | 'medium' | 'high';
if (history && history.successRate > 80) riskLevel = 'low';
else if (history && history.successRate > 50) riskLevel = 'medium';
else if (!history && cropData.difficulty === 'challenging') riskLevel = 'high';
else riskLevel = 'medium';
// Calculate planting window
const now = new Date();
const plantStart = new Date(now);
const plantOptimal = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const plantEnd = new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000);
const harvestStart = new Date(plantOptimal.getTime() + cropData.growingDays * 24 * 60 * 60 * 1000);
const harvestEnd = new Date(harvestStart.getTime() + 14 * 24 * 60 * 60 * 1000);
const projectedYield = spaceForCrop * cropData.yieldPerSqm;
const projectedRevenue = projectedYield * demandItem.averageWillingPrice;
recommendations.push({
id: `rec-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
growerId,
cropType: demandItem.produceType,
recommendedQuantity: Math.round(spaceForCrop * 10) / 10,
quantityUnit: 'sqm',
projectedYieldKg: Math.round(projectedYield * 10) / 10,
projectedRevenueUsd: Math.round(projectedRevenue * 100) / 100,
demandScore: demandItem.aggregatePriority * 10,
competitionLevel: demandItem.matchedGrowers < 3 ? 'low' :
demandItem.matchedGrowers < 6 ? 'medium' : 'high',
riskLevel,
plantingWindow: {
start: plantStart.toISOString(),
end: plantEnd.toISOString(),
optimal: plantOptimal.toISOString()
},
harvestWindow: {
start: harvestStart.toISOString(),
end: harvestEnd.toISOString()
},
reasoning: [
`${Math.round(demandItem.gapKg)}kg weekly demand gap in your area`,
history ? `Your success rate: ${history.successRate}%` : 'New crop opportunity',
`${demandItem.matchedGrowers} other growers currently supplying`
],
tips: this.generateGrowingTips(demandItem.produceType, profile.experienceLevel),
priority: demandItem.urgency === 'immediate' ? 'critical' :
demandItem.urgency === 'this_week' ? 'high' :
demandItem.urgency === 'this_month' ? 'medium' : 'low'
});
}
return recommendations.sort((a, b) =>
a.priority === 'critical' ? -1 : b.priority === 'critical' ? 1 :
a.priority === 'high' ? -1 : b.priority === 'high' ? 1 : 0
);
}
/**
* Generate crop-specific growing tips
*/
private generateGrowingTips(cropType: string, experienceLevel: string): string[] {
const tips: string[] = [];
const crop = this.cropData[cropType.toLowerCase()];
if (!crop) return ['Research growing requirements before planting'];
if (experienceLevel === 'beginner') {
tips.push(`${cropType} takes approximately ${crop.growingDays} days to harvest`);
if (crop.difficulty === 'easy') {
tips.push('Great choice for beginners!');
}
}
if (crop.companions.length > 0) {
tips.push(`Good companions: ${crop.companions.join(', ')}`);
}
if (crop.avoid.length > 0) {
tips.push(`Avoid planting near: ${crop.avoid.join(', ')}`);
}
tips.push(`Expected yield: ${crop.yieldPerSqm}kg per sqm`);
return tips.slice(0, 4);
}
/**
* Generate rotation advice
*/
private generateRotationAdvice(growerId: string): RotationAdvice {
const profile = this.growerProfiles.get(growerId);
if (!profile) {
return {
growerId,
currentCrops: [],
recommendedNext: [],
avoidCrops: [],
soilRestPeriod: 0,
reasoning: 'No growing history available'
};
}
const currentCrops = profile.specializations;
const avoid = new Set<string>();
const recommended = new Set<string>();
for (const current of currentCrops) {
const cropData = this.cropData[current.toLowerCase()];
if (cropData) {
cropData.avoid.forEach(c => avoid.add(c));
cropData.companions.forEach(c => recommended.add(c));
}
}
// Don't recommend crops already being grown
currentCrops.forEach(c => recommended.delete(c.toLowerCase()));
return {
growerId,
currentCrops,
recommendedNext: Array.from(recommended).slice(0, 5),
avoidCrops: Array.from(avoid),
soilRestPeriod: currentCrops.includes('tomato') || currentCrops.includes('pepper') ? 30 : 14,
reasoning: `Based on ${currentCrops.length} current crops and companion planting principles`
};
}
/**
* Calculate grower performance metrics
*/
private calculatePerformance(growerId: string): GrowerPerformance {
const profile = this.growerProfiles.get(growerId);
if (!profile) {
return {
growerId,
totalPlantsGrown: 0,
successRate: 0,
avgYieldPerSqm: 0,
topCrops: [],
carbonFootprintKg: 0,
localDeliveryPercent: 100,
customerSatisfaction: 0,
trend: 'stable'
};
}
const totalPlants = profile.growingHistory.reduce((sum, h) => sum + h.avgYield * 2, 0);
const avgSuccess = profile.growingHistory.length > 0
? profile.growingHistory.reduce((sum, h) => sum + h.successRate, 0) / profile.growingHistory.length
: 0;
const topCrops = profile.growingHistory
.sort((a, b) => b.avgYield - a.avgYield)
.slice(0, 3)
.map(h => ({ crop: h.cropType, count: Math.round(h.avgYield * 2), successRate: h.successRate }));
return {
growerId,
totalPlantsGrown: Math.round(totalPlants),
successRate: Math.round(avgSuccess),
avgYieldPerSqm: profile.growingHistory.length > 0
? Math.round(profile.growingHistory.reduce((sum, h) => sum + h.avgYield, 0) / profile.growingHistory.length * 10) / 10
: 0,
topCrops,
carbonFootprintKg: Math.round(totalPlants * 0.1 * 10) / 10, // Estimate
localDeliveryPercent: 85, // Estimate
customerSatisfaction: Math.min(100, 60 + avgSuccess * 0.4),
trend: avgSuccess > 75 ? 'improving' : avgSuccess < 50 ? 'declining' : 'stable'
};
}
/**
* Find market opportunities
*/
private findOpportunities(): GrowingOpportunity[] {
const opportunities: GrowingOpportunity[] = [];
const forecaster = getDemandForecaster();
const currentSeason = this.getCurrentSeason();
// Check multiple regions
const regions = [
{ lat: 40.7128, lon: -74.0060, name: 'Metro' },
{ lat: 40.85, lon: -73.95, name: 'North' },
{ lat: 40.55, lon: -74.15, name: 'South' }
];
for (const region of regions) {
const signal = forecaster.generateDemandSignal(
region.lat, region.lon, 50, region.name, currentSeason
);
for (const item of signal.demandItems) {
if (item.gapKg > 20) {
opportunities.push({
id: `opp-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
cropType: item.produceType,
demandGapKg: item.gapKg,
currentSupplyKg: item.matchedSupply,
pricePerKg: item.averageWillingPrice,
windowCloses: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
estimatedRevenue: item.gapKg * item.averageWillingPrice,
competitorCount: item.matchedGrowers,
successProbability: item.inSeason ? 0.8 : 0.5
});
}
}
}
return opportunities.sort((a, b) => b.estimatedRevenue - a.estimatedRevenue).slice(0, 20);
}
/**
* Generate seasonal alerts
*/
private generateSeasonalAlerts(): SeasonalAlert[] {
const alerts: SeasonalAlert[] = [];
const currentSeason = this.getCurrentSeason();
const nextSeason = this.getNextSeason(currentSeason);
// Planting window alerts
for (const [crop, data] of Object.entries(this.cropData)) {
if (data.seasons.includes(nextSeason) && !data.seasons.includes(currentSeason)) {
alerts.push({
id: `alert-${Date.now()}-${crop}`,
alertType: 'planting_window',
cropType: crop,
message: `${crop} planting season approaching - prepare to plant in ${nextSeason}`,
actionRequired: 'Order seeds and prepare growing area',
deadline: this.getSeasonStartDate(nextSeason),
priority: 'medium'
});
}
}
// Demand spike alerts
for (const opp of this.opportunities.slice(0, 3)) {
if (opp.demandGapKg > 100) {
alerts.push({
id: `alert-${Date.now()}-demand-${opp.cropType}`,
alertType: 'demand_spike',
cropType: opp.cropType,
message: `High demand for ${opp.cropType}: ${Math.round(opp.demandGapKg)}kg weekly gap`,
actionRequired: 'Consider expanding production',
priority: 'high'
});
}
}
return alerts;
}
/**
* Notify growers about urgent opportunities
*/
private notifyUrgentOpportunities(): void {
const urgentOpps = this.opportunities.filter(o =>
o.estimatedRevenue > 500 && o.competitorCount < 3
);
for (const opp of urgentOpps.slice(0, 3)) {
this.createAlert('info', `Growing Opportunity: ${opp.cropType}`,
`${Math.round(opp.demandGapKg)}kg demand gap, estimated $${Math.round(opp.estimatedRevenue)} revenue`,
{
actionRequired: `Consider planting ${opp.cropType}`,
relatedEntityType: 'opportunity'
}
);
}
}
/**
* Get current season
*/
private getCurrentSeason(): 'spring' | 'summer' | 'fall' | 'winter' {
const month = new Date().getMonth();
if (month >= 2 && month <= 4) return 'spring';
if (month >= 5 && month <= 7) return 'summer';
if (month >= 8 && month <= 10) return 'fall';
return 'winter';
}
/**
* Get next season
*/
private getNextSeason(current: string): 'spring' | 'summer' | 'fall' | 'winter' {
const order = ['spring', 'summer', 'fall', 'winter'];
const idx = order.indexOf(current);
return order[(idx + 1) % 4] as any;
}
/**
* Get season start date
*/
private getSeasonStartDate(season: string): string {
const year = new Date().getFullYear();
const dates: Record<string, string> = {
'spring': `${year}-03-20`,
'summer': `${year}-06-21`,
'fall': `${year}-09-22`,
'winter': `${year}-12-21`
};
return dates[season] || dates['spring'];
}
/**
* Register a grower profile
*/
registerGrowerProfile(profile: GrowerProfile): void {
this.growerProfiles.set(profile.growerId, profile);
}
/**
* Get grower profile
*/
getGrowerProfile(growerId: string): GrowerProfile | null {
return this.growerProfiles.get(growerId) || null;
}
/**
* Get recommendations for a grower
*/
getRecommendations(growerId: string): CropRecommendation[] {
return this.recommendations.get(growerId) || [];
}
/**
* Get rotation advice for a grower
*/
getRotationAdvice(growerId: string): RotationAdvice | null {
return this.rotationAdvice.get(growerId) || null;
}
/**
* Get market opportunities
*/
getOpportunities(): GrowingOpportunity[] {
return this.opportunities;
}
/**
* Get grower performance
*/
getPerformance(growerId: string): GrowerPerformance | null {
return this.performance.get(growerId) || null;
}
/**
* Get seasonal alerts
*/
getSeasonalAlerts(): SeasonalAlert[] {
return this.seasonalAlerts;
}
}
// Singleton instance
let growerAgentInstance: GrowerAdvisoryAgent | null = null;
export function getGrowerAdvisoryAgent(): GrowerAdvisoryAgent {
if (!growerAgentInstance) {
growerAgentInstance = new GrowerAdvisoryAgent();
}
return growerAgentInstance;
}