/** * 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 = new Map(); private recommendations: Map = new Map(); private rotationAdvice: Map = new Map(); private opportunities: GrowingOpportunity[] = []; private performance: Map = new Map(); private seasonalAlerts: SeasonalAlert[] = []; // Crop knowledge base private cropData: Record = { '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 { // 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.getChain().slice(1); const ownerPlants = new Map(); 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(); 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++; } existing.yield += plant.plant.growthMetrics?.estimatedYieldKg || 2; 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(); const recommended = new Set(); 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 = { '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; }