Aniket2006 commited on
Commit
c7df30e
·
1 Parent(s): bec40f7

Add comprehensive context: all indices, SAR, weather, persona-based responses

Browse files
Files changed (1) hide show
  1. app.py +426 -119
app.py CHANGED
@@ -1,10 +1,13 @@
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
@@ -23,7 +26,8 @@ import asyncio
23
  import google.generativeai as genai
24
 
25
  from supabase_client import SupabaseClient
26
- from prompts import SYSTEM_PROMPT, build_context_prompt
 
27
 
28
  # ============================================================================
29
  # LOGGING
@@ -35,6 +39,10 @@ logging.basicConfig(
35
  )
36
  logger = logging.getLogger("ChatbotService")
37
 
 
 
 
 
38
  # ============================================================================
39
  # GEMINI SETUP
40
  # ============================================================================
@@ -47,16 +55,17 @@ else:
47
  model = None
48
  logger.warning("GEMINI_API_KEY not set - chatbot will return mock responses")
49
 
50
- # Supabase client
51
  supabase = SupabaseClient()
 
52
 
53
  # ============================================================================
54
  # FASTAPI
55
  # ============================================================================
56
  app = FastAPI(
57
  title="AGROW Chatbot Service",
58
- description="AI agricultural advisor with conversation storage",
59
- version="1.0.0"
60
  )
61
 
62
  app.add_middleware(
@@ -74,7 +83,7 @@ class ChatRequest(BaseModel):
74
  session_id: str
75
  message: str
76
  user_id: Optional[str] = None
77
- field_context: Optional[Dict[str, Any]] = None # Pipeline data
78
 
79
  class ChatResponse(BaseModel):
80
  response: str
@@ -102,51 +111,351 @@ class HistoryResponse(BaseModel):
102
  session_id: str
103
  messages: List[MessageModel]
104
 
105
- class SessionListItem(BaseModel):
106
- id: str
107
- title: str
108
- created_at: str
109
- updated_at: str
110
- message_count: int
111
 
112
  # ============================================================================
113
- # HELPER FUNCTIONS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  # ============================================================================
115
- def generate_response(user_message: str, history: List[Dict], context: Optional[Dict] = None) -> tuple[str, List[str]]:
116
- """Generate AI response using Gemini."""
117
- context_used = []
118
-
119
- # Build conversation history for context
120
- conversation = []
121
- for msg in history[-10:]: # Last 10 messages for context
122
- role = "user" if msg.get("role") == "user" else "model"
123
- conversation.append({"role": role, "parts": [msg.get("content", "")]})
124
-
125
- # Build context prompt if pipeline data available
126
- context_prompt = ""
127
- if context:
128
- context_prompt = build_context_prompt(context)
129
- context_used = list(context.keys())
130
-
131
- # Combine system prompt with context
132
- full_system = SYSTEM_PROMPT
133
- if context_prompt:
134
- full_system += f"\n\n## Current Field Analysis:\n{context_prompt}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  if model is None:
137
- # Mock response if no API key
138
- return f"I received your question: '{user_message}'. Please configure GEMINI_API_KEY for real responses.", []
139
 
140
  try:
141
- # Create chat with system instruction
142
- chat = model.start_chat(history=conversation)
 
 
 
143
 
144
  # Generate response
145
- response = chat.send_message(
146
- f"[System: {full_system}]\n\nUser: {user_message}",
147
  generation_config=genai.types.GenerationConfig(
148
  temperature=0.7,
149
- max_output_tokens=1024,
150
  )
151
  )
152
 
@@ -154,7 +463,8 @@ def generate_response(user_message: str, history: List[Dict], context: Optional[
154
 
155
  except Exception as e:
156
  logger.error(f"Gemini error: {e}")
157
- return f"I apologize, but I encountered an error. Please try again. Error: {str(e)}", []
 
158
 
159
  # ============================================================================
160
  # API ENDPOINTS
@@ -163,13 +473,8 @@ def generate_response(user_message: str, history: List[Dict], context: Optional[
163
  async def root():
164
  return {
165
  "service": "AGROW Chatbot Service",
166
- "version": "1.0.0",
167
- "endpoints": {
168
- "/chat": "POST - Send message, get AI response",
169
- "/session/new": "POST - Create new chat session",
170
- "/session/{id}/history": "GET - Get conversation history",
171
- "/sessions/{user_id}": "GET - List user's sessions"
172
- }
173
  }
174
 
175
  @app.get("/health")
@@ -185,13 +490,11 @@ async def health():
185
  async def create_session(request: SessionRequest):
186
  """Create a new chat session."""
187
  logger.info(f"Creating new session for user: {request.user_id}")
188
-
189
  try:
190
  session = supabase.create_session(
191
  user_id=request.user_id,
192
  title=request.title or "New Conversation"
193
  )
194
-
195
  return SessionResponse(
196
  session_id=session["id"],
197
  title=session["title"],
@@ -204,8 +507,8 @@ async def create_session(request: SessionRequest):
204
 
205
  @app.post("/chat", response_model=ChatResponse)
206
  async def chat(request: ChatRequest):
207
- """Send a message and get AI response."""
208
- logger.info(f"Chat request - Session: {request.session_id}, Message: {request.message[:50]}...")
209
 
210
  try:
211
  # Load conversation history
@@ -218,12 +521,14 @@ async def chat(request: ChatRequest):
218
  content=request.message
219
  )
220
 
 
 
 
 
 
 
221
  # Generate AI response
222
- response_text, context_used = generate_response(
223
- request.message,
224
- history,
225
- request.field_context
226
- )
227
 
228
  # Save assistant response
229
  assistant_msg_id = supabase.add_message(
@@ -233,9 +538,7 @@ async def chat(request: ChatRequest):
233
  context_used=context_used
234
  )
235
 
236
- # Update session timestamp
237
  supabase.update_session_timestamp(request.session_id)
238
-
239
  logger.info(f"Response generated - {len(response_text)} chars")
240
 
241
  return ChatResponse(
@@ -252,14 +555,69 @@ async def chat(request: ChatRequest):
252
  raise HTTPException(500, str(e))
253
 
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  @app.get("/session/{session_id}/history", response_model=HistoryResponse)
256
  async def get_history(session_id: str):
257
  """Get conversation history for a session."""
258
- logger.info(f"Loading history for session: {session_id}")
259
-
260
  try:
261
  messages = supabase.get_messages(session_id)
262
-
263
  return HistoryResponse(
264
  session_id=session_id,
265
  messages=[
@@ -273,7 +631,6 @@ async def get_history(session_id: str):
273
  ]
274
  )
275
  except Exception as e:
276
- logger.error(f"History error: {e}")
277
  raise HTTPException(500, str(e))
278
 
279
 
@@ -281,75 +638,25 @@ async def get_history(session_id: str):
281
  async def list_sessions(user_id: str):
282
  """List all chat sessions for a user."""
283
  logger.info(f"Listing sessions for user: {user_id}")
284
-
285
  try:
286
  sessions = supabase.get_user_sessions(user_id)
287
-
288
- return {
289
- "user_id": user_id,
290
- "sessions": sessions,
291
- "count": len(sessions)
292
- }
293
  except Exception as e:
294
- logger.error(f"List sessions error: {e}")
295
  raise HTTPException(500, str(e))
296
 
297
 
298
  @app.delete("/session/{session_id}")
299
  async def delete_session(session_id: str):
300
- """Delete a chat session and its messages."""
301
  logger.info(f"Deleting session: {session_id}")
302
-
303
  try:
304
  supabase.delete_session(session_id)
305
  return {"status": "deleted", "session_id": session_id}
306
  except Exception as e:
307
- logger.error(f"Delete error: {e}")
308
- raise HTTPException(500, str(e))
309
-
310
-
311
- @app.post("/chat/stream")
312
- async def chat_stream(request: ChatRequest):
313
- """Stream chat response for Flutter app."""
314
- logger.info(f"Stream chat - Session: {request.session_id}")
315
-
316
- try:
317
- history = supabase.get_messages(request.session_id)
318
-
319
- supabase.add_message(
320
- session_id=request.session_id,
321
- role="user",
322
- content=request.message
323
- )
324
-
325
- response_text, context_used = generate_response(
326
- request.message, history, request.field_context
327
- )
328
-
329
- assistant_msg_id = supabase.add_message(
330
- session_id=request.session_id,
331
- role="assistant",
332
- content=response_text,
333
- context_used=context_used
334
- )
335
-
336
- supabase.update_session_timestamp(request.session_id)
337
-
338
- async def stream_response():
339
- yield f"data: {json.dumps({'type': 'metadata', 'session_id': request.session_id, 'message_id': assistant_msg_id})}\n\n"
340
- for i in range(0, len(response_text), 15):
341
- yield f"data: {json.dumps({'type': 'chunk', 'text': response_text[i:i+15]})}\n\n"
342
- await asyncio.sleep(0.03)
343
- yield f"data: {json.dumps({'type': 'done', 'full_text': response_text})}\n\n"
344
-
345
- return StreamingResponse(stream_response(), media_type="text/event-stream")
346
-
347
- except Exception as e:
348
- logger.error(f"Stream error: {e}")
349
  raise HTTPException(500, str(e))
350
 
351
 
352
  if __name__ == "__main__":
353
  import uvicorn
354
- logger.info("Starting AGROW Chatbot Service")
355
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
  AGROW Agricultural Chatbot Service
3
  ===================================
4
+ AI-powered agricultural advisor with comprehensive context from:
5
+ - SAR bands (VV, VH) and analysis
6
+ - Sentinel-2 vegetation indices (all 13)
7
+ - Weather data (current + forecast)
8
+ - Clustering and stress patterns
9
+ - Farmer profile from questionnaire
10
+ - Field data from coordinates_quad
11
  """
12
 
13
  import os
 
26
  import google.generativeai as genai
27
 
28
  from supabase_client import SupabaseClient
29
+ from context_aggregator import ContextAggregator
30
+ from prompts import PERSONA_DEFINITIONS, EXPERIENCE_MAP, TECH_COMFORT_MAP, INNOVATION_MAP, FARMING_GOAL_MAP
31
 
32
  # ============================================================================
33
  # LOGGING
 
39
  )
40
  logger = logging.getLogger("ChatbotService")
41
 
42
+ print("=" * 50)
43
+ print(f"===== Application Startup at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
44
+ print("=" * 50)
45
+
46
  # ============================================================================
47
  # GEMINI SETUP
48
  # ============================================================================
 
55
  model = None
56
  logger.warning("GEMINI_API_KEY not set - chatbot will return mock responses")
57
 
58
+ # Supabase and Context Aggregator
59
  supabase = SupabaseClient()
60
+ context_aggregator = ContextAggregator(timeout=60)
61
 
62
  # ============================================================================
63
  # FASTAPI
64
  # ============================================================================
65
  app = FastAPI(
66
  title="AGROW Chatbot Service",
67
+ description="AI agricultural advisor with comprehensive satellite context",
68
+ version="2.0.0"
69
  )
70
 
71
  app.add_middleware(
 
83
  session_id: str
84
  message: str
85
  user_id: Optional[str] = None
86
+ field_id: Optional[str] = None # Specific field to analyze
87
 
88
  class ChatResponse(BaseModel):
89
  response: str
 
111
  session_id: str
112
  messages: List[MessageModel]
113
 
 
 
 
 
 
 
114
 
115
  # ============================================================================
116
+ # PERSONA DETECTION
117
+ # ============================================================================
118
+ def detect_persona(questionnaire: Dict) -> str:
119
+ """Detect user persona from questionnaire answers."""
120
+ if not questionnaire:
121
+ return "experienced_farmer_traditional"
122
+
123
+ experience = questionnaire.get("experience", "2 - 5 years")
124
+ tech = questionnaire.get("tech_comfort", "I can use basic features")
125
+ innovation = questionnaire.get("innovation", "I try new methods occasionally")
126
+ goal = questionnaire.get("farming_goal", "Earn Income / Livelihood")
127
+ role = questionnaire.get("role", "Farmer")
128
+
129
+ # Map to persona
130
+ years = EXPERIENCE_MAP.get(experience, 3)
131
+ tech_level = TECH_COMFORT_MAP.get(tech, "moderate")
132
+ innovation_level = INNOVATION_MAP.get(innovation, "moderate")
133
+ goal_type = FARMING_GOAL_MAP.get(goal, "income")
134
+
135
+ # Role-based override
136
+ if role == "Agricultural Officer":
137
+ return "agricultural_officer"
138
+ elif role in ["Agronomist", "Researcher"]:
139
+ return "agronomist_researcher"
140
+
141
+ # Experience + innovation matrix
142
+ if years < 3:
143
+ return "new_farmer_tech_savvy" if tech_level == "advanced" else "new_farmer_basic_tech"
144
+ elif goal_type == "commercial":
145
+ return "commercial_farmer"
146
+ elif innovation_level == "innovative":
147
+ return "experienced_farmer_innovative"
148
+ else:
149
+ return "experienced_farmer_traditional"
150
+
151
+
152
+ # ============================================================================
153
+ # FETCH COMPREHENSIVE CONTEXT
154
  # ============================================================================
155
+ def fetch_field_data(user_id: str, field_id: Optional[str] = None) -> Optional[Dict]:
156
+ """Fetch field data from Supabase coordinates_quad."""
157
+ try:
158
+ if field_id:
159
+ query = supabase.client.table("coordinates_quad").select("*").eq("id", field_id).limit(1).execute()
160
+ else:
161
+ query = supabase.client.table("coordinates_quad").select("*").eq("user_id", user_id).limit(1).execute()
162
+
163
+ if query.data and len(query.data) > 0:
164
+ field = query.data[0]
165
+ # Calculate center point
166
+ lats = [field.get(f"lat{i}", 0) for i in range(1, 5)]
167
+ lons = [field.get(f"lon{i}", 0) for i in range(1, 5)]
168
+ center_lat = sum(lats) / 4
169
+ center_lon = sum(lons) / 4
170
+
171
+ return {
172
+ "id": field.get("id"),
173
+ "name": field.get("name", "My Field"),
174
+ "crop_type": field.get("crop_type", "Wheat"),
175
+ "area_acres": field.get("area_acres", 1.0),
176
+ "coordinates": {
177
+ "center_lat": center_lat,
178
+ "center_lon": center_lon,
179
+ "bbox": [min(lons), min(lats), max(lons), max(lats)]
180
+ }
181
+ }
182
+ except Exception as e:
183
+ logger.error(f"Error fetching field data: {e}")
184
+ return None
185
+
186
+
187
+ def fetch_user_profile(user_id: str) -> Dict:
188
+ """Fetch user profile and questionnaire from Supabase."""
189
+ try:
190
+ query = supabase.client.table("user_profiles").select(
191
+ "full_name, address, questionnaire_data"
192
+ ).eq("user_id", user_id).limit(1).execute()
193
+
194
+ if query.data and len(query.data) > 0:
195
+ profile = query.data[0]
196
+ return {
197
+ "name": profile.get("full_name", ""),
198
+ "location": profile.get("address", ""),
199
+ "questionnaire": profile.get("questionnaire_data", {})
200
+ }
201
+ except Exception as e:
202
+ logger.error(f"Error fetching user profile: {e}")
203
+ return {"name": "", "location": "", "questionnaire": {}}
204
+
205
+
206
+ def build_comprehensive_context(user_id: str, field_id: Optional[str] = None) -> Dict:
207
+ """Build comprehensive context from all data sources."""
208
+ context = {
209
+ "fetch_timestamp": datetime.now().isoformat(),
210
+ "data_sources": []
211
+ }
212
+
213
+ # 1. Fetch field data
214
+ field_data = fetch_field_data(user_id, field_id)
215
+ if field_data:
216
+ context["field_info"] = field_data
217
+ context["data_sources"].append("coordinates_quad")
218
+ logger.info(f"Field data fetched: {field_data.get('name')}")
219
+ else:
220
+ return context # Can't proceed without field
221
+
222
+ # 2. Fetch user profile and questionnaire
223
+ user_profile = fetch_user_profile(user_id)
224
+ questionnaire = user_profile.get("questionnaire", {})
225
+ persona = detect_persona(questionnaire)
226
+
227
+ context["farmer_profile"] = {
228
+ "name": user_profile.get("name", ""),
229
+ "location": user_profile.get("location", ""),
230
+ "persona": persona,
231
+ "questionnaire": questionnaire
232
+ }
233
+ context["data_sources"].append("user_profiles")
234
+ logger.info(f"Farmer persona detected: {persona}")
235
+
236
+ # 3. Fetch satellite context using ContextAggregator
237
+ coordinates = field_data.get("coordinates", {})
238
+ crop_type = field_data.get("crop_type", "Wheat")
239
+ area_acres = field_data.get("area_acres", 1.0)
240
+
241
+ satellite_context = context_aggregator.fetch_full_context(
242
+ coordinates=coordinates,
243
+ crop_type=crop_type,
244
+ area_acres=area_acres,
245
+ farmer_context={"profile": user_profile, "questionnaire": questionnaire}
246
+ )
247
+
248
+ # Merge satellite data
249
+ if satellite_context.get("sar_bands"):
250
+ context["sar_bands"] = satellite_context["sar_bands"]
251
+ context["data_sources"].append("sar_api")
252
+
253
+ if satellite_context.get("vegetation_indices"):
254
+ context["vegetation_indices"] = satellite_context["vegetation_indices"]
255
+ context["data_sources"].append("sentinel2_api")
256
+
257
+ if satellite_context.get("soil_indicators"):
258
+ context["soil_indicators"] = satellite_context["soil_indicators"]
259
+
260
+ if satellite_context.get("clustering"):
261
+ context["clustering"] = satellite_context["clustering"]
262
+
263
+ if satellite_context.get("temporal_trends"):
264
+ context["temporal_trends"] = satellite_context["temporal_trends"]
265
+
266
+ if satellite_context.get("historical_trends"):
267
+ context["historical_trends"] = satellite_context["historical_trends"]
268
+
269
+ if satellite_context.get("weather"):
270
+ context["weather"] = satellite_context["weather"]
271
+ context["data_sources"].append("weather_api")
272
+
273
+ if satellite_context.get("stressed_patches"):
274
+ context["stressed_patches"] = satellite_context["stressed_patches"]
275
+
276
+ if satellite_context.get("zone_analysis"):
277
+ context["zone_analysis"] = satellite_context["zone_analysis"]
278
+
279
+ if satellite_context.get("anomalies"):
280
+ context["anomalies"] = satellite_context["anomalies"]
281
+
282
+ logger.info(f"Context built from sources: {context['data_sources']}")
283
+ return context
284
+
285
+
286
+ # ============================================================================
287
+ # BUILD LLM PROMPT WITH CONTEXT
288
+ # ============================================================================
289
+ def build_llm_prompt(query: str, context: Dict, history: List[Dict] = None) -> str:
290
+ """Build comprehensive prompt for LLM with all context."""
291
+
292
+ # Get persona for response style
293
+ persona_key = context.get("farmer_profile", {}).get("persona", "experienced_farmer_traditional")
294
+ persona = PERSONA_DEFINITIONS.get(persona_key, PERSONA_DEFINITIONS["experienced_farmer_traditional"])
295
+
296
+ # Build context string
297
+ context_parts = []
298
+
299
+ # Field info
300
+ if context.get("field_info"):
301
+ field = context["field_info"]
302
+ context_parts.append(f"""## Field Information
303
+ - Name: {field.get('name', 'Unknown')}
304
+ - Crop: {field.get('crop_type', 'Unknown')}
305
+ - Area: {field.get('area_acres', 0):.2f} acres
306
+ - Location: {field.get('coordinates', {}).get('center_lat', 0):.4f}°N, {field.get('coordinates', {}).get('center_lon', 0):.4f}°E""")
307
+
308
+ # Vegetation indices (all 13)
309
+ if context.get("vegetation_indices"):
310
+ vi = context["vegetation_indices"]
311
+ vi_lines = ["## Vegetation Indices (Sentinel-2)"]
312
+ for name, data in vi.items():
313
+ if isinstance(data, dict):
314
+ mean = data.get("mean", data.get("value", "N/A"))
315
+ change = data.get("temporal_change", data.get("change", ""))
316
+ change_str = f" (Δ{change:+.3f})" if isinstance(change, (int, float)) else ""
317
+ vi_lines.append(f"- {name}: {mean:.4f}{change_str}" if isinstance(mean, float) else f"- {name}: {mean}")
318
+ context_parts.append("\n".join(vi_lines))
319
+
320
+ # SAR bands
321
+ if context.get("sar_bands"):
322
+ sar = context["sar_bands"]
323
+ context_parts.append(f"""## SAR Backscatter (Sentinel-1)
324
+ - VV: {sar.get('VV', {}).get('mean', 'N/A')} dB
325
+ - VH: {sar.get('VH', {}).get('mean', 'N/A')} dB
326
+ - VV/VH Ratio: {sar.get('VV_VH_ratio', 'N/A')}""")
327
+
328
+ # Soil indicators
329
+ if context.get("soil_indicators"):
330
+ soil = context["soil_indicators"]
331
+ soil_lines = ["## Soil Indicators"]
332
+ for name, data in soil.items():
333
+ level = data.get("level", "unknown") if isinstance(data, dict) else data
334
+ value = data.get("value", "") if isinstance(data, dict) else ""
335
+ val_str = f" ({value:.2f})" if isinstance(value, float) else ""
336
+ soil_lines.append(f"- {name.replace('_', ' ').title()}: {level}{val_str}")
337
+ context_parts.append("\n".join(soil_lines))
338
+
339
+ # Clustering
340
+ if context.get("clustering"):
341
+ cl = context["clustering"]
342
+ context_parts.append(f"""## Spatial Clustering
343
+ - Clusters: {cl.get('num_clusters', 'N/A')}
344
+ - Distribution: {json.dumps(cl.get('cluster_distribution', {}))}
345
+ - Pattern: {cl.get('spatial_pattern', 'N/A')}""")
346
+
347
+ # Temporal trends
348
+ if context.get("temporal_trends"):
349
+ tt = context["temporal_trends"]
350
+ context_parts.append(f"""## Temporal Trends
351
+ - NDVI Trend: {tt.get('ndvi_trend', 'N/A')}
352
+ - 7-day Change: {tt.get('7_day_change', 'N/A')}
353
+ - 30-day Change: {tt.get('30_day_change', 'N/A')}
354
+ - Anomaly: {tt.get('anomaly_detected', 'N/A')}""")
355
+
356
+ # Weather
357
+ if context.get("weather"):
358
+ w = context["weather"]
359
+ current = w.get("current", {})
360
+ forecast = w.get("forecast_7_day", w.get("forecast", {}))
361
+ context_parts.append(f"""## Weather Data
362
+ - Current: {current.get('temp', 'N/A')}°C, Humidity: {current.get('humidity', 'N/A')}%
363
+ - 7-day Avg Temp: {w.get('7_day_avg', {}).get('temp', 'N/A')}°C
364
+ - Precipitation: {w.get('7_day_avg', {}).get('total_precipitation', 'N/A')} mm
365
+ - Forecast Rain Probability: {forecast.get('rain_probability', 'N/A')}%""")
366
+
367
+ # Stressed patches
368
+ if context.get("stressed_patches") and len(context["stressed_patches"]) > 0:
369
+ patches = context["stressed_patches"][:3] # Top 3
370
+ patch_lines = ["## Stress Zones Detected"]
371
+ for p in patches:
372
+ patch_lines.append(f"- {p.get('location', 'Unknown')}: Score {p.get('stress_score', 0):.2f}, Area: {p.get('area_percent', 0):.1f}%")
373
+ context_parts.append("\n".join(patch_lines))
374
+
375
+ # Zone analysis
376
+ if context.get("zone_analysis"):
377
+ za = context["zone_analysis"]
378
+ if za.get("most_critical"):
379
+ mc = za["most_critical"]
380
+ context_parts.append(f"""## Priority Zone
381
+ - Location: {mc.get('location', 'Unknown')}
382
+ - Issue: {mc.get('issue', 'Unknown')}
383
+ - Urgency: {mc.get('urgency', 'Medium')}""")
384
+
385
+ # Farmer profile
386
+ if context.get("farmer_profile"):
387
+ fp = context["farmer_profile"]
388
+ q = fp.get("questionnaire", {})
389
+ context_parts.append(f"""## Farmer Profile
390
+ - Experience: {q.get('experience', 'Unknown')}
391
+ - Farming Goal: {q.get('farming_goal', 'Unknown')}
392
+ - Tech Comfort: {q.get('tech_comfort', 'Unknown')}
393
+ - Persona: {persona.get('description', '')}""")
394
+
395
+ # Build conversation history
396
+ history_text = ""
397
+ if history and len(history) > 0:
398
+ recent = history[-4:] # Last 2 exchanges
399
+ history_text = "\n## Recent Conversation\n"
400
+ for msg in recent:
401
+ role = "User" if msg.get("role") == "user" else "Assistant"
402
+ history_text += f"{role}: {msg.get('content', '')[:150]}...\n"
403
+
404
+ # Combine into full prompt
405
+ context_str = "\n\n".join(context_parts)
406
+
407
+ prompt = f"""You are AGROW AI, an expert agricultural advisor for Indian farmers.
408
+
409
+ # RESPONSE STYLE FOR THIS USER
410
+ {persona.get('description', '')}
411
+ - Style: {persona.get('style', '')}
412
+ - Format: {persona.get('format', '')}
413
+ - Focus: {persona.get('focus', '')}
414
+ - Tone: {persona.get('tone', '')}
415
+ - Recommendations: {persona.get('recommendations', '')}
416
+
417
+ # FIELD ANALYSIS DATA
418
+ {context_str}
419
+ {history_text}
420
+
421
+ # USER QUERY
422
+ {query}
423
+
424
+ # INSTRUCTIONS
425
+ 1. Analyze the satellite data thoroughly
426
+ 2. Provide actionable recommendations matching the user's persona
427
+ 3. Reference specific data values when relevant
428
+ 4. Be concise but comprehensive
429
+ 5. Prioritize urgent issues first
430
+
431
+ Provide your response:"""
432
+
433
+ return prompt
434
+
435
+
436
+ # ============================================================================
437
+ # GENERATE RESPONSE
438
+ # ============================================================================
439
+ def generate_response(user_message: str, history: List[Dict], context: Dict) -> tuple[str, List[str]]:
440
+ """Generate AI response using comprehensive context."""
441
+ context_used = context.get("data_sources", [])
442
 
443
  if model is None:
444
+ return f"Please configure GEMINI_API_KEY for real responses.", []
 
445
 
446
  try:
447
+ # Build comprehensive prompt
448
+ prompt = build_llm_prompt(user_message, context, history)
449
+
450
+ # Log context summary
451
+ logger.info(f"Context: {len(context_used)} sources, Prompt: {len(prompt)} chars")
452
 
453
  # Generate response
454
+ response = model.generate_content(
455
+ prompt,
456
  generation_config=genai.types.GenerationConfig(
457
  temperature=0.7,
458
+ max_output_tokens=2048,
459
  )
460
  )
461
 
 
463
 
464
  except Exception as e:
465
  logger.error(f"Gemini error: {e}")
466
+ return f"I apologize, but I encountered an error: {str(e)}", []
467
+
468
 
469
  # ============================================================================
470
  # API ENDPOINTS
 
473
  async def root():
474
  return {
475
  "service": "AGROW Chatbot Service",
476
+ "version": "2.0.0",
477
+ "features": ["comprehensive_context", "persona_based_responses", "satellite_analysis"]
 
 
 
 
 
478
  }
479
 
480
  @app.get("/health")
 
490
  async def create_session(request: SessionRequest):
491
  """Create a new chat session."""
492
  logger.info(f"Creating new session for user: {request.user_id}")
 
493
  try:
494
  session = supabase.create_session(
495
  user_id=request.user_id,
496
  title=request.title or "New Conversation"
497
  )
 
498
  return SessionResponse(
499
  session_id=session["id"],
500
  title=session["title"],
 
507
 
508
  @app.post("/chat", response_model=ChatResponse)
509
  async def chat(request: ChatRequest):
510
+ """Send a message and get AI response with full context."""
511
+ logger.info(f"Chat request - Session: {request.session_id}")
512
 
513
  try:
514
  # Load conversation history
 
521
  content=request.message
522
  )
523
 
524
+ # Build comprehensive context
525
+ context = {}
526
+ if request.user_id:
527
+ context = build_comprehensive_context(request.user_id, request.field_id)
528
+ logger.info(f"Context built: {context.get('data_sources', [])}")
529
+
530
  # Generate AI response
531
+ response_text, context_used = generate_response(request.message, history, context)
 
 
 
 
532
 
533
  # Save assistant response
534
  assistant_msg_id = supabase.add_message(
 
538
  context_used=context_used
539
  )
540
 
 
541
  supabase.update_session_timestamp(request.session_id)
 
542
  logger.info(f"Response generated - {len(response_text)} chars")
543
 
544
  return ChatResponse(
 
555
  raise HTTPException(500, str(e))
556
 
557
 
558
+ @app.post("/chat/stream")
559
+ async def chat_stream(request: ChatRequest):
560
+ """Stream chat response with full context."""
561
+ logger.info(f"Stream chat - Session: {request.session_id}")
562
+
563
+ try:
564
+ history = supabase.get_messages(request.session_id)
565
+
566
+ supabase.add_message(
567
+ session_id=request.session_id,
568
+ role="user",
569
+ content=request.message
570
+ )
571
+
572
+ # Build comprehensive context
573
+ context = {}
574
+ if request.user_id:
575
+ context = build_comprehensive_context(request.user_id, request.field_id)
576
+ logger.info(f"Context built: {context.get('data_sources', [])}")
577
+
578
+ # Generate response
579
+ response_text, context_used = generate_response(request.message, history, context)
580
+
581
+ assistant_msg_id = supabase.add_message(
582
+ session_id=request.session_id,
583
+ role="assistant",
584
+ content=response_text,
585
+ context_used=context_used
586
+ )
587
+
588
+ supabase.update_session_timestamp(request.session_id)
589
+
590
+ async def stream_response():
591
+ # Send metadata
592
+ yield f"data: {json.dumps({'type': 'metadata', 'session_id': request.session_id, 'message_id': assistant_msg_id, 'context_sources': context_used})}\n\n"
593
+
594
+ # Stream text in chunks
595
+ for i in range(0, len(response_text), 15):
596
+ yield f"data: {json.dumps({'type': 'chunk', 'text': response_text[i:i+15]})}\n\n"
597
+ await asyncio.sleep(0.03)
598
+
599
+ # Done signal
600
+ yield f"data: {json.dumps({'type': 'done', 'full_text': response_text})}\n\n"
601
+
602
+ return StreamingResponse(stream_response(), media_type="text/event-stream")
603
+
604
+ except Exception as e:
605
+ logger.error(f"Stream error: {e}")
606
+ raise HTTPException(500, str(e))
607
+
608
+
609
+ @app.get("/context/{user_id}")
610
+ async def get_context(user_id: str, field_id: Optional[str] = None):
611
+ """Get the comprehensive context JSON for debugging."""
612
+ context = build_comprehensive_context(user_id, field_id)
613
+ return context
614
+
615
+
616
  @app.get("/session/{session_id}/history", response_model=HistoryResponse)
617
  async def get_history(session_id: str):
618
  """Get conversation history for a session."""
 
 
619
  try:
620
  messages = supabase.get_messages(session_id)
 
621
  return HistoryResponse(
622
  session_id=session_id,
623
  messages=[
 
631
  ]
632
  )
633
  except Exception as e:
 
634
  raise HTTPException(500, str(e))
635
 
636
 
 
638
  async def list_sessions(user_id: str):
639
  """List all chat sessions for a user."""
640
  logger.info(f"Listing sessions for user: {user_id}")
 
641
  try:
642
  sessions = supabase.get_user_sessions(user_id)
643
+ return {"user_id": user_id, "sessions": sessions, "count": len(sessions)}
 
 
 
 
 
644
  except Exception as e:
 
645
  raise HTTPException(500, str(e))
646
 
647
 
648
  @app.delete("/session/{session_id}")
649
  async def delete_session(session_id: str):
650
+ """Delete a chat session."""
651
  logger.info(f"Deleting session: {session_id}")
 
652
  try:
653
  supabase.delete_session(session_id)
654
  return {"status": "deleted", "session_id": session_id}
655
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  raise HTTPException(500, str(e))
657
 
658
 
659
  if __name__ == "__main__":
660
  import uvicorn
661
+ logger.info("Starting AGROW Chatbot Service v2.0")
662
  uvicorn.run(app, host="0.0.0.0", port=7860)