Spaces:
Sleeping
Sleeping
Commit ·
e2fa5a7
1
Parent(s): 07a23c0
Implement multi-stage reasoning architecture (Claim→Validate→Contradict→Confirm)
Browse files- app.py +116 -112
- intent_classifier.py +138 -0
- priority_mapper.py +165 -0
- prompts.py +171 -96
- reasoning_engine.py +374 -0
app.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
AGROW Agricultural Chatbot Service
|
| 3 |
===================================
|
| 4 |
-
AI-powered agricultural advisor
|
| 5 |
-
-
|
|
|
|
| 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
|
|
|
|
| 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]:
|
| 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 |
-
#
|
| 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
|
| 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":
|
| 168 |
}
|
| 169 |
},
|
| 170 |
-
timeout=
|
| 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
|
| 186 |
time.sleep(wait_time)
|
| 187 |
continue
|
| 188 |
return "I'm currently busy. Please try again in a moment."
|
| 189 |
else:
|
| 190 |
-
|
| 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 |
-
|
| 204 |
-
|
| 205 |
-
|
| 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 |
-
#
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
-
#
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
|
|
|
| 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 |
-
#
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 3 |
-
=====================================
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
-
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
- Use simple language
|
| 22 |
-
-
|
| 23 |
-
- Give
|
| 24 |
-
-
|
| 25 |
- Be concise but thorough
|
| 26 |
|
| 27 |
-
|
| 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 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def build_context_prompt(context: dict) -> str:
|
| 41 |
-
"""
|
| 42 |
-
|
| 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 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
| 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 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 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)
|