Aniket2006 commited on
Commit
e2fa5a7
·
1 Parent(s): 07a23c0

Implement multi-stage reasoning architecture (Claim→Validate→Contradict→Confirm)

Browse files
Files changed (5) hide show
  1. app.py +116 -112
  2. intent_classifier.py +138 -0
  3. priority_mapper.py +165 -0
  4. prompts.py +171 -96
  5. reasoning_engine.py +374 -0
app.py CHANGED
@@ -1,10 +1,10 @@
1
  """
2
  AGROW Agricultural Chatbot Service
3
  ===================================
4
- AI-powered agricultural advisor using Gemini LLM with:
5
- - Context from pipeline outputs (stress, NDVI, forecasts)
 
6
  - Supabase conversation storage
7
- - Session management
8
  """
9
 
10
  import os
@@ -21,7 +21,8 @@ from fastapi.middleware.cors import CORSMiddleware
21
  from pydantic import BaseModel
22
 
23
  from supabase_client import SupabaseClient
24
- from prompts import SYSTEM_PROMPT, build_context_prompt
 
25
 
26
  # ============================================================================
27
  # LOGGING
@@ -53,17 +54,15 @@ def get_available_model():
53
  return None, None
54
 
55
  for model in models_to_try:
56
- # Try v1beta first, then v1
57
  for version in ["v1beta", "v1"]:
58
  url = f"https://generativelanguage.googleapis.com/{version}/models/{model}:generateContent"
59
  try:
60
- import requests
61
  resp = requests.post(
62
  f"{url}?key={GEMINI_API_KEY}",
63
  json={"contents": [{"parts": [{"text": "test"}]}]},
64
  timeout=10
65
  )
66
- if resp.status_code in [200, 429]: # 200=OK, 429=rate limit (but model exists)
67
  logger.info(f"Found working model: {model} on {version}")
68
  return url, model
69
  except:
@@ -71,7 +70,6 @@ def get_available_model():
71
 
72
  return None, None
73
 
74
- # Find working model at startup
75
  GEMINI_URL, GEMINI_MODEL = get_available_model()
76
  if GEMINI_URL:
77
  logger.info(f"Using Gemini model: {GEMINI_MODEL}")
@@ -88,59 +86,7 @@ else:
88
  supabase = SupabaseClient()
89
 
90
  # ============================================================================
91
- # FASTAPI
92
- # ============================================================================
93
- app = FastAPI(
94
- title="AGROW Chatbot Service",
95
- description="AI agricultural advisor with conversation storage",
96
- version="1.0.0"
97
- )
98
-
99
- app.add_middleware(
100
- CORSMiddleware,
101
- allow_origins=["*"],
102
- allow_credentials=True,
103
- allow_methods=["*"],
104
- allow_headers=["*"],
105
- )
106
-
107
- # ============================================================================
108
- # REQUEST/RESPONSE MODELS
109
- # ============================================================================
110
- class ChatRequest(BaseModel):
111
- session_id: str
112
- message: str
113
- user_id: Optional[str] = None
114
- field_context: Optional[Dict[str, Any]] = None
115
-
116
- class ChatResponse(BaseModel):
117
- response: str
118
- session_id: str
119
- message_id: str
120
- context_used: List[str]
121
- timestamp: str
122
-
123
- class SessionRequest(BaseModel):
124
- user_id: str
125
- title: Optional[str] = None
126
-
127
- class SessionResponse(BaseModel):
128
- session_id: str
129
- title: str
130
- created_at: str
131
-
132
- class MessageModel(BaseModel):
133
- id: str
134
- role: str
135
- content: str
136
- created_at: str
137
-
138
- class HistoryResponse(BaseModel):
139
- session_id: str
140
- messages: List[MessageModel]
141
-
142
- # ============================================================================
143
- # HELPER FUNCTIONS
144
  # ============================================================================
145
  def call_gemini_api(prompt: str) -> str:
146
  """Call Gemini API directly using REST with retry logic."""
@@ -148,10 +94,9 @@ def call_gemini_api(prompt: str) -> str:
148
  return "Please configure GEMINI_API_KEY for real responses."
149
 
150
  url = f"{GEMINI_URL}?key={GEMINI_API_KEY}"
151
- logger.info(f"Calling Gemini API: {GEMINI_URL}")
152
 
153
  max_retries = 3
154
- retry_delay = 2 # seconds
155
 
156
  for attempt in range(max_retries):
157
  try:
@@ -164,31 +109,27 @@ def call_gemini_api(prompt: str) -> str:
164
  }],
165
  "generationConfig": {
166
  "temperature": 0.7,
167
- "maxOutputTokens": 1024,
168
  }
169
  },
170
- timeout=30
171
  )
172
 
173
- logger.info(f"Gemini response status: {response.status_code}")
174
-
175
  if response.status_code == 200:
176
  data = response.json()
177
  if "candidates" in data and len(data["candidates"]) > 0:
178
  return data["candidates"][0]["content"]["parts"][0]["text"]
179
  return "No response generated."
180
  elif response.status_code == 429:
181
- # Rate limit - wait and retry
182
  if attempt < max_retries - 1:
183
  import time
184
  wait_time = retry_delay * (2 ** attempt)
185
- logger.warning(f"Rate limited, waiting {wait_time}s before retry...")
186
  time.sleep(wait_time)
187
  continue
188
  return "I'm currently busy. Please try again in a moment."
189
  else:
190
- error_msg = response.text[:500]
191
- logger.error(f"Gemini API error: {response.status_code} - {error_msg}")
192
  return f"API error: {response.status_code}"
193
 
194
  except Exception as e:
@@ -200,40 +141,62 @@ def call_gemini_api(prompt: str) -> str:
200
  return "Failed after retries. Please try again."
201
 
202
 
203
- def generate_response(user_message: str, history: List[Dict], context: Optional[Dict] = None) -> tuple[str, List[str]]:
204
- """Generate AI response using Gemini."""
205
- context_used = []
206
-
207
- # Build context prompt if pipeline data available
208
- context_prompt = ""
209
- if context:
210
- context_prompt = build_context_prompt(context)
211
- context_used = list(context.keys())
212
-
213
- # Combine system prompt with context
214
- full_system = SYSTEM_PROMPT
215
- if context_prompt:
216
- full_system += f"\n\n## Current Field Analysis:\n{context_prompt}"
217
-
218
- # Build conversation context
219
- history_text = ""
220
- for msg in history[-6:]:
221
- role = "User" if msg.get("role") == "user" else "Assistant"
222
- history_text += f"{role}: {msg.get('content', '')}\n"
223
-
224
- # Create full prompt
225
- full_prompt = f"""{full_system}
226
 
227
- ## Previous Conversation:
228
- {history_text}
 
 
 
 
 
 
229
 
230
- ## Current Question:
231
- User: {user_message}
 
 
 
 
 
232
 
233
- ## Your Response (as AGROW AI agricultural advisor):"""
234
-
235
- response_text = call_gemini_api(full_prompt)
236
- return response_text, context_used
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
  # ============================================================================
239
  # API ENDPOINTS
@@ -242,9 +205,10 @@ User: {user_message}
242
  async def root():
243
  return {
244
  "service": "AGROW Chatbot Service",
245
- "version": "1.0.0",
 
246
  "endpoints": {
247
- "/chat": "POST - Send message, get AI response",
248
  "/session/new": "POST - Create new chat session",
249
  "/session/{id}/history": "GET - Get conversation history",
250
  "/sessions/{user_id}": "GET - List user's sessions"
@@ -256,6 +220,7 @@ async def health():
256
  return {
257
  "status": "healthy",
258
  "gemini_configured": GEMINI_API_KEY is not None,
 
259
  "supabase_configured": supabase.is_configured()
260
  }
261
 
@@ -283,7 +248,7 @@ async def create_session(request: SessionRequest):
283
 
284
  @app.post("/chat", response_model=ChatResponse)
285
  async def chat(request: ChatRequest):
286
- """Send a message and get AI response."""
287
  logger.info(f"Chat request - Session: {request.session_id}, Message: {request.message[:50]}...")
288
 
289
  try:
@@ -297,13 +262,39 @@ async def chat(request: ChatRequest):
297
  content=request.message
298
  )
299
 
300
- # Generate AI response
301
- response_text, context_used = generate_response(
302
- request.message,
303
- history,
304
- request.field_context
 
 
 
 
305
  )
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  # Save assistant response
308
  assistant_msg_id = supabase.add_message(
309
  session_id=request.session_id,
@@ -322,7 +313,8 @@ async def chat(request: ChatRequest):
322
  session_id=request.session_id,
323
  message_id=assistant_msg_id,
324
  context_used=context_used,
325
- timestamp=datetime.now().isoformat()
 
326
  )
327
 
328
  except Exception as e:
@@ -387,7 +379,19 @@ async def delete_session(session_id: str):
387
  raise HTTPException(500, str(e))
388
 
389
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  if __name__ == "__main__":
391
  import uvicorn
392
- logger.info("Starting AGROW Chatbot Service")
393
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
  AGROW Agricultural Chatbot Service
3
  ===================================
4
+ AI-powered agricultural advisor with:
5
+ - Multi-stage reasoning (Claim Validate Contradict → Confirm)
6
+ - Priority-based context selection
7
  - Supabase conversation storage
 
8
  """
9
 
10
  import os
 
21
  from pydantic import BaseModel
22
 
23
  from supabase_client import SupabaseClient
24
+ from reasoning_engine import ReasoningEngine, simple_reason
25
+ from intent_classifier import IntentClassifier
26
 
27
  # ============================================================================
28
  # LOGGING
 
54
  return None, None
55
 
56
  for model in models_to_try:
 
57
  for version in ["v1beta", "v1"]:
58
  url = f"https://generativelanguage.googleapis.com/{version}/models/{model}:generateContent"
59
  try:
 
60
  resp = requests.post(
61
  f"{url}?key={GEMINI_API_KEY}",
62
  json={"contents": [{"parts": [{"text": "test"}]}]},
63
  timeout=10
64
  )
65
+ if resp.status_code in [200, 429]:
66
  logger.info(f"Found working model: {model} on {version}")
67
  return url, model
68
  except:
 
70
 
71
  return None, None
72
 
 
73
  GEMINI_URL, GEMINI_MODEL = get_available_model()
74
  if GEMINI_URL:
75
  logger.info(f"Using Gemini model: {GEMINI_MODEL}")
 
86
  supabase = SupabaseClient()
87
 
88
  # ============================================================================
89
+ # LLM CALLER
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  # ============================================================================
91
  def call_gemini_api(prompt: str) -> str:
92
  """Call Gemini API directly using REST with retry logic."""
 
94
  return "Please configure GEMINI_API_KEY for real responses."
95
 
96
  url = f"{GEMINI_URL}?key={GEMINI_API_KEY}"
 
97
 
98
  max_retries = 3
99
+ retry_delay = 2
100
 
101
  for attempt in range(max_retries):
102
  try:
 
109
  }],
110
  "generationConfig": {
111
  "temperature": 0.7,
112
+ "maxOutputTokens": 2048,
113
  }
114
  },
115
+ timeout=60
116
  )
117
 
 
 
118
  if response.status_code == 200:
119
  data = response.json()
120
  if "candidates" in data and len(data["candidates"]) > 0:
121
  return data["candidates"][0]["content"]["parts"][0]["text"]
122
  return "No response generated."
123
  elif response.status_code == 429:
 
124
  if attempt < max_retries - 1:
125
  import time
126
  wait_time = retry_delay * (2 ** attempt)
127
+ logger.warning(f"Rate limited, waiting {wait_time}s...")
128
  time.sleep(wait_time)
129
  continue
130
  return "I'm currently busy. Please try again in a moment."
131
  else:
132
+ logger.error(f"Gemini API error: {response.status_code}")
 
133
  return f"API error: {response.status_code}"
134
 
135
  except Exception as e:
 
141
  return "Failed after retries. Please try again."
142
 
143
 
144
+ # Initialize reasoning engine
145
+ reasoning_engine = ReasoningEngine(llm_caller=call_gemini_api)
146
+ intent_classifier = IntentClassifier()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ # ============================================================================
149
+ # FASTAPI
150
+ # ============================================================================
151
+ app = FastAPI(
152
+ title="AGROW Chatbot Service",
153
+ description="AI agricultural advisor with multi-stage reasoning",
154
+ version="2.0.0"
155
+ )
156
 
157
+ app.add_middleware(
158
+ CORSMiddleware,
159
+ allow_origins=["*"],
160
+ allow_credentials=True,
161
+ allow_methods=["*"],
162
+ allow_headers=["*"],
163
+ )
164
 
165
+ # ============================================================================
166
+ # REQUEST/RESPONSE MODELS
167
+ # ============================================================================
168
+ class ChatRequest(BaseModel):
169
+ session_id: str
170
+ message: str
171
+ user_id: Optional[str] = None
172
+ field_context: Optional[Dict[str, Any]] = None
173
+
174
+ class ChatResponse(BaseModel):
175
+ response: str
176
+ session_id: str
177
+ message_id: str
178
+ context_used: List[str]
179
+ timestamp: str
180
+ reasoning_trace: Optional[Dict[str, Any]] = None
181
+
182
+ class SessionRequest(BaseModel):
183
+ user_id: str
184
+ title: Optional[str] = None
185
+
186
+ class SessionResponse(BaseModel):
187
+ session_id: str
188
+ title: str
189
+ created_at: str
190
+
191
+ class MessageModel(BaseModel):
192
+ id: str
193
+ role: str
194
+ content: str
195
+ created_at: str
196
+
197
+ class HistoryResponse(BaseModel):
198
+ session_id: str
199
+ messages: List[MessageModel]
200
 
201
  # ============================================================================
202
  # API ENDPOINTS
 
205
  async def root():
206
  return {
207
  "service": "AGROW Chatbot Service",
208
+ "version": "2.0.0",
209
+ "architecture": "Multi-Stage Reasoning (Claim→Validate→Contradict→Confirm)",
210
  "endpoints": {
211
+ "/chat": "POST - Send message, get AI response with reasoning",
212
  "/session/new": "POST - Create new chat session",
213
  "/session/{id}/history": "GET - Get conversation history",
214
  "/sessions/{user_id}": "GET - List user's sessions"
 
220
  return {
221
  "status": "healthy",
222
  "gemini_configured": GEMINI_API_KEY is not None,
223
+ "gemini_model": GEMINI_MODEL,
224
  "supabase_configured": supabase.is_configured()
225
  }
226
 
 
248
 
249
  @app.post("/chat", response_model=ChatResponse)
250
  async def chat(request: ChatRequest):
251
+ """Send a message and get AI response with multi-stage reasoning."""
252
  logger.info(f"Chat request - Session: {request.session_id}, Message: {request.message[:50]}...")
253
 
254
  try:
 
262
  content=request.message
263
  )
264
 
265
+ # Detect intent first
266
+ intent = intent_classifier.classify(request.message)
267
+ logger.info(f"Intent: {intent['primary_intent']} ({intent['confidence']})")
268
+
269
+ # Determine if we need full reasoning or simple response
270
+ use_full_reasoning = (
271
+ intent["confidence"] > 0.6 and
272
+ request.field_context and
273
+ intent["primary_intent"] not in ["general_query"]
274
  )
275
 
276
+ if use_full_reasoning:
277
+ # Full multi-stage reasoning
278
+ logger.info("Using multi-stage reasoning pipeline")
279
+ response_text, reasoning_trace = reasoning_engine.process_query(
280
+ query=request.message,
281
+ context=request.field_context
282
+ )
283
+ context_used = list(reasoning_trace.get("evidence_summary", {}).get("primary", []))
284
+ else:
285
+ # Simple single-stage response
286
+ logger.info("Using simple response mode")
287
+ response_text = simple_reason(
288
+ query=request.message,
289
+ context=request.field_context or {},
290
+ llm_caller=call_gemini_api
291
+ )
292
+ reasoning_trace = {
293
+ "mode": "simple",
294
+ "intent": intent
295
+ }
296
+ context_used = list(request.field_context.keys()) if request.field_context else []
297
+
298
  # Save assistant response
299
  assistant_msg_id = supabase.add_message(
300
  session_id=request.session_id,
 
313
  session_id=request.session_id,
314
  message_id=assistant_msg_id,
315
  context_used=context_used,
316
+ timestamp=datetime.now().isoformat(),
317
+ reasoning_trace=reasoning_trace
318
  )
319
 
320
  except Exception as e:
 
379
  raise HTTPException(500, str(e))
380
 
381
 
382
+ # Intent analysis endpoint (for debugging)
383
+ @app.post("/analyze-intent")
384
+ async def analyze_intent(request: Dict[str, str]):
385
+ """Analyze query intent without generating response."""
386
+ query = request.get("query", "")
387
+ intent = intent_classifier.classify(query)
388
+ return {
389
+ "query": query,
390
+ "intent": intent
391
+ }
392
+
393
+
394
  if __name__ == "__main__":
395
  import uvicorn
396
+ logger.info("Starting AGROW Chatbot Service v2.0")
397
  uvicorn.run(app, host="0.0.0.0", port=7860)
intent_classifier.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Intent Classifier for Agricultural Chatbot
3
+ ==========================================
4
+ Detects user intent to determine context priority.
5
+ """
6
+
7
+ from typing import Dict, List, Tuple
8
+ import re
9
+
10
+ # Intent categories with keywords
11
+ INTENT_PATTERNS = {
12
+ "vegetation_health": {
13
+ "keywords": ["yellow", "yellowing", "brown", "dying", "wilting", "healthy", "health",
14
+ "crop health", "plant health", "leaf", "leaves", "chlorophyll", "green"],
15
+ "sub_intents": ["chlorophyll_issue", "nutrient_deficiency", "general_health"]
16
+ },
17
+ "water_stress": {
18
+ "keywords": ["water", "irrigation", "irrigate", "dry", "drought", "moisture",
19
+ "thirsty", "watering", "rain", "wet"],
20
+ "sub_intents": ["drought_stress", "overwatering", "irrigation_timing"]
21
+ },
22
+ "nutrient_status": {
23
+ "keywords": ["fertilizer", "nutrient", "nitrogen", "phosphorus", "potassium",
24
+ "npk", "deficiency", "feeding", "feed"],
25
+ "sub_intents": ["nitrogen_deficiency", "nutrient_excess", "fertilizer_timing"]
26
+ },
27
+ "pest_disease": {
28
+ "keywords": ["pest", "disease", "insect", "bug", "infection", "fungus",
29
+ "blight", "rot", "spots", "holes", "eating"],
30
+ "sub_intents": ["pest_damage", "fungal_disease", "bacterial_issue"]
31
+ },
32
+ "forecast_query": {
33
+ "keywords": ["forecast", "predict", "future", "next week", "tomorrow",
34
+ "will", "expect", "trend", "coming days"],
35
+ "sub_intents": ["growth_forecast", "stress_prediction", "weather_impact"]
36
+ },
37
+ "zone_specific": {
38
+ "keywords": ["area", "zone", "patch", "section", "part", "corner",
39
+ "northeast", "northwest", "southeast", "southwest", "north", "south"],
40
+ "sub_intents": ["zone_diagnosis", "zone_comparison"]
41
+ },
42
+ "action_recommendation": {
43
+ "keywords": ["what should", "how to", "fix", "solve", "recommend", "advice",
44
+ "help", "do", "action", "steps", "treatment"],
45
+ "sub_intents": ["immediate_action", "long_term_plan"]
46
+ },
47
+ "comparison": {
48
+ "keywords": ["compare", "better", "worse", "change", "changed", "difference",
49
+ "last week", "before", "improvement", "decline"],
50
+ "sub_intents": ["temporal_comparison", "zone_comparison"]
51
+ },
52
+ "general_query": {
53
+ "keywords": ["what", "how", "why", "tell", "about", "explain"],
54
+ "sub_intents": ["general_info"]
55
+ }
56
+ }
57
+
58
+
59
+ class IntentClassifier:
60
+ """Classifies user queries into agricultural intent categories."""
61
+
62
+ def __init__(self):
63
+ self.patterns = INTENT_PATTERNS
64
+
65
+ def classify(self, query: str) -> Dict:
66
+ """
67
+ Classify the intent of a user query.
68
+
69
+ Returns:
70
+ {
71
+ "primary_intent": str,
72
+ "sub_intents": List[str],
73
+ "confidence": float,
74
+ "matched_keywords": List[str]
75
+ }
76
+ """
77
+ query_lower = query.lower()
78
+ intent_scores = {}
79
+ matched_keywords = {}
80
+
81
+ # Score each intent based on keyword matches
82
+ for intent, config in self.patterns.items():
83
+ keywords = config["keywords"]
84
+ matches = [kw for kw in keywords if kw in query_lower]
85
+
86
+ if matches:
87
+ # Score based on number and specificity of matches
88
+ score = len(matches) * 0.2
89
+ # Boost for longer, more specific matches
90
+ for match in matches:
91
+ score += len(match) * 0.01
92
+
93
+ intent_scores[intent] = min(score, 1.0)
94
+ matched_keywords[intent] = matches
95
+
96
+ if not intent_scores:
97
+ # Default to general query
98
+ return {
99
+ "primary_intent": "general_query",
100
+ "sub_intents": ["general_info"],
101
+ "confidence": 0.5,
102
+ "matched_keywords": []
103
+ }
104
+
105
+ # Get highest scoring intent
106
+ primary_intent = max(intent_scores, key=intent_scores.get)
107
+ confidence = intent_scores[primary_intent]
108
+
109
+ # Get sub-intents
110
+ sub_intents = self._detect_sub_intents(query_lower, primary_intent)
111
+
112
+ return {
113
+ "primary_intent": primary_intent,
114
+ "sub_intents": sub_intents,
115
+ "confidence": round(confidence, 2),
116
+ "matched_keywords": matched_keywords.get(primary_intent, [])
117
+ }
118
+
119
+ def _detect_sub_intents(self, query: str, primary_intent: str) -> List[str]:
120
+ """Detect more specific sub-intents within the primary intent."""
121
+ sub_intents = []
122
+ config = self.patterns.get(primary_intent, {})
123
+
124
+ # Add base sub-intents
125
+ if config.get("sub_intents"):
126
+ sub_intents.append(config["sub_intents"][0])
127
+
128
+ # Detect additional context
129
+ if "why" in query:
130
+ sub_intents.append("causal_analysis")
131
+ if "how much" in query or "how many" in query:
132
+ sub_intents.append("quantitative")
133
+ if "when" in query:
134
+ sub_intents.append("temporal")
135
+ if "where" in query:
136
+ sub_intents.append("spatial")
137
+
138
+ return sub_intents if sub_intents else ["general"]
priority_mapper.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Priority Context Mapper for Agricultural Chatbot
3
+ =================================================
4
+ Maps detected intents to prioritized context selection.
5
+ """
6
+
7
+ from typing import Dict, List, Any, Optional
8
+
9
+ # Intent to Context Priority Mapping
10
+ INTENT_CONTEXT_PRIORITIES = {
11
+ "vegetation_health": {
12
+ "priority_1": ["NDVI", "EVI", "NDRE", "RECI", "temporal_trends.NDVI"],
13
+ "priority_2": ["clustering.stressed_patches", "anomalies", "PSRI"],
14
+ "priority_3": ["weather.temperature", "SMI", "B05", "B08"],
15
+ "priority_4": ["SAR.VV", "previous_analysis", "farmer_actions"]
16
+ },
17
+ "water_stress": {
18
+ "priority_1": ["SMI", "NDWI", "SAR.VV", "SAR.VH"],
19
+ "priority_2": ["temporal_trends.SMI", "weather.precipitation", "weather.evapotranspiration"],
20
+ "priority_3": ["NDVI", "clustering.moisture_clusters", "B11", "B12"],
21
+ "priority_4": ["farmer_actions.irrigation", "forecast.rain", "previous_analysis"]
22
+ },
23
+ "nutrient_status": {
24
+ "priority_1": ["NDRE", "RECI", "MCARI", "B05", "B06", "B07"],
25
+ "priority_2": ["NDVI", "EVI", "temporal_trends.NDRE"],
26
+ "priority_3": ["SMI", "SFI", "clustering.nutrient_clusters"],
27
+ "priority_4": ["farmer_actions.fertilizer", "weather", "previous_analysis"]
28
+ },
29
+ "pest_disease": {
30
+ "priority_1": ["anomalies", "PSRI", "PRI", "spatial_patterns.hotspots"],
31
+ "priority_2": ["NDVI", "temporal_trends.sudden_changes", "clustering.outliers"],
32
+ "priority_3": ["weather.humidity", "B04", "B05"],
33
+ "priority_4": ["farmer_actions.spraying", "previous_analysis", "historical_issues"]
34
+ },
35
+ "zone_specific": {
36
+ "priority_1": ["clustering.zone_stats", "patch_assignments", "spatial_embeddings"],
37
+ "priority_2": ["anomalies.in_zone", "all_indices.zone_values"],
38
+ "priority_3": ["temporal_trends.zone_specific"],
39
+ "priority_4": ["previous_analysis.zone_notes", "farmer_actions.zone_specific"]
40
+ },
41
+ "forecast_query": {
42
+ "priority_1": ["forecast.predictions", "temporal_trends.all", "weather.forecast"],
43
+ "priority_2": ["NDVI", "SMI", "current_stress_level"],
44
+ "priority_3": ["historical_patterns", "growth_stage"],
45
+ "priority_4": ["farmer_actions.planned", "previous_analysis"]
46
+ },
47
+ "action_recommendation": {
48
+ "priority_1": ["stress_summary", "NDVI", "SMI", "anomalies"],
49
+ "priority_2": ["weather.current", "weather.forecast"],
50
+ "priority_3": ["clustering.priority_zones", "temporal_trends"],
51
+ "priority_4": ["farmer_actions", "previous_analysis", "recommendations_history"]
52
+ },
53
+ "comparison": {
54
+ "priority_1": ["temporal_trends.all", "historical.NDVI", "historical.SMI"],
55
+ "priority_2": ["change_detection", "improvement_metrics"],
56
+ "priority_3": ["weather.historical", "farmer_actions.historical"],
57
+ "priority_4": ["previous_analysis", "baseline_values"]
58
+ },
59
+ "general_query": {
60
+ "priority_1": ["NDVI", "stress_summary", "weather.current"],
61
+ "priority_2": ["SMI", "anomalies", "clustering.summary"],
62
+ "priority_3": ["temporal_trends", "forecast"],
63
+ "priority_4": ["farmer_actions", "previous_analysis"]
64
+ }
65
+ }
66
+
67
+
68
+ class PriorityContextMapper:
69
+ """Maps intents to prioritized context for selective retrieval."""
70
+
71
+ def __init__(self):
72
+ self.priority_map = INTENT_CONTEXT_PRIORITIES
73
+
74
+ def get_context_priorities(self, intent: str) -> Dict[str, List[str]]:
75
+ """Get context priorities for a given intent."""
76
+ return self.priority_map.get(intent, self.priority_map["general_query"])
77
+
78
+ def extract_priority_context(
79
+ self,
80
+ intent: str,
81
+ full_context: Dict[str, Any],
82
+ priority_levels: List[int] = [1, 2, 3, 4]
83
+ ) -> Dict[str, Dict[str, Any]]:
84
+ """
85
+ Extract context based on priority levels.
86
+
87
+ Args:
88
+ intent: Detected intent
89
+ full_context: Complete context data
90
+ priority_levels: Which priority levels to include
91
+
92
+ Returns:
93
+ {
94
+ "priority_1": {...},
95
+ "priority_2": {...},
96
+ ...
97
+ }
98
+ """
99
+ priorities = self.get_context_priorities(intent)
100
+ result = {}
101
+
102
+ for level in priority_levels:
103
+ key = f"priority_{level}"
104
+ if key in priorities:
105
+ result[key] = self._extract_fields(
106
+ full_context,
107
+ priorities[key]
108
+ )
109
+
110
+ return result
111
+
112
+ def _extract_fields(
113
+ self,
114
+ context: Dict[str, Any],
115
+ field_paths: List[str]
116
+ ) -> Dict[str, Any]:
117
+ """Extract specific fields from context using dot notation paths."""
118
+ extracted = {}
119
+
120
+ for path in field_paths:
121
+ value = self._get_nested_value(context, path)
122
+ if value is not None:
123
+ # Use last part of path as key for simplicity
124
+ key = path.split(".")[-1]
125
+ extracted[key] = value
126
+
127
+ return extracted
128
+
129
+ def _get_nested_value(self, data: Dict, path: str) -> Any:
130
+ """Get nested value using dot notation (e.g., 'weather.temperature')."""
131
+ keys = path.split(".")
132
+ current = data
133
+
134
+ for key in keys:
135
+ if isinstance(current, dict) and key in current:
136
+ current = current[key]
137
+ else:
138
+ return None
139
+
140
+ return current
141
+
142
+ def build_staged_context(
143
+ self,
144
+ intent: str,
145
+ full_context: Dict[str, Any]
146
+ ) -> Dict[str, Any]:
147
+ """
148
+ Build context organized by reasoning stages.
149
+
150
+ Returns:
151
+ {
152
+ "claim_context": {...}, # Priority 1
153
+ "validate_context": {...}, # Priority 2
154
+ "contradict_context": {...}, # Priority 3
155
+ "confirm_context": {...} # Priority 4
156
+ }
157
+ """
158
+ priority_context = self.extract_priority_context(intent, full_context)
159
+
160
+ return {
161
+ "claim_context": priority_context.get("priority_1", {}),
162
+ "validate_context": priority_context.get("priority_2", {}),
163
+ "contradict_context": priority_context.get("priority_3", {}),
164
+ "confirm_context": priority_context.get("priority_4", {})
165
+ }
prompts.py CHANGED
@@ -1,109 +1,184 @@
1
  """
2
- LLM Prompts for Agricultural Advisor
3
- =====================================
4
- System prompts and context builders for Gemini.
5
  """
6
 
7
- SYSTEM_PROMPT = """You are AGROW AI, an expert agricultural advisor for farmers in India. You help farmers understand their crop health, soil conditions, and provide actionable recommendations.
8
-
9
- ## Your Expertise:
10
- - Crop stress analysis using satellite imagery
11
- - Soil health interpretation (N, P, K, moisture)
12
- - Vegetation indices (NDVI, NDWI, EVI, SAVI)
13
- - Weather impact on farming
14
- - Pest and disease identification
15
- - Irrigation recommendations
16
- - Fertilizer application timing
17
- - Harvest optimization
18
-
19
- ## Communication Style:
20
- - Be warm, supportive, and encouraging
21
- - Use simple language (assume farmer may not know technical terms)
22
- - When using technical terms, explain them briefly
23
- - Give practical, actionable advice
24
- - Consider Indian farming context (seasons, crops, practices)
25
  - Be concise but thorough
26
 
27
- ## Response Format:
28
- - Start with a direct answer to the question
29
- - Provide 2-3 specific recommendations when relevant
30
- - End with encouragement or a helpful tip
31
- - Use bullet points for lists
32
 
33
- ## Safety:
34
- - Never recommend chemicals without proper precautions
35
- - Suggest consulting local agricultural officers for serious issues
36
- - Recommend soil testing before major fertilizer decisions
37
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  def build_context_prompt(context: dict) -> str:
41
- """Build context prompt from pipeline analysis data."""
42
- parts = []
43
-
44
- # Stress Analysis
45
- if "stress_score" in context:
46
- score = context["stress_score"]
47
- level = "low" if score < 0.3 else "moderate" if score < 0.6 else "high"
48
- parts.append(f"• Crop Stress Level: {level} ({score:.2f}/1.0)")
49
 
50
- # Vegetation Indices
51
- if "ndvi" in context:
52
- ndvi = context["ndvi"]
53
- health = "excellent" if ndvi > 0.7 else "good" if ndvi > 0.5 else "concerning" if ndvi > 0.3 else "poor"
54
- parts.append(f"• NDVI (Plant Health): {ndvi:.3f} - {health}")
 
 
 
 
 
55
 
56
- if "ndwi" in context:
57
- ndwi = context["ndwi"]
58
- water = "adequate" if ndwi > 0 else "stressed"
59
- parts.append(f"• NDWI (Water Content): {ndwi:.3f} - {water}")
60
-
61
- if "evi" in context:
62
- parts.append(f"• EVI (Vegetation Vigor): {context['evi']:.3f}")
63
-
64
- # Soil Data
65
- if "soil" in context:
66
- soil = context["soil"]
67
- parts.append(f"• Soil Moisture: {soil.get('moisture', 'N/A')}")
68
- parts.append(f"• Soil NPK: N={soil.get('nitrogen', 'N/A')}, P={soil.get('phosphorus', 'N/A')}, K={soil.get('potassium', 'N/A')}")
69
-
70
- # Forecast
71
- if "forecast" in context:
72
- fc = context["forecast"]
73
- parts.append(f"• 20-day Forecast: {fc.get('trend', 'stable')}")
74
-
75
- # Field Info
76
- if "field_name" in context:
77
- parts.append(f"• Field: {context['field_name']}")
78
-
79
- if "crop_type" in context:
80
- parts.append(f"• Crop: {context['crop_type']}")
81
-
82
- if "area" in context:
83
- parts.append(f"• Area: {context['area']} acres")
84
-
85
- # Clusters/Zones
86
- if "zones" in context:
87
- zones = context["zones"]
88
- parts.append(f"• Field Zones: {len(zones)} distinct areas identified")
89
- for i, zone in enumerate(zones[:3]): # Max 3 zones
90
- parts.append(f" - Zone {i+1}: {zone.get('condition', 'N/A')}")
91
-
92
- # Weather
93
- if "weather" in context:
94
- w = context["weather"]
95
- parts.append(f"• Current Weather: {w.get('condition', 'N/A')}, {w.get('temp', 'N/A')}°C")
96
-
97
- if not parts:
98
- return ""
99
-
100
- return "\n".join(parts)
101
 
102
 
103
- # Quick response templates for common queries
104
- QUICK_RESPONSES = {
105
- "stress": "Based on your field's stress analysis, ",
106
- "water": "Looking at the water content indicators, ",
107
- "fertilizer": "Considering your soil nutrient levels, ",
108
- "harvest": "For optimal harvest timing, ",
109
- }
 
 
1
  """
2
+ LLM Prompts for Multi-Stage Reasoning
3
+ ======================================
4
+ Prompts for each stage: Claim Validate → Contradict → Confirm
5
  """
6
 
7
+ # =============================================================================
8
+ # SYSTEM PROMPT (Base context)
9
+ # =============================================================================
10
+
11
+ SYSTEM_PROMPT = """You are AGROW AI, an expert agricultural advisor for farmers in India.
12
+
13
+ You specialize in:
14
+ - Satellite imagery interpretation (Sentinel-2, SAR data)
15
+ - Vegetation indices analysis (NDVI, NDRE, EVI, SMI, etc.)
16
+ - Crop stress diagnosis (water, nutrient, pest/disease)
17
+ - Climate-smart farming recommendations
18
+ - Regional crop knowledge (wheat, rice, cotton, sugarcane, pulses, etc.)
19
+
20
+ Communication Style:
21
+ - Use simple, practical language farmers can understand
22
+ - Mention specific numbers from data when available
23
+ - Give actionable recommendations
24
+ - Reference local conditions when possible
25
  - Be concise but thorough
26
 
27
+ When you lack specific data, acknowledge it and give general guidance."""
 
 
 
 
28
 
29
+ # =============================================================================
30
+ # STAGE 1: CLAIM PROMPT
31
+ # =============================================================================
32
+
33
+ CLAIM_PROMPT = """You are analyzing agricultural satellite data to diagnose crop issues.
34
+
35
+ USER QUERY: {query}
36
+
37
+ AVAILABLE EVIDENCE (Primary indicators only):
38
+ {priority_1_context}
39
+
40
+ Based ONLY on this primary evidence:
41
+ 1. State your initial hypothesis about what's happening
42
+ 2. Cite specific values that support your hypothesis
43
+ 3. Rate your confidence (0.0 to 1.0)
44
+
45
+ Respond in JSON format:
46
+ {{
47
+ "initial_claim": "Your hypothesis in 1-2 sentences",
48
+ "hypothesis": "single_word_label",
49
+ "evidence_cited": ["index1: value", "index2: value"],
50
+ "confidence": 0.X,
51
+ "uncertainties": ["what you're unsure about"]
52
+ }}"""
53
+
54
+ # =============================================================================
55
+ # STAGE 2: VALIDATE PROMPT
56
+ # =============================================================================
57
+
58
+ VALIDATE_PROMPT = """You previously hypothesized: {previous_hypothesis}
59
+ Initial confidence: {previous_confidence}
60
+
61
+ ADDITIONAL SUPPORTING EVIDENCE:
62
+ {priority_2_context}
63
+
64
+ Does this new evidence:
65
+ 1. CONFIRM your hypothesis? (increases confidence)
66
+ 2. WEAKEN your hypothesis? (decreases confidence)
67
+ 3. Add SPATIAL context? (where is the issue concentrated?)
68
+
69
+ Respond in JSON format:
70
+ {{
71
+ "validation_result": "confirmed|weakened|neutral",
72
+ "confidence_updated": 0.X,
73
+ "spatial_notes": "location details if any",
74
+ "reasoning": "why confidence changed"
75
+ }}"""
76
+
77
+ # =============================================================================
78
+ # STAGE 3: CONTRADICT PROMPT
79
+ # =============================================================================
80
+
81
+ CONTRADICT_PROMPT = """CURRENT HYPOTHESIS: {hypothesis} (confidence: {confidence})
82
+
83
+ YOUR TASK: Actively look for evidence that CONTRADICTS this hypothesis.
84
+
85
+ ALTERNATIVE CAUSAL FACTORS TO CONSIDER:
86
+ {priority_3_context}
87
+
88
+ Questions to answer:
89
+ 1. Could something ELSE explain the symptoms?
90
+ 2. Is there evidence that contradicts the current hypothesis?
91
+ 3. What's an alternative explanation?
92
+
93
+ Respond in JSON format:
94
+ {{
95
+ "contradiction_found": true|false,
96
+ "contradicting_evidence": ["evidence that doesn't fit"],
97
+ "alternative_hypothesis": "alternative explanation",
98
+ "alternative_confidence": 0.X,
99
+ "reasoning": "why alternative might be correct"
100
+ }}"""
101
+
102
+ # =============================================================================
103
+ # STAGE 4: CONFIRM PROMPT
104
+ # =============================================================================
105
+
106
+ CONFIRM_PROMPT = """COMPETING HYPOTHESES:
107
+ 1. {hypothesis_1} (confidence: {conf_1})
108
+ 2. {hypothesis_2} (confidence: {conf_2})
109
+
110
+ FINAL VALIDATION DATA:
111
+ {priority_4_context}
112
 
113
+ Determine the FINAL diagnosis by:
114
+ 1. Weighing evidence for each hypothesis
115
+ 2. Considering farmer's recent actions
116
+ 3. Checking consistency with previous analyses
117
+ 4. Identifying the ROOT CAUSE vs symptoms
118
+
119
+ Respond in JSON format:
120
+ {{
121
+ "final_diagnosis": "clear diagnosis statement",
122
+ "confidence": 0.X,
123
+ "causal_chain": "A → B → C → symptom",
124
+ "root_cause": "the underlying cause",
125
+ "symptoms": ["observable symptoms"],
126
+ "recommendation": "what to do next"
127
+ }}"""
128
+
129
+ # =============================================================================
130
+ # RESPONSE GENERATION PROMPT
131
+ # =============================================================================
132
+
133
+ RESPONSE_PROMPT = """Based on this diagnostic analysis, generate a helpful response for the farmer.
134
+
135
+ USER QUERY: {query}
136
+
137
+ DIAGNOSIS RESULT:
138
+ {diagnosis}
139
+
140
+ EVIDENCE SUMMARY:
141
+ {evidence}
142
+
143
+ Generate a response that:
144
+ 1. Directly answers the farmer's question
145
+ 2. Explains the diagnosis in simple terms
146
+ 3. Cites key evidence (with numbers)
147
+ 4. Provides actionable recommendations
148
+ 5. Is concise but complete (3-5 paragraphs max)
149
+
150
+ Use emojis sparingly for visual clarity (📊 for data, 🔬 for analysis, ✅ for recommendations).
151
+
152
+ Respond in natural language (not JSON)."""
153
+
154
+ # =============================================================================
155
+ # HELPER FUNCTIONS
156
+ # =============================================================================
157
 
158
  def build_context_prompt(context: dict) -> str:
159
+ """Convert context dict to readable string for LLM."""
160
+ if not context:
161
+ return "No specific data available."
 
 
 
 
 
162
 
163
+ lines = []
164
+ for key, value in context.items():
165
+ if isinstance(value, dict):
166
+ lines.append(f"**{key}**:")
167
+ for k, v in value.items():
168
+ lines.append(f" - {k}: {v}")
169
+ elif isinstance(value, list):
170
+ lines.append(f"**{key}**: {', '.join(str(v) for v in value)}")
171
+ else:
172
+ lines.append(f"**{key}**: {value}")
173
 
174
+ return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
 
177
+ def format_stage_prompt(template: str, **kwargs) -> str:
178
+ """Format a stage prompt with provided values."""
179
+ # Convert context dicts to strings
180
+ for key, value in kwargs.items():
181
+ if isinstance(value, dict):
182
+ kwargs[key] = build_context_prompt(value)
183
+
184
+ return template.format(**kwargs)
reasoning_engine.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-Stage Reasoning Engine for Agricultural Chatbot
3
+ ======================================================
4
+ Implements: Claim → Validate → Contradict → Confirm pipeline
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from typing import Dict, List, Any, Optional, Tuple
10
+ from dataclasses import dataclass
11
+
12
+ from intent_classifier import IntentClassifier
13
+ from priority_mapper import PriorityContextMapper
14
+ from prompts import (
15
+ SYSTEM_PROMPT, CLAIM_PROMPT, VALIDATE_PROMPT,
16
+ CONTRADICT_PROMPT, CONFIRM_PROMPT, RESPONSE_PROMPT,
17
+ format_stage_prompt, build_context_prompt
18
+ )
19
+
20
+ logger = logging.getLogger("ReasoningEngine")
21
+
22
+
23
+ @dataclass
24
+ class StageResult:
25
+ """Result from a single reasoning stage."""
26
+ stage: str
27
+ output: Dict[str, Any]
28
+ context_used: List[str]
29
+ confidence: float
30
+
31
+
32
+ @dataclass
33
+ class ReasoningResult:
34
+ """Complete reasoning chain result."""
35
+ claim: StageResult
36
+ validation: StageResult
37
+ contradiction: StageResult
38
+ confirmation: StageResult
39
+ final_diagnosis: str
40
+ final_confidence: float
41
+ causal_chain: str
42
+ recommendation: str
43
+ evidence_summary: Dict[str, List[str]]
44
+
45
+
46
+ class ReasoningEngine:
47
+ """
48
+ Multi-stage reasoning engine for agricultural chatbot.
49
+ Does NOT ingest all context - uses priority-based selection.
50
+ """
51
+
52
+ def __init__(self, llm_caller):
53
+ """
54
+ Args:
55
+ llm_caller: Function that takes (prompt: str) -> str
56
+ """
57
+ self.llm = llm_caller
58
+ self.intent_classifier = IntentClassifier()
59
+ self.priority_mapper = PriorityContextMapper()
60
+
61
+ def process_query(
62
+ self,
63
+ query: str,
64
+ context: Optional[Dict[str, Any]] = None
65
+ ) -> Tuple[str, Dict[str, Any]]:
66
+ """
67
+ Process a user query through the full reasoning pipeline.
68
+
69
+ Returns:
70
+ (response_text, reasoning_trace)
71
+ """
72
+ logger.info(f"Processing query: {query[:50]}...")
73
+
74
+ # Stage 1: Classify intent
75
+ intent = self.intent_classifier.classify(query)
76
+ logger.info(f"Detected intent: {intent['primary_intent']} ({intent['confidence']})")
77
+
78
+ # Stage 2: Get prioritized context
79
+ staged_context = self.priority_mapper.build_staged_context(
80
+ intent=intent["primary_intent"],
81
+ full_context=context or {}
82
+ )
83
+
84
+ # Stage 3: Multi-stage reasoning
85
+ reasoning_result = self._reason(query, intent, staged_context)
86
+
87
+ # Stage 4: Generate response
88
+ response = self._generate_response(query, reasoning_result)
89
+
90
+ # Build reasoning trace
91
+ trace = self._build_trace(intent, reasoning_result, staged_context)
92
+
93
+ return response, trace
94
+
95
+ def _reason(
96
+ self,
97
+ query: str,
98
+ intent: Dict,
99
+ staged_context: Dict
100
+ ) -> ReasoningResult:
101
+ """Execute 4-stage reasoning pipeline."""
102
+
103
+ # Stage A: Initial Claim
104
+ claim = self._stage_claim(query, staged_context["claim_context"])
105
+
106
+ # Stage B: Validate
107
+ validation = self._stage_validate(
108
+ claim.output.get("hypothesis", "unknown"),
109
+ claim.confidence,
110
+ staged_context["validate_context"]
111
+ )
112
+
113
+ # Stage C: Contradict
114
+ contradiction = self._stage_contradict(
115
+ validation.output.get("hypothesis", claim.output.get("hypothesis", "unknown")),
116
+ validation.confidence,
117
+ staged_context["contradict_context"]
118
+ )
119
+
120
+ # Stage D: Confirm
121
+ confirmation = self._stage_confirm(
122
+ hypothesis_1=claim.output.get("hypothesis", "unknown"),
123
+ conf_1=validation.confidence,
124
+ hypothesis_2=contradiction.output.get("alternative_hypothesis", "none"),
125
+ conf_2=contradiction.output.get("alternative_confidence", 0),
126
+ context=staged_context["confirm_context"]
127
+ )
128
+
129
+ return ReasoningResult(
130
+ claim=claim,
131
+ validation=validation,
132
+ contradiction=contradiction,
133
+ confirmation=confirmation,
134
+ final_diagnosis=confirmation.output.get("final_diagnosis", "Undetermined"),
135
+ final_confidence=confirmation.confidence,
136
+ causal_chain=confirmation.output.get("causal_chain", ""),
137
+ recommendation=confirmation.output.get("recommendation", ""),
138
+ evidence_summary={
139
+ "primary": claim.context_used,
140
+ "supporting": validation.context_used,
141
+ "alternative": contradiction.context_used,
142
+ "validation": confirmation.context_used
143
+ }
144
+ )
145
+
146
+ def _stage_claim(self, query: str, context: Dict) -> StageResult:
147
+ """Stage 3A: Make initial claim using Priority 1 context."""
148
+ prompt = format_stage_prompt(
149
+ CLAIM_PROMPT,
150
+ query=query,
151
+ priority_1_context=context
152
+ )
153
+
154
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{prompt}"
155
+ response = self.llm(full_prompt)
156
+
157
+ try:
158
+ output = self._parse_json(response)
159
+ except:
160
+ output = {
161
+ "initial_claim": response,
162
+ "hypothesis": "general_issue",
163
+ "confidence": 0.5
164
+ }
165
+
166
+ return StageResult(
167
+ stage="claim",
168
+ output=output,
169
+ context_used=list(context.keys()),
170
+ confidence=output.get("confidence", 0.5)
171
+ )
172
+
173
+ def _stage_validate(
174
+ self,
175
+ hypothesis: str,
176
+ confidence: float,
177
+ context: Dict
178
+ ) -> StageResult:
179
+ """Stage 3B: Validate hypothesis using Priority 2 context."""
180
+ prompt = format_stage_prompt(
181
+ VALIDATE_PROMPT,
182
+ previous_hypothesis=hypothesis,
183
+ previous_confidence=confidence,
184
+ priority_2_context=context
185
+ )
186
+
187
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{prompt}"
188
+ response = self.llm(full_prompt)
189
+
190
+ try:
191
+ output = self._parse_json(response)
192
+ except:
193
+ output = {
194
+ "validation_result": "neutral",
195
+ "confidence_updated": confidence
196
+ }
197
+
198
+ # Carry forward hypothesis
199
+ output["hypothesis"] = hypothesis
200
+
201
+ return StageResult(
202
+ stage="validate",
203
+ output=output,
204
+ context_used=list(context.keys()),
205
+ confidence=output.get("confidence_updated", confidence)
206
+ )
207
+
208
+ def _stage_contradict(
209
+ self,
210
+ hypothesis: str,
211
+ confidence: float,
212
+ context: Dict
213
+ ) -> StageResult:
214
+ """Stage 3C: Seek contradictions using Priority 3 context."""
215
+ prompt = format_stage_prompt(
216
+ CONTRADICT_PROMPT,
217
+ hypothesis=hypothesis,
218
+ confidence=confidence,
219
+ priority_3_context=context
220
+ )
221
+
222
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{prompt}"
223
+ response = self.llm(full_prompt)
224
+
225
+ try:
226
+ output = self._parse_json(response)
227
+ except:
228
+ output = {
229
+ "contradiction_found": False,
230
+ "alternative_hypothesis": "none",
231
+ "alternative_confidence": 0
232
+ }
233
+
234
+ return StageResult(
235
+ stage="contradict",
236
+ output=output,
237
+ context_used=list(context.keys()),
238
+ confidence=output.get("alternative_confidence", 0)
239
+ )
240
+
241
+ def _stage_confirm(
242
+ self,
243
+ hypothesis_1: str,
244
+ conf_1: float,
245
+ hypothesis_2: str,
246
+ conf_2: float,
247
+ context: Dict
248
+ ) -> StageResult:
249
+ """Stage 3D: Final confirmation using Priority 4 context."""
250
+ prompt = format_stage_prompt(
251
+ CONFIRM_PROMPT,
252
+ hypothesis_1=hypothesis_1,
253
+ conf_1=conf_1,
254
+ hypothesis_2=hypothesis_2,
255
+ conf_2=conf_2,
256
+ priority_4_context=context
257
+ )
258
+
259
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{prompt}"
260
+ response = self.llm(full_prompt)
261
+
262
+ try:
263
+ output = self._parse_json(response)
264
+ except:
265
+ # Generate simple output if parsing fails
266
+ output = {
267
+ "final_diagnosis": f"Likely {hypothesis_1}",
268
+ "confidence": conf_1,
269
+ "recommendation": "Further investigation recommended"
270
+ }
271
+
272
+ return StageResult(
273
+ stage="confirm",
274
+ output=output,
275
+ context_used=list(context.keys()),
276
+ confidence=output.get("confidence", conf_1)
277
+ )
278
+
279
+ def _generate_response(self, query: str, result: ReasoningResult) -> str:
280
+ """Generate final user-facing response."""
281
+ prompt = format_stage_prompt(
282
+ RESPONSE_PROMPT,
283
+ query=query,
284
+ diagnosis=json.dumps({
285
+ "diagnosis": result.final_diagnosis,
286
+ "confidence": result.final_confidence,
287
+ "causal_chain": result.causal_chain,
288
+ "recommendation": result.recommendation
289
+ }, indent=2),
290
+ evidence=json.dumps(result.evidence_summary, indent=2)
291
+ )
292
+
293
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{prompt}"
294
+ response = self.llm(full_prompt)
295
+
296
+ return response
297
+
298
+ def _build_trace(
299
+ self,
300
+ intent: Dict,
301
+ result: ReasoningResult,
302
+ staged_context: Dict
303
+ ) -> Dict[str, Any]:
304
+ """Build reasoning trace for debugging/transparency."""
305
+ return {
306
+ "intent_detected": intent["primary_intent"],
307
+ "intent_confidence": intent["confidence"],
308
+ "sub_intents": intent["sub_intents"],
309
+ "stages": {
310
+ "claim": {
311
+ "hypothesis": result.claim.output.get("hypothesis"),
312
+ "confidence": result.claim.confidence,
313
+ "context_used": result.claim.context_used
314
+ },
315
+ "validation": {
316
+ "result": result.validation.output.get("validation_result"),
317
+ "confidence": result.validation.confidence,
318
+ "context_used": result.validation.context_used
319
+ },
320
+ "contradiction": {
321
+ "found": result.contradiction.output.get("contradiction_found"),
322
+ "alternative": result.contradiction.output.get("alternative_hypothesis"),
323
+ "confidence": result.contradiction.confidence,
324
+ "context_used": result.contradiction.context_used
325
+ },
326
+ "confirmation": {
327
+ "final": result.final_diagnosis,
328
+ "confidence": result.final_confidence,
329
+ "context_used": result.confirmation.context_used
330
+ }
331
+ },
332
+ "causal_chain": result.causal_chain,
333
+ "evidence_summary": result.evidence_summary
334
+ }
335
+
336
+ def _parse_json(self, text: str) -> Dict:
337
+ """Extract and parse JSON from LLM response."""
338
+ # Try to find JSON in response
339
+ text = text.strip()
340
+
341
+ # Look for JSON block
342
+ if "```json" in text:
343
+ start = text.find("```json") + 7
344
+ end = text.find("```", start)
345
+ text = text[start:end].strip()
346
+ elif "```" in text:
347
+ start = text.find("```") + 3
348
+ end = text.find("```", start)
349
+ text = text[start:end].strip()
350
+
351
+ # Find JSON object
352
+ start = text.find("{")
353
+ end = text.rfind("}") + 1
354
+ if start >= 0 and end > start:
355
+ text = text[start:end]
356
+
357
+ return json.loads(text)
358
+
359
+
360
+ # Simple interface for single-stage reasoning (fallback)
361
+ def simple_reason(query: str, context: Dict, llm_caller) -> str:
362
+ """Simplified single-stage reasoning for when full pipeline isn't needed."""
363
+ context_str = build_context_prompt(context) if context else "No context available."
364
+
365
+ prompt = f"""{SYSTEM_PROMPT}
366
+
367
+ User Query: {query}
368
+
369
+ Available Context:
370
+ {context_str}
371
+
372
+ Provide a helpful, actionable response."""
373
+
374
+ return llm_caller(prompt)