File size: 14,190 Bytes
e2fa5a7
 
 
ee8289e
 
e2fa5a7
 
 
 
 
ee8289e
 
 
 
e2fa5a7
 
ee8289e
 
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
504a512
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e2fa5a7
ee8289e
 
 
 
 
 
 
 
 
e2fa5a7
 
 
ee8289e
 
 
 
 
 
 
 
 
 
 
 
e2fa5a7
 
ee8289e
 
 
 
e2fa5a7
 
 
ee8289e
e2fa5a7
 
 
 
 
 
 
 
 
ee8289e
 
 
e2fa5a7
 
 
 
 
 
ee8289e
e2fa5a7
ee8289e
e2fa5a7
ee8289e
 
 
 
 
 
 
 
e2fa5a7
 
 
 
ee8289e
e2fa5a7
ee8289e
 
 
 
e2fa5a7
 
 
 
 
 
 
 
ee8289e
 
e2fa5a7
 
ee8289e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e2fa5a7
 
 
 
 
ee8289e
e2fa5a7
 
 
ee8289e
e2fa5a7
 
ee8289e
e2fa5a7
 
 
 
 
ee8289e
 
 
 
e2fa5a7
 
ee8289e
504a512
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee8289e
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
"""
Intent Classifier for Agricultural Chatbot
==========================================
Detects user intent with sub-intents for priority context selection.
Matching Developer Specification categories.
"""

from typing import Dict, List, Tuple
import re

# =============================================================================
# INTENT PATTERNS - Based on Developer Spec
# =============================================================================

INTENT_PATTERNS = {
    "vegetation_health": {
        "keywords": [
            "yellow", "yellowing", "brown", "browning", "dying", "wilting", 
            "healthy", "health", "crop health", "plant health", "leaf", "leaves",
            "chlorophyll", "green", "greenness", "stunted", "weak", "pale",
            "ndvi", "evi", "vegetation", "biomass", "vigor", "canopy"
        ],
        "phrases": [
            "why is my crop", "is my crop healthy", "crop looks", 
            "plants look", "leaves turning", "plant dying"
        ],
        "sub_intents": ["chlorophyll_issue", "nutrient_deficiency", "general_health"],
        "priority": 1
    },
    
    "water_stress": {
        "keywords": [
            "water", "irrigation", "irrigate", "dry", "drought", "moisture",
            "thirsty", "watering", "rain", "wet", "smi", "ndwi", "soil moisture",
            "dehydrated", "wilt", "drooping", "crispy", "parched"
        ],
        "phrases": [
            "need water", "should i water", "need irrigation", "drought stress",
            "when to irrigate", "soil is dry", "field is dry"
        ],
        "sub_intents": ["drought_stress", "overwatering", "irrigation_timing"],
        "priority": 2
    },
    
    "nutrient_status": {
        "keywords": [
            "fertilizer", "nutrient", "nitrogen", "phosphorus", "potassium",
            "npk", "deficiency", "feeding", "feed", "ndre", "reci", "mcari",
            "urea", "dap", "mop", "manure", "compost", "micronutrient"
        ],
        "phrases": [
            "need fertilizer", "nutrient deficiency", "should i fertilize",
            "lacking nutrients", "nitrogen deficiency", "fertilizer amount"
        ],
        "sub_intents": ["nitrogen_deficiency", "nutrient_excess", "fertilizer_timing"],
        "priority": 3
    },
    
    "pest_disease": {
        "keywords": [
            "pest", "disease", "insect", "bug", "infection", "fungus",
            "blight", "rot", "spots", "holes", "eating", "aphid", "borer",
            "rust", "mildew", "virus", "bacteria", "infestation", "damage"
        ],
        "phrases": [
            "pest attack", "disease problem", "insect damage", "fungal infection",
            "what is eating", "spots on leaves", "pest risk"
        ],
        "sub_intents": ["pest_damage", "fungal_disease", "bacterial_issue", "viral_disease"],
        "priority": 4
    },
    
    "zone_specific": {
        "keywords": [
            "area", "zone", "patch", "section", "part", "corner", "side",
            "northeast", "northwest", "southeast", "southwest", "north", "south",
            "east", "west", "center", "edge", "boundary", "specific"
        ],
        "phrases": [
            "which area", "which zone", "which part", "where is the problem",
            "affected area", "problem zone", "specific area"
        ],
        "sub_intents": ["zone_diagnosis", "zone_comparison", "spatial_query"],
        "priority": 5
    },
    
    "forecast_query": {
        "keywords": [
            "forecast", "predict", "prediction", "future", "next week", "tomorrow",
            "will", "expect", "trend", "coming days", "upcoming", "projection",
            "growth", "yield", "estimate", "outlook"
        ],
        "phrases": [
            "what will happen", "next week", "in the future", "will my crop",
            "expected yield", "growth forecast", "weather forecast"
        ],
        "sub_intents": ["growth_forecast", "stress_prediction", "weather_impact", "yield_forecast"],
        "priority": 6
    },
    
    "action_recommendation": {
        "keywords": [
            "what should", "how to", "fix", "solve", "recommend", "advice",
            "help", "do", "action", "steps", "treatment", "remedy", "solution",
            "best practice", "suggestion", "improve"
        ],
        "phrases": [
            "what should i do", "how do i fix", "how to solve", "recommend",
            "give me advice", "best action", "immediate action"
        ],
        "sub_intents": ["immediate_action", "long_term_plan", "preventive_action"],
        "priority": 7
    },
    
    "comparison": {
        "keywords": [
            "compare", "comparison", "better", "worse", "change", "changed", 
            "difference", "last week", "before", "improvement", "decline",
            "progress", "regression", "historical", "trend"
        ],
        "phrases": [
            "compared to", "better than", "worse than", "has it improved",
            "how has it changed", "over time", "last month"
        ],
        "sub_intents": ["temporal_comparison", "zone_comparison", "historical_analysis"],
        "priority": 8
    },
    
    "field_comparison": {
        "keywords": [
            "compare", "comparison", "versus", " vs ", "other field", "another field",
            "between fields", "both fields", "which field", "differ", "different field",
            "my other", "second field", "first field"
        ],
        "phrases": [
            "compare with", "compared to my", "how does my * compare", "between my fields",
            "which field is better", "difference between", "compare * and *",
            "other farm", "other farmland", "another farm"
        ],
        "sub_intents": ["multi_field_analysis", "field_ranking", "relative_health"],
        "priority": 2
    },
    
    "general_query": {
        "keywords": [
            "what", "how", "why", "tell", "about", "explain", "hello", "hi",
            "information", "details", "overview", "status", "summary"
        ],
        "phrases": [
            "tell me about", "what is", "how does", "explain"
        ],
        "sub_intents": ["general_info"],
        "priority": 9
    }
}

# Hindi/regional language keywords (common agricultural terms)
REGIONAL_KEYWORDS = {
    "vegetation_health": ["पीला", "पत्ते", "सूखा", "मुरझाना"],
    "water_stress": ["पानी", "सिंचाई", "सूखा"],
    "nutrient_status": ["खाद", "यूरिया", "उर्वरक"],
    "pest_disease": ["कीट", "रोग", "कीड़ा"]
}


# =============================================================================
# INTENT CLASSIFIER
# =============================================================================

class IntentClassifier:
    """
    Classifies user queries into agricultural intent categories.
    Uses keyword matching, phrase matching, and confidence scoring.
    """
    
    def __init__(self):
        self.patterns = INTENT_PATTERNS
        self.regional = REGIONAL_KEYWORDS
    
    def classify(self, query: str) -> Dict:
        """
        Classify the intent of a user query.
        
        Returns:
            {
                "primary_intent": str,
                "sub_intents": List[str],
                "confidence": float (0.0-1.0),
                "matched_keywords": List[str],
                "all_intents": List[Tuple[str, float]]  # All detected intents with scores
            }
        """
        query_lower = query.lower()
        intent_scores = {}
        matched_keywords = {}
        
        # Score each intent
        for intent, config in self.patterns.items():
            score, matches = self._score_intent(query_lower, config)
            
            # Also check regional keywords
            if intent in self.regional:
                for kw in self.regional[intent]:
                    if kw in query:
                        score += 0.3
                        matches.append(kw)
            
            if score > 0:
                intent_scores[intent] = min(score, 1.0)
                matched_keywords[intent] = matches
        
        if not intent_scores:
            return self._default_response()
        
        # Sort intents by score
        sorted_intents = sorted(intent_scores.items(), key=lambda x: x[1], reverse=True)
        primary_intent = sorted_intents[0][0]
        confidence = sorted_intents[0][1]
        
        # Get sub-intents
        sub_intents = self._detect_sub_intents(query_lower, primary_intent)
        
        return {
            "primary_intent": primary_intent,
            "sub_intents": sub_intents,
            "confidence": round(confidence, 2),
            "matched_keywords": matched_keywords.get(primary_intent, []),
            "all_intents": [(intent, round(score, 2)) for intent, score in sorted_intents[:3]]
        }
    
    def _score_intent(self, query: str, config: Dict) -> Tuple[float, List[str]]:
        """Calculate score for a single intent."""
        score = 0.0
        matches = []
        
        keywords = config.get("keywords", [])
        phrases = config.get("phrases", [])
        
        # Check keyword matches
        for kw in keywords:
            if kw in query:
                score += 0.15
                matches.append(kw)
                # Bonus for exact word match (not substring)
                if re.search(rf'\b{re.escape(kw)}\b', query):
                    score += 0.05
        
        # Check phrase matches (higher score)
        for phrase in phrases:
            if phrase in query:
                score += 0.35
                matches.append(phrase)
        
        # Boost for multiple matches
        if len(matches) >= 3:
            score += 0.1
        
        return score, matches
    
    def _detect_sub_intents(self, query: str, primary_intent: str) -> List[str]:
        """Detect more specific sub-intents within the primary intent."""
        sub_intents = []
        config = self.patterns.get(primary_intent, {})
        
        # Add base sub-intent
        if config.get("sub_intents"):
            sub_intents.append(config["sub_intents"][0])
        
        # Detect question type
        if "why" in query:
            sub_intents.append("causal_analysis")
        if "how much" in query or "how many" in query or "quantity" in query:
            sub_intents.append("quantitative")
        if "when" in query:
            sub_intents.append("temporal")
        if "where" in query:
            sub_intents.append("spatial")
        if "should" in query or "recommend" in query:
            sub_intents.append("recommendation_needed")
        if "urgent" in query or "immediately" in query or "emergency" in query:
            sub_intents.append("urgent")
        
        return sub_intents if sub_intents else ["general"]
    
    def extract_field_names(self, query: str, available_fields: List[str]) -> List[str]:
        """
        Extract field names mentioned in the query.
        
        This enables dynamic field comparison - when a user mentions
        another field name, we can fetch data for that field and compare.
        
        Args:
            query: User's message
            available_fields: List of user's registered field names
            
        Returns:
            List of detected field names (in order of appearance)
        """
        if not available_fields:
            return []
        
        query_lower = query.lower()
        detected = []
        
        # Check each registered field name
        for field_name in available_fields:
            if not field_name:
                continue
            # Check if field name appears in query (case-insensitive)
            if field_name.lower() in query_lower:
                detected.append(field_name)
        
        # Also check for ordinal patterns like "field 1", "field 2", "first field"
        ordinal_map = {
            "first": 0, "1st": 0, "field 1": 0, "field one": 0,
            "second": 1, "2nd": 1, "field 2": 1, "field two": 1,
            "third": 2, "3rd": 2, "field 3": 2, "field three": 2
        }
        
        for pattern, idx in ordinal_map.items():
            if pattern in query_lower and idx < len(available_fields):
                field = available_fields[idx]
                if field not in detected:
                    detected.append(field)
        
        return detected
    
    def is_field_comparison_query(self, query: str, available_fields: List[str]) -> bool:
        """
        Check if the query is asking to compare multiple fields.
        
        Returns True if:
        1. Multiple field names are mentioned, OR
        2. Comparison keywords + at least one field name
        """
        intent = self.classify(query)
        mentioned_fields = self.extract_field_names(query, available_fields)
        
        # Multiple fields mentioned
        if len(mentioned_fields) >= 2:
            return True
        
        # Comparison intent + at least one field mentioned
        if intent["primary_intent"] in ["field_comparison", "comparison"]:
            if len(mentioned_fields) >= 1:
                return True
            # Check for phrases like "other field", "another farm"
            comparison_phrases = ["other field", "another field", "other farm", "another farm", 
                                  "my other", "between fields"]
            query_lower = query.lower()
            for phrase in comparison_phrases:
                if phrase in query_lower:
                    return True
        
        return False
    
    def _default_response(self) -> Dict:
        """Return default classification for unrecognized queries."""
        return {
            "primary_intent": "general_query",
            "sub_intents": ["general_info"],
            "confidence": 0.4,
            "matched_keywords": [],
            "all_intents": [("general_query", 0.4)]
        }
    
    def get_priority_for_intent(self, intent: str) -> int:
        """Get priority number for an intent."""
        return self.patterns.get(intent, {}).get("priority", 9)