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
653 lines
22 KiB
TypeScript
653 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.getChain().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++;
|
|
}
|
|
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<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;
|
|
}
|