""" Pydantic Models for Agricultural Chatbot ========================================= Structured data models matching the Developer Specification. """ from pydantic import BaseModel, Field from typing import Optional, List, Dict, Any from datetime import datetime # ============================================================================= # VEGETATION INDICES MODELS # ============================================================================= class IndexValue(BaseModel): """Single vegetation index with value and interpretation.""" current: Optional[float] = None trend_7d: Optional[float] = None trend_30d: Optional[float] = None min: Optional[float] = None max: Optional[float] = None interpretation: Optional[str] = None zone_values: Optional[Dict[str, float]] = None # NW, NE, SW, SE class VegetationIndices(BaseModel): """Complete vegetation indices data.""" NDVI: Optional[IndexValue] = None EVI: Optional[IndexValue] = None NDRE: Optional[IndexValue] = None RECI: Optional[IndexValue] = None NDWI: Optional[IndexValue] = None SMI: Optional[IndexValue] = None PSRI: Optional[IndexValue] = None PRI: Optional[IndexValue] = None MCARI: Optional[IndexValue] = None SOMI: Optional[IndexValue] = None SFI: Optional[IndexValue] = None # ============================================================================= # SAR DATA MODELS # ============================================================================= class SARBands(BaseModel): """SAR band data from Sentinel-1.""" VV: Optional[float] = None VH: Optional[float] = None VV_VH_ratio: Optional[float] = None VV_trend: Optional[str] = None # "increasing", "decreasing", "stable" VH_trend: Optional[str] = None interpretation: Optional[str] = None # ============================================================================= # CLUSTERING/STRESS MODELS # ============================================================================= class ClusterStats(BaseModel): """Statistics for a stress cluster.""" cluster_id: int num_patches: int percentage: float # % of field stress_score_mean: float stress_score_std: Optional[float] = None dominant_location: Optional[str] = None # "northeast", "southwest", etc. spectral_signature: Optional[Dict[str, float]] = None class AnomalyPatch(BaseModel): """Detected anomaly patch.""" patch_id: int location: Optional[str] = None anomaly_type: str # "spectral_outlier", "temporal_anomaly", etc. stress_score: float coordinates: Optional[List[float]] = None class ClusteringData(BaseModel): """All clustering and anomaly data.""" clusters: List[ClusterStats] = [] stressed_clusters: List[ClusterStats] = [] anomalies_detected: int = 0 anomaly_patches: List[AnomalyPatch] = [] # ============================================================================= # WEATHER MODELS # ============================================================================= class WeatherData(BaseModel): """Weather information.""" avg_temp_max: Optional[float] = None avg_temp_min: Optional[float] = None heat_stress_days: Optional[int] = None total_precipitation_mm: Optional[float] = None avg_humidity: Optional[float] = None consecutive_dry_days: Optional[int] = None heat_stress: bool = False drought_stress: bool = False class WeatherForecast(BaseModel): """Weather forecast data.""" rain_expected: bool = False rain_mm: Optional[float] = None temperature_trend: Optional[str] = None # ============================================================================= # FARMER CONTEXT MODELS # ============================================================================= class FarmerActions(BaseModel): """Farmer's recent actions.""" last_irrigation: Optional[str] = None # ISO date days_since_irrigation: Optional[int] = None last_fertilizer: Optional[str] = None days_since_fertilizer: Optional[int] = None last_spraying: Optional[str] = None notes: List[str] = [] class FarmerProfile(BaseModel): """Farmer profile information.""" role: Optional[str] = None years_farming: Optional[int] = None irrigation_method: Optional[str] = None farming_goal: Optional[str] = None # ============================================================================= # PRIORITY CONTEXT MODELS # ============================================================================= class Priority1Context(BaseModel): """Primary evidence - most diagnostic data.""" NDVI: Optional[Dict] = None EVI: Optional[Dict] = None NDRE: Optional[Dict] = None RECI: Optional[Dict] = None temporal_trends: Optional[Dict] = None class Priority2Context(BaseModel): """Supporting evidence - clusters, anomalies.""" clustering: Optional[Dict] = None anomalies: Optional[Dict] = None PSRI: Optional[Dict] = None class Priority3Context(BaseModel): """Causal factors - weather, soil, bands.""" weather: Optional[Dict] = None SMI: Optional[Dict] = None band_values: Optional[Dict] = None class Priority4Context(BaseModel): """Validation - SAR, previous analysis, farmer actions.""" SAR: Optional[Dict] = None previous_analysis: Optional[Dict] = None farmer_actions: Optional[Dict] = None # ============================================================================= # REASONING STAGE MODELS # ============================================================================= class ClaimStage(BaseModel): """Stage 3A: Initial claim output.""" initial_claim: str hypothesis: str evidence_cited: List[str] = [] confidence: float uncertainties: List[str] = [] class ValidateStage(BaseModel): """Stage 3B: Validation output.""" validation_result: str # "confirmed", "weakened", "neutral" confidence_updated: float spatial_notes: Optional[str] = None new_evidence_summary: Optional[str] = None class ContradictStage(BaseModel): """Stage 3C: Contradiction output.""" contradiction_found: bool contradicting_evidence: List[str] = [] alternative_hypothesis: Optional[str] = None alternative_confidence: float = 0.0 reasoning: Optional[str] = None class ConfirmStage(BaseModel): """Stage 3D: Confirmation output.""" final_diagnosis: str confidence: float causal_chain: Optional[str] = None root_cause: Optional[str] = None symptoms: List[str] = [] evidence_summary: Optional[Dict[str, List[str]]] = None recommendation: Optional[str] = None # ============================================================================= # REASONING TRACE MODELS # ============================================================================= class StageTrace(BaseModel): """Trace for a single reasoning stage.""" hypothesis: Optional[str] = None result: Optional[str] = None found: Optional[bool] = None alternative: Optional[str] = None final: Optional[str] = None confidence: float context_used: List[str] class ReasoningTrace(BaseModel): """Complete reasoning trace for transparency.""" intent_detected: str intent_confidence: float sub_intents: List[str] stages: Dict[str, StageTrace] causal_chain: Optional[str] = None evidence_summary: Dict[str, List[str]] class ContextPriorityUsed(BaseModel): """Which context was used at each priority level.""" priority_1: List[str] = [] priority_2: List[str] = [] priority_3: List[str] = [] priority_4: List[str] = [] # ============================================================================= # API REQUEST/RESPONSE MODELS # ============================================================================= class ChatRequest(BaseModel): """Chat request from Flutter app.""" session_id: str message: str user_id: Optional[str] = None field_context: Optional[Dict[str, Any]] = None field_name: Optional[str] = None # Optional specific field class ResponseContent(BaseModel): """Main response content.""" message: str confidence: float diagnosis: Optional[str] = None class ChatResponse(BaseModel): """Full chat response matching spec.""" response: ResponseContent session_id: str message_id: str timestamp: str reasoning_trace: Optional[ReasoningTrace] = None context_priority_used: Optional[ContextPriorityUsed] = None suggested_followups: List[str] = [] # ============================================================================= # FULL SATELLITE CONTEXT # ============================================================================= class FullSatelliteContext(BaseModel): """Complete satellite context for reasoning.""" field_info: Dict[str, Any] = {} vegetation_indices: Optional[VegetationIndices] = None sar_bands: Optional[SARBands] = None clustering: Optional[ClusteringData] = None weather: Optional[WeatherData] = None weather_forecast: Optional[WeatherForecast] = None farmer_actions: Optional[FarmerActions] = None previous_analysis: Optional[Dict[str, Any]] = None temporal_trends: Optional[Dict[str, Any]] = None band_values: Optional[Dict[str, float]] = None # ============================================================================= # HELPER FUNCTIONS # ============================================================================= def create_empty_response(session_id: str, message: str = "Unable to process") -> ChatResponse: """Create a fallback empty response.""" return ChatResponse( response=ResponseContent( message=message, confidence=0.0, diagnosis=None ), session_id=session_id, message_id="", timestamp=datetime.now().isoformat(), reasoning_trace=None, context_priority_used=None, suggested_followups=[] )