Aniket2006 commited on
Commit
aaf7391
·
1 Parent(s): 83e9fb0

Simplify chatbot: single LLM call, remove complex reasoning

Browse files
Files changed (1) hide show
  1. app.py +155 -739
app.py CHANGED
@@ -1,9 +1,9 @@
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
 
@@ -23,9 +23,6 @@ from pydantic import BaseModel
23
  import asyncio
24
 
25
  from supabase_client import SupabaseClient
26
- from reasoning_engine import ReasoningEngine, simple_reason
27
- from intent_classifier import IntentClassifier
28
- from context_aggregator import ContextAggregator, fetch_field_context
29
 
30
  # ============================================================================
31
  # LOGGING
@@ -40,301 +37,75 @@ logger = logging.getLogger("ChatbotService")
40
  # ============================================================================
41
  # GEMINI SETUP - Multi-API Key Fallback System
42
  # ============================================================================
43
-
44
- # Load multiple API keys (GEMINI_API_KEY_1 through GEMINI_API_KEY_5)
45
  def load_gemini_api_keys() -> List[str]:
46
  """Load all available Gemini API keys from environment."""
47
  keys = []
48
-
49
- # Primary key
50
  primary = os.environ.get("GEMINI_API_KEY")
51
  if primary:
52
  keys.append(primary)
53
-
54
- # Fallback keys 1-5
55
  for i in range(1, 6):
56
  key = os.environ.get(f"GEMINI_API_KEY_{i}")
57
  if key and key not in keys:
58
  keys.append(key)
59
-
60
  return keys
61
 
62
  GEMINI_API_KEYS = load_gemini_api_keys()
63
- current_key_index = 0 # Track which key is currently in use
64
-
65
  logger.info(f"Loaded {len(GEMINI_API_KEYS)} Gemini API key(s)")
66
 
67
- # Try to discover available models at startup
68
  def get_available_model(api_key: str):
69
- """Try to find an available Gemini model with given API key."""
70
- models_to_try = [
71
- "gemini-2.0-flash",
72
- "gemini-1.5-flash",
73
- "gemini-1.5-pro",
74
- "gemini-pro",
75
- "gemini-1.0-pro",
76
- ]
77
-
78
  if not api_key:
79
  return None, None
80
-
81
  for model in models_to_try:
82
- for version in ["v1beta", "v1"]:
83
- url = f"https://generativelanguage.googleapis.com/{version}/models/{model}:generateContent"
84
- try:
85
- resp = requests.post(
86
- f"{url}?key={api_key}",
87
- json={"contents": [{"parts": [{"text": "test"}]}]},
88
- timeout=10
89
- )
90
- if resp.status_code in [200, 429]:
91
- logger.info(f"Found working model: {model} on {version}")
92
- return url, model
93
- except:
94
- pass
95
-
96
  return None, None
97
 
98
- # Initialize with first available key
99
  GEMINI_URL, GEMINI_MODEL = None, None
100
  if GEMINI_API_KEYS:
101
  GEMINI_URL, GEMINI_MODEL = get_available_model(GEMINI_API_KEYS[0])
102
-
103
- if GEMINI_URL:
104
  logger.info(f"Using Gemini model: {GEMINI_MODEL}")
105
- else:
106
- GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
107
- logger.warning("Could not discover model, using default gemini-2.0-flash")
108
-
109
- if GEMINI_API_KEYS:
110
- logger.info(f"Gemini API configured with {len(GEMINI_API_KEYS)} fallback key(s)")
111
- else:
112
- logger.warning("No GEMINI_API_KEY set - chatbot will return mock responses")
113
 
114
  # Supabase client
115
  supabase = SupabaseClient()
116
 
117
  # ============================================================================
118
- # LLM CALLER WITH FALLBACK
119
  # ============================================================================
120
- def get_next_api_key() -> Optional[str]:
121
- """Rotate to the next available API key."""
122
- global current_key_index
123
- if not GEMINI_API_KEYS:
124
- return None
125
- current_key_index = (current_key_index + 1) % len(GEMINI_API_KEYS)
126
- logger.info(f"Rotated to API key {current_key_index + 1}/{len(GEMINI_API_KEYS)}")
127
- return GEMINI_API_KEYS[current_key_index]
128
-
129
-
130
- # ============================================================================
131
- # STRICT RATE LIMITER - Maximum 5 API calls per minute
132
- # ============================================================================
133
- import time
134
- from collections import deque
135
-
136
- # Global rate limiter - tracks timestamps of API calls
137
- _api_call_timestamps: deque = deque(maxlen=100) # Rolling window
138
- RATE_LIMIT_CALLS = 5 # Maximum calls per minute
139
- RATE_LIMIT_WINDOW = 60 # Window in seconds
140
-
141
- def check_rate_limit() -> bool:
142
- """
143
- Check if we're within rate limits.
144
- Returns True if call is allowed, False if blocked.
145
- """
146
- global _api_call_timestamps
147
- current_time = time.time()
148
-
149
- # Remove timestamps older than 1 minute
150
- while _api_call_timestamps and current_time - _api_call_timestamps[0] > RATE_LIMIT_WINDOW:
151
- _api_call_timestamps.popleft()
152
-
153
- # Check if we've exceeded the limit
154
- if len(_api_call_timestamps) >= RATE_LIMIT_CALLS:
155
- oldest_call = _api_call_timestamps[0]
156
- wait_time = RATE_LIMIT_WINDOW - (current_time - oldest_call)
157
- logger.warning(f"RATE LIMIT: {len(_api_call_timestamps)} calls in last minute. Wait {wait_time:.0f}s")
158
- return False
159
-
160
- return True
161
-
162
- def record_api_call():
163
- """Record an API call timestamp."""
164
- _api_call_timestamps.append(time.time())
165
- logger.info(f"API call recorded: {len(_api_call_timestamps)}/{RATE_LIMIT_CALLS} in last minute")
166
-
167
- def get_rate_limit_status() -> dict:
168
- """Get current rate limit status."""
169
- current_time = time.time()
170
- # Clean old timestamps
171
- while _api_call_timestamps and current_time - _api_call_timestamps[0] > RATE_LIMIT_WINDOW:
172
- _api_call_timestamps.popleft()
173
-
174
- calls_remaining = RATE_LIMIT_CALLS - len(_api_call_timestamps)
175
- if _api_call_timestamps:
176
- reset_in = RATE_LIMIT_WINDOW - (current_time - _api_call_timestamps[0])
177
- else:
178
- reset_in = 0
179
-
180
- return {
181
- "calls_made": len(_api_call_timestamps),
182
- "calls_remaining": max(0, calls_remaining),
183
- "reset_in_seconds": max(0, int(reset_in))
184
- }
185
-
186
-
187
- def call_gemini_api(prompt: str) -> str:
188
- """
189
- Call Gemini API with fallback across multiple API keys.
190
-
191
- STRICT RATE LIMIT: Maximum 5 calls per minute.
192
- Returns "RATE_LIMITED" if limit exceeded.
193
-
194
- Automatically rotates to next key on:
195
- - 429 (rate limit)
196
- - 403 (quota exceeded)
197
- - 500+ (server errors)
198
- """
199
- global current_key_index
200
-
201
- # STRICT RATE LIMIT CHECK - 5 calls/minute max
202
- if not check_rate_limit():
203
- logger.warning("RATE LIMIT EXCEEDED - returning fallback signal")
204
- return "RATE_LIMITED"
205
-
206
- # Record ONE API call at the start (not per retry/rotation)
207
- record_api_call()
208
-
209
- if not GEMINI_API_KEYS:
210
- return "Please configure GEMINI_API_KEY for real responses."
211
-
212
- max_retries_per_key = 2
213
- keys_tried = 0
214
-
215
- while keys_tried < len(GEMINI_API_KEYS):
216
- current_key = GEMINI_API_KEYS[current_key_index]
217
- url = f"{GEMINI_URL}?key={current_key}"
218
-
219
- for attempt in range(max_retries_per_key):
220
- try:
221
- response = requests.post(
222
- url,
223
- headers={"Content-Type": "application/json"},
224
- json={
225
- "contents": [{"parts": [{"text": prompt}]}],
226
- "generationConfig": {
227
- "temperature": 0.7,
228
- "maxOutputTokens": 2048,
229
- }
230
- },
231
- timeout=60
232
- )
233
-
234
- if response.status_code == 200:
235
- data = response.json()
236
- if "candidates" in data and len(data["candidates"]) > 0:
237
- return data["candidates"][0]["content"]["parts"][0]["text"]
238
- return "No response generated."
239
-
240
- elif response.status_code in [400, 429, 403, 500, 502, 503]:
241
- # Rate limit, quota exceeded, or server error - try next key
242
- logger.warning(f"API key {current_key_index + 1} got {response.status_code}, rotating...")
243
- get_next_api_key()
244
- keys_tried += 1
245
- break # Exit retry loop, try next key
246
-
247
- else:
248
- logger.error(f"Gemini API error: {response.status_code}")
249
- return f"API error: {response.status_code}"
250
-
251
- except requests.exceptions.Timeout:
252
- logger.warning(f"Timeout on key {current_key_index + 1}, attempt {attempt + 1}")
253
- if attempt == max_retries_per_key - 1:
254
- get_next_api_key()
255
- keys_tried += 1
256
- continue
257
-
258
- except Exception as e:
259
- logger.error(f"Gemini request error: {e}")
260
- if attempt == max_retries_per_key - 1:
261
- get_next_api_key()
262
- keys_tried += 1
263
- continue
264
-
265
- return "All API keys exhausted. Please try again later."
266
-
267
-
268
- # Initialize reasoning engine
269
- reasoning_engine = ReasoningEngine(llm_caller=call_gemini_api)
270
- intent_classifier = IntentClassifier()
271
-
272
- # Session history for conversation memory (follow-up awareness)
273
- # In production, this should be stored in Supabase/Redis, but for now use in-memory
274
- session_history: Dict[str, List[Dict]] = {}
275
- MAX_HISTORY_TURNS = 5
276
-
277
-
278
- def get_session_history(session_id: str) -> List[Dict]:
279
- """Get recent conversation history for a session."""
280
- return session_history.get(session_id, [])[-MAX_HISTORY_TURNS:]
281
-
282
-
283
- def add_to_session_history(session_id: str, role: str, content: str,
284
- intent: str = None, diagnosis: str = None):
285
- """Add a turn to session history for follow-up awareness."""
286
- if session_id not in session_history:
287
- session_history[session_id] = []
288
-
289
- turn = {
290
- "role": role,
291
- "content": content[:500], # Truncate long content
292
- "intent": intent,
293
- "diagnosis": diagnosis[:200] if diagnosis else None
294
- }
295
- session_history[session_id].append(turn)
296
-
297
- # Keep only last N turns
298
- if len(session_history[session_id]) > MAX_HISTORY_TURNS * 2:
299
- session_history[session_id] = session_history[session_id][-MAX_HISTORY_TURNS:]
300
 
 
 
 
 
 
 
301
 
302
- def build_field_coordinates(field_data: Dict) -> Optional[Dict]:
303
- """Build coordinates dict from field data with lat/lon corners."""
304
- if not field_data:
305
- return None
306
-
307
- lats = []
308
- lons = []
309
- for i in range(1, 5):
310
- lat = field_data.get(f"lat{i}")
311
- lon = field_data.get(f"lon{i}")
312
- if lat is not None and lon is not None:
313
- lats.append(float(lat))
314
- lons.append(float(lon))
315
-
316
- if not lats or not lons:
317
- return None
318
-
319
- center_lat = sum(lats) / len(lats)
320
- center_lon = sum(lons) / len(lons)
321
-
322
- return {
323
- "center_lat": round(center_lat, 6),
324
- "center_lon": round(center_lon, 6),
325
- "bbox": [min(lons), min(lats), max(lons), max(lats)]
326
- }
327
 
 
328
 
329
  # ============================================================================
330
- # FASTAPI
331
  # ============================================================================
332
- app = FastAPI(
333
- title="AGROW Chatbot Service",
334
- description="AI agricultural advisor with multi-stage reasoning",
335
- version="2.0.0"
336
- )
337
-
338
  app.add_middleware(
339
  CORSMiddleware,
340
  allow_origins=["*"],
@@ -343,36 +114,28 @@ app.add_middleware(
343
  allow_headers=["*"],
344
  )
345
 
 
 
 
 
346
  # ============================================================================
347
- # REQUEST/RESPONSE MODELS (Matching Developer Spec)
348
  # ============================================================================
349
  class ChatRequest(BaseModel):
350
  session_id: str
351
  message: str
352
  user_id: Optional[str] = None
353
- field_context: Optional[Dict[str, Any]] = None
354
- field_name: Optional[str] = None # Optional specific field
355
 
356
  class ResponseContent(BaseModel):
357
  message: str
358
- confidence: float
359
- diagnosis: Optional[str] = None
360
-
361
- class ContextPriorityUsed(BaseModel):
362
- priority_1: List[str] = []
363
- priority_2: List[str] = []
364
- priority_3: List[str] = []
365
- priority_4: List[str] = []
366
 
367
  class ChatResponse(BaseModel):
368
- """Full response matching developer spec."""
369
  response: ResponseContent
370
  session_id: str
371
  message_id: str
372
  timestamp: str
373
- reasoning_trace: Optional[Dict[str, Any]] = None
374
- context_priority_used: Optional[ContextPriorityUsed] = None
375
- suggested_followups: List[str] = []
376
 
377
  class SessionRequest(BaseModel):
378
  user_id: str
@@ -383,16 +146,85 @@ class SessionResponse(BaseModel):
383
  title: str
384
  created_at: str
385
 
386
- class MessageModel(BaseModel):
387
- id: str
388
- role: str
389
- content: str
390
- created_at: str
 
 
 
391
 
392
- class HistoryResponse(BaseModel):
393
- session_id: str
394
- messages: List[MessageModel]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
 
 
 
396
 
397
  # ============================================================================
398
  # API ENDPOINTS
@@ -401,37 +233,27 @@ class HistoryResponse(BaseModel):
401
  async def root():
402
  return {
403
  "service": "AGROW Chatbot Service",
404
- "version": "2.0.0",
405
- "architecture": "Multi-Stage Reasoning (Claim→Validate→Contradict→Confirm)",
406
- "endpoints": {
407
- "/chat": "POST - Send message, get AI response with reasoning",
408
- "/session/new": "POST - Create new chat session",
409
- "/session/{id}/history": "GET - Get conversation history",
410
- "/sessions/{user_id}": "GET - List user's sessions"
411
- }
412
  }
413
 
414
  @app.get("/health")
415
  async def health():
416
  return {
417
  "status": "healthy",
418
- "gemini_configured": GEMINI_API_KEY is not None,
419
- "gemini_model": GEMINI_MODEL,
420
- "supabase_configured": supabase.is_configured()
421
  }
422
 
423
-
424
  @app.post("/session/new", response_model=SessionResponse)
425
  async def create_session(request: SessionRequest):
426
  """Create a new chat session."""
427
  logger.info(f"Creating new session for user: {request.user_id}")
428
-
429
  try:
430
  session = supabase.create_session(
431
  user_id=request.user_id,
432
  title=request.title or "New Conversation"
433
  )
434
-
435
  return SessionResponse(
436
  session_id=session["id"],
437
  title=session["title"],
@@ -441,15 +263,14 @@ async def create_session(request: SessionRequest):
441
  logger.error(f"Failed to create session: {e}")
442
  raise HTTPException(500, str(e))
443
 
444
-
445
  @app.post("/chat", response_model=ChatResponse)
446
  async def chat(request: ChatRequest):
447
- """Send a message and get AI response with multi-stage reasoning."""
448
- logger.info(f"Chat request - Session: {request.session_id}, Message: {request.message[:50]}...")
449
 
450
  try:
451
- # Load conversation history
452
- history = supabase.get_messages(request.session_id)
453
 
454
  # Save user message
455
  user_msg_id = supabase.add_message(
@@ -458,214 +279,25 @@ async def chat(request: ChatRequest):
458
  content=request.message
459
  )
460
 
461
- # Build context - combine passed context with Supabase field data
462
- context = request.field_context or {}
463
-
464
- # Fetch field data from Supabase if user_id provided
465
- if request.user_id:
466
- field_context = supabase.get_field_context(request.user_id)
467
- if field_context:
468
- context.update({
469
- "user_field": field_context,
470
- "crop_type": field_context.get("crop_type"),
471
- "field_name": field_context.get("field_name"),
472
- "area_acres": field_context.get("area_acres"),
473
- "coordinates": field_context.get("coordinates"),
474
- })
475
- logger.info(f"Field context loaded: {field_context.get('field_name')}, {field_context.get('crop_type')}")
476
-
477
- # Also get user profile
478
- user_profile = supabase.get_user_profile(request.user_id)
479
- if user_profile:
480
- context["user_profile"] = user_profile
481
-
482
- # Get all user fields for comparison detection
483
- all_user_fields = supabase.get_user_fields(request.user_id)
484
- field_names = [f.get("name", "") for f in all_user_fields if f.get("name")]
485
- else:
486
- user_profile = None
487
- all_user_fields = []
488
- field_names = []
489
-
490
- # Create persona from user profile for tailored responses
491
- from prompts import create_user_persona, format_weather_context, format_zone_context, format_trend_context, format_conversation_history
492
- persona = create_user_persona(user_profile, all_user_fields)
493
- context["persona"] = persona
494
- logger.info(f"User persona: {persona.get('type')}")
495
-
496
- # Get conversation history for follow-up awareness
497
- conv_history = get_session_history(request.session_id)
498
- context["conversation_history"] = conv_history
499
- if conv_history:
500
- logger.info(f"Loaded {len(conv_history)} previous turns for context")
501
-
502
- # Detect intent first
503
- intent = intent_classifier.classify(request.message)
504
- logger.info(f"Intent: {intent['primary_intent']} ({intent['confidence']})")
505
-
506
- # Check if this is a field comparison query
507
- is_comparison_query = intent_classifier.is_field_comparison_query(
508
- request.message, field_names
509
- )
510
- mentioned_fields = intent_classifier.extract_field_names(
511
- request.message, field_names
512
- )
513
-
514
- if is_comparison_query and len(mentioned_fields) >= 1:
515
- logger.info(f"Field comparison detected - fields: {mentioned_fields}")
516
- context["comparison_requested"] = True
517
- context["mentioned_fields"] = mentioned_fields
518
-
519
- # Fetch satellite data for each mentioned field
520
- comparison_contexts = {}
521
- for field_name in mentioned_fields:
522
- field_data = next(
523
- (f for f in all_user_fields if f.get("name") == field_name),
524
- None
525
- )
526
- if field_data:
527
- # Build coordinates from field data
528
- field_coords = build_field_coordinates(field_data)
529
- if field_coords:
530
- try:
531
- field_satellite = fetch_field_context(
532
- coordinates=field_coords,
533
- crop_type=field_data.get("crop_type", "Wheat"),
534
- area_acres=field_data.get("area_acres", 1.0),
535
- fetch_satellite=True
536
- )
537
- comparison_contexts[field_name] = {
538
- "field_info": field_data,
539
- "satellite_data": field_satellite
540
- }
541
- logger.info(f"Fetched data for comparison field: {field_name}")
542
- except Exception as e:
543
- logger.warning(f"Could not fetch data for {field_name}: {e}")
544
-
545
- context["comparison_fields"] = comparison_contexts
546
-
547
- # Fetch satellite data for technical queries (vegetation, water, nutrient intents)
548
- satellite_intents = [
549
- "vegetation_health", "water_stress", "nutrient_status",
550
- "pest_disease", "forecast_query", "zone_specific", "action_recommendation",
551
- "field_comparison" # Also fetch for comparison queries
552
- ]
553
- if (intent["primary_intent"] in satellite_intents and
554
- context.get("coordinates") and
555
- intent["confidence"] >= 0.3): # Lowered threshold to 0.3
556
-
557
- logger.info("Fetching satellite data from HF Space APIs...")
558
- try:
559
- satellite_context = fetch_field_context(
560
- coordinates=context.get("coordinates"),
561
- crop_type=context.get("crop_type", "Wheat"),
562
- area_acres=context.get("area_acres", 1.0),
563
- fetch_satellite=True
564
- )
565
- context.update(satellite_context)
566
- logger.info(f"Satellite data loaded: {list(satellite_context.keys())}")
567
- except Exception as e:
568
- logger.warning(f"Could not fetch satellite data: {e}")
569
-
570
- # Determine if we need full reasoning or simple response
571
- # DEFAULT: Use simple mode (1 call) to conserve API quota
572
- # Full reasoning (5 calls) only for: deep analysis, diagnosis, or explicit comparison
573
- full_reasoning_keywords = [
574
- "diagnose", "diagnosis", "deep analysis", "detailed", "compare fields",
575
- "why is", "investigate", "root cause", "analyze thoroughly"
576
- ]
577
- message_lower = request.message.lower()
578
-
579
- use_full_reasoning = (
580
- intent["confidence"] >= 0.7 and # Raised from 0.4 - need high confidence
581
- context and
582
- intent["primary_intent"] not in ["general_query"] and
583
- any(kw in message_lower for kw in full_reasoning_keywords) # Explicit request needed
584
- )
585
-
586
- if use_full_reasoning:
587
- # Full multi-stage reasoning (5 API calls)
588
- logger.info("Using multi-stage reasoning pipeline (explicit request)")
589
- response_text, reasoning_trace = reasoning_engine.process_query(
590
- query=request.message,
591
- context=context
592
- )
593
- # Extract context priority info from trace
594
- context_priority = reasoning_trace.get("context_priority_used", {})
595
- diagnosis = reasoning_trace.get("stages", {}).get("confirmation", {}).get("final")
596
- confidence = reasoning_trace.get("stages", {}).get("confirmation", {}).get("confidence", 0.7)
597
- suggested_followups = reasoning_trace.get("suggested_followups", [])
598
- else:
599
- # Simple single-stage response (1 API call) - DEFAULT
600
- logger.info("Using simple response mode (API-efficient)")
601
- response_text = simple_reason(
602
- query=request.message,
603
- context=context,
604
- llm_caller=call_gemini_api
605
- )
606
-
607
- # Check if rate limited or API exhausted - use fallback
608
- if response_text in ["RATE_LIMITED", "API_EXHAUSTED"]:
609
- logger.warning("Using fallback response due to rate limit")
610
- response_text = generate_fallback_response(request.message, context)
611
-
612
- reasoning_trace = {
613
- "mode": "simple",
614
- "intent": intent,
615
- "field_context": context.get("user_field") if context else None,
616
- "rate_limit_status": get_rate_limit_status()
617
- }
618
- context_priority = {}
619
- diagnosis = None
620
- confidence = 0.5
621
- # Generate simple followups
622
- from prompts import generate_followup_questions
623
- suggested_followups = generate_followup_questions(
624
- intent["primary_intent"],
625
- "general"
626
- )
627
 
628
  # Save assistant response
629
  assistant_msg_id = supabase.add_message(
630
  session_id=request.session_id,
631
  role="assistant",
632
- content=response_text,
633
- context_used=list(context_priority.get("priority_1", []))
634
- )
635
-
636
- # Save to session history for follow-up awareness
637
- add_to_session_history(
638
- request.session_id, "user", request.message,
639
- intent=intent["primary_intent"]
640
- )
641
- add_to_session_history(
642
- request.session_id, "assistant", response_text,
643
- diagnosis=diagnosis
644
  )
645
 
646
- # Update session timestamp
647
  supabase.update_session_timestamp(request.session_id)
648
-
649
  logger.info(f"Response generated - {len(response_text)} chars")
650
 
651
- # Build spec-compliant response
652
  return ChatResponse(
653
- response=ResponseContent(
654
- message=response_text,
655
- confidence=confidence,
656
- diagnosis=diagnosis
657
- ),
658
  session_id=request.session_id,
659
  message_id=assistant_msg_id,
660
- timestamp=datetime.now().isoformat(),
661
- reasoning_trace=reasoning_trace,
662
- context_priority_used=ContextPriorityUsed(
663
- priority_1=context_priority.get("priority_1", []),
664
- priority_2=context_priority.get("priority_2", []),
665
- priority_3=context_priority.get("priority_3", []),
666
- priority_4=context_priority.get("priority_4", [])
667
- ) if context_priority else None,
668
- suggested_followups=suggested_followups
669
  )
670
 
671
  except Exception as e:
@@ -673,300 +305,84 @@ async def chat(request: ChatRequest):
673
  logger.error(traceback.format_exc())
674
  raise HTTPException(500, str(e))
675
 
676
-
677
- # ============================================================================
678
- # STREAMING CHAT ENDPOINT (SSE)
679
- # ============================================================================
680
  @app.post("/chat/stream")
681
  async def chat_stream(request: ChatRequest):
682
- """
683
- Send a message and get AI response streamed via Server-Sent Events.
684
- Provides typewriter-style text reveal for better UX.
685
- """
686
  logger.info(f"Stream chat request - Session: {request.session_id}")
687
 
688
  try:
689
- # Save user message first
690
- user_msg_id = supabase.add_message(
 
691
  session_id=request.session_id,
692
  role="user",
693
  content=request.message
694
  )
695
 
696
- # Build context (same as regular chat)
697
- context = request.field_context or {}
698
 
699
- if request.user_id:
700
- field_context = supabase.get_field_context(request.user_id)
701
- if field_context:
702
- context.update({
703
- "user_field": field_context,
704
- "crop_type": field_context.get("crop_type"),
705
- "field_name": field_context.get("field_name"),
706
- "area_acres": field_context.get("area_acres"),
707
- "coordinates": field_context.get("coordinates"),
708
- })
709
-
710
- user_profile = supabase.get_user_profile(request.user_id)
711
- if user_profile:
712
- context["user_profile"] = user_profile
713
-
714
- all_user_fields = supabase.get_user_fields(request.user_id)
715
- else:
716
- user_profile = None
717
- all_user_fields = []
718
-
719
- from prompts import create_user_persona
720
- persona = create_user_persona(user_profile, all_user_fields)
721
- context["persona"] = persona
722
-
723
- # Detect intent
724
- intent = intent_classifier.classify(request.message)
725
- logger.info(f"Stream - Intent: {intent['primary_intent']} ({intent['confidence']})")
726
-
727
- # Fetch satellite data if needed
728
- satellite_intents = [
729
- "vegetation_health", "water_stress", "nutrient_status",
730
- "pest_disease", "forecast_query", "zone_specific", "action_recommendation"
731
- ]
732
- if (intent["primary_intent"] in satellite_intents and
733
- context.get("coordinates") and
734
- intent["confidence"] >= 0.3):
735
- try:
736
- satellite_context = fetch_field_context(
737
- coordinates=context.get("coordinates"),
738
- crop_type=context.get("crop_type", "Wheat"),
739
- area_acres=context.get("area_acres", 1.0),
740
- fetch_satellite=True
741
- )
742
- context.update(satellite_context)
743
- except Exception as e:
744
- logger.warning(f"Could not fetch satellite data: {e}")
745
-
746
- # Determine reasoning mode - DEFAULT to simple (1 API call)
747
- # Full reasoning (5 calls) only for explicit deep analysis requests
748
- full_reasoning_keywords = [
749
- "diagnose", "diagnosis", "deep analysis", "detailed", "compare fields",
750
- "why is", "investigate", "root cause", "analyze thoroughly"
751
- ]
752
- message_lower = request.message.lower()
753
-
754
- use_full_reasoning = (
755
- intent["confidence"] >= 0.7 and
756
- context and
757
- intent["primary_intent"] not in ["general_query"] and
758
- any(kw in message_lower for kw in full_reasoning_keywords)
759
- )
760
-
761
- # Get full response first (reasoning happens here)
762
- if use_full_reasoning:
763
- logger.info("Stream using multi-stage reasoning (explicit request)")
764
- response_text, reasoning_trace = reasoning_engine.process_query(
765
- query=request.message,
766
- context=context
767
- )
768
- diagnosis = reasoning_trace.get("stages", {}).get("confirmation", {}).get("final")
769
- confidence = reasoning_trace.get("stages", {}).get("confirmation", {}).get("confidence", 0.7)
770
- else:
771
- logger.info("Stream using simple response mode (API-efficient)")
772
- response_text = simple_reason(
773
- query=request.message,
774
- context=context,
775
- llm_caller=call_gemini_api
776
- )
777
-
778
- # Check if rate limited - use fallback
779
- if response_text in ["RATE_LIMITED", "API_EXHAUSTED"]:
780
- logger.warning("Stream: Using fallback due to rate limit")
781
- response_text = generate_fallback_response(request.message, context)
782
-
783
- reasoning_trace = {"mode": "simple", "intent": intent, "rate_limit": get_rate_limit_status()}
784
- diagnosis = None
785
- confidence = 0.5
786
-
787
- # Save assistant message
788
  assistant_msg_id = supabase.add_message(
789
  session_id=request.session_id,
790
  role="assistant",
791
  content=response_text
792
  )
793
 
794
- # Update session history
795
- add_to_session_history(request.session_id, "user", request.message, intent=intent["primary_intent"])
796
- add_to_session_history(request.session_id, "assistant", response_text, diagnosis=diagnosis)
797
  supabase.update_session_timestamp(request.session_id)
798
 
799
- # Generate followups
800
- from prompts import generate_followup_questions
801
- suggested_followups = generate_followup_questions(intent["primary_intent"], diagnosis or "general")
802
-
803
- # Stream the response in chunks
804
  async def generate_stream():
805
- """Yield response chunks with delays for typewriter effect."""
806
- chunk_size = 12 # Characters per chunk
807
- delay = 0.04 # 40ms between chunks
808
 
809
- # First, send metadata
810
- metadata = {
811
- "type": "metadata",
812
- "session_id": request.session_id,
813
- "message_id": assistant_msg_id,
814
- "confidence": confidence,
815
- "diagnosis": diagnosis,
816
- "suggested_followups": suggested_followups
817
- }
818
- yield f"data: {json.dumps(metadata)}\n\n"
819
 
820
- # Stream the text in chunks
821
  for i in range(0, len(response_text), chunk_size):
822
- chunk = response_text[i:i + chunk_size]
823
  yield f"data: {json.dumps({'type': 'chunk', 'text': chunk})}\n\n"
824
  await asyncio.sleep(delay)
825
 
826
- # Send completion signal
827
  yield f"data: {json.dumps({'type': 'done', 'full_text': response_text})}\n\n"
828
 
829
- return StreamingResponse(
830
- generate_stream(),
831
- media_type="text/event-stream",
832
- headers={
833
- "Cache-Control": "no-cache",
834
- "Connection": "keep-alive",
835
- "X-Accel-Buffering": "no" # Disable nginx buffering
836
- }
837
- )
838
 
839
  except Exception as e:
840
  logger.error(f"Stream chat error: {e}")
841
- logger.error(traceback.format_exc())
842
-
843
- async def error_stream():
844
- yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
845
-
846
- return StreamingResponse(error_stream(), media_type="text/event-stream")
847
 
848
- @app.get("/session/{session_id}/history", response_model=HistoryResponse)
849
  async def get_history(session_id: str):
850
  """Get conversation history for a session."""
851
- logger.info(f"Loading history for session: {session_id}")
852
-
853
  try:
854
  messages = supabase.get_messages(session_id)
855
-
856
- return HistoryResponse(
857
- session_id=session_id,
858
- messages=[
859
- MessageModel(
860
- id=msg.get("id", ""),
861
- role=msg.get("role", ""),
862
- content=msg.get("content", ""),
863
- created_at=msg.get("created_at", "")
864
- )
865
- for msg in messages
866
- ]
867
- )
868
  except Exception as e:
869
- logger.error(f"History error: {e}")
870
  raise HTTPException(500, str(e))
871
 
872
-
873
  @app.get("/sessions/{user_id}")
874
  async def list_sessions(user_id: str):
875
  """List all chat sessions for a user."""
876
  logger.info(f"Listing sessions for user: {user_id}")
877
-
878
  try:
879
  sessions = supabase.get_user_sessions(user_id)
880
-
881
- return {
882
- "user_id": user_id,
883
- "sessions": sessions,
884
- "count": len(sessions)
885
- }
886
  except Exception as e:
887
- logger.error(f"List sessions error: {e}")
888
  raise HTTPException(500, str(e))
889
 
890
-
891
  @app.delete("/session/{session_id}")
892
  async def delete_session(session_id: str):
893
- """Delete a chat session and its messages."""
894
  logger.info(f"Deleting session: {session_id}")
895
-
896
  try:
897
  supabase.delete_session(session_id)
898
  return {"status": "deleted", "session_id": session_id}
899
  except Exception as e:
900
- logger.error(f"Delete error: {e}")
901
  raise HTTPException(500, str(e))
902
 
903
-
904
- # Intent analysis endpoint (for debugging)
905
- @app.post("/analyze-intent")
906
- async def analyze_intent(request: Dict[str, str]):
907
- """Analyze query intent without generating response."""
908
- query = request.get("query", "")
909
- intent = intent_classifier.classify(query)
910
- return {
911
- "query": query,
912
- "intent": intent
913
- }
914
-
915
-
916
- # Satellite context endpoint (for debugging and direct access)
917
- @app.post("/satellite-context")
918
- async def get_satellite_context(request: Dict[str, Any]):
919
- """
920
- Fetch satellite band data from HF Space APIs.
921
-
922
- Request body:
923
- {
924
- "user_id": "firebase_or_anon_id",
925
- "coordinates": {"center_lat": 30.9, "center_lon": 75.8, "bbox": [...]},
926
- "crop_type": "Wheat",
927
- "area_acres": 1.0
928
- }
929
- """
930
- logger.info("Fetching satellite context...")
931
-
932
- user_id = request.get("user_id")
933
- coordinates = request.get("coordinates")
934
- crop_type = request.get("crop_type", "Wheat")
935
- area_acres = request.get("area_acres", 1.0)
936
-
937
- # If user_id provided, fetch from Supabase
938
- if user_id and not coordinates:
939
- field_context = supabase.get_field_context(user_id)
940
- if field_context:
941
- coordinates = field_context.get("coordinates")
942
- crop_type = field_context.get("crop_type", crop_type)
943
- area_acres = field_context.get("area_acres", area_acres)
944
-
945
- if not coordinates:
946
- return {"error": "No coordinates available", "data": None}
947
-
948
- try:
949
- aggregator = ContextAggregator(timeout=60)
950
- raw_context = aggregator.fetch_full_context(
951
- coordinates=coordinates,
952
- crop_type=crop_type,
953
- area_acres=area_acres
954
- )
955
- formatted_context = aggregator.format_for_llm(raw_context)
956
-
957
- return {
958
- "success": True,
959
- "raw_context": raw_context,
960
- "formatted_context": formatted_context,
961
- "timestamp": datetime.now().isoformat()
962
- }
963
- except Exception as e:
964
- logger.error(f"Satellite context error: {e}")
965
- return {"success": False, "error": str(e)}
966
-
967
-
968
  if __name__ == "__main__":
969
  import uvicorn
970
- logger.info("Starting AGROW Chatbot Service v2.0")
971
  uvicorn.run(app, host="0.0.0.0", port=7860)
972
-
 
1
  """
2
+ AGROW Agricultural Chatbot Service (Simplified)
3
+ ================================================
4
  AI-powered agricultural advisor with:
5
+ - Single LLM call (no multi-stage reasoning)
6
+ - Multi-API key fallback
7
  - Supabase conversation storage
8
  """
9
 
 
23
  import asyncio
24
 
25
  from supabase_client import SupabaseClient
 
 
 
26
 
27
  # ============================================================================
28
  # LOGGING
 
37
  # ============================================================================
38
  # GEMINI SETUP - Multi-API Key Fallback System
39
  # ============================================================================
 
 
40
  def load_gemini_api_keys() -> List[str]:
41
  """Load all available Gemini API keys from environment."""
42
  keys = []
 
 
43
  primary = os.environ.get("GEMINI_API_KEY")
44
  if primary:
45
  keys.append(primary)
 
 
46
  for i in range(1, 6):
47
  key = os.environ.get(f"GEMINI_API_KEY_{i}")
48
  if key and key not in keys:
49
  keys.append(key)
 
50
  return keys
51
 
52
  GEMINI_API_KEYS = load_gemini_api_keys()
53
+ current_key_index = 0
 
54
  logger.info(f"Loaded {len(GEMINI_API_KEYS)} Gemini API key(s)")
55
 
56
+ # Find working model
57
  def get_available_model(api_key: str):
58
+ """Try to find an available Gemini model."""
59
+ models_to_try = ["gemini-2.0-flash", "gemini-1.5-flash", "gemini-1.5-pro"]
 
 
 
 
 
 
 
60
  if not api_key:
61
  return None, None
 
62
  for model in models_to_try:
63
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
64
+ try:
65
+ resp = requests.post(
66
+ f"{url}?key={api_key}",
67
+ json={"contents": [{"parts": [{"text": "test"}]}]},
68
+ timeout=10
69
+ )
70
+ if resp.status_code in [200, 429]:
71
+ logger.info(f"Found working model: {model}")
72
+ return url, model
73
+ except:
74
+ pass
 
 
75
  return None, None
76
 
 
77
  GEMINI_URL, GEMINI_MODEL = None, None
78
  if GEMINI_API_KEYS:
79
  GEMINI_URL, GEMINI_MODEL = get_available_model(GEMINI_API_KEYS[0])
 
 
80
  logger.info(f"Using Gemini model: {GEMINI_MODEL}")
 
 
 
 
 
 
 
 
81
 
82
  # Supabase client
83
  supabase = SupabaseClient()
84
 
85
  # ============================================================================
86
+ # SIMPLE SYSTEM PROMPT
87
  # ============================================================================
88
+ SYSTEM_PROMPT = """You are AGROW AI, an expert agricultural advisor for Indian farmers.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ SPECIALIZATIONS:
91
+ - Crop health analysis and diagnosis
92
+ - Irrigation and water management
93
+ - Pest and disease identification
94
+ - Weather-based farming advice
95
+ - Soil health recommendations
96
 
97
+ COMMUNICATION STYLE:
98
+ - Use simple, practical language farmers understand
99
+ - Give actionable, prioritized recommendations
100
+ - Reference local conditions when available
101
+ - Be concise but thorough
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ When you lack specific data, provide general guidance based on the query."""
104
 
105
  # ============================================================================
106
+ # FASTAPI SETUP
107
  # ============================================================================
108
+ app = FastAPI(title="AGROW Chatbot Service", version="2.0")
 
 
 
 
 
109
  app.add_middleware(
110
  CORSMiddleware,
111
  allow_origins=["*"],
 
114
  allow_headers=["*"],
115
  )
116
 
117
+ print("=" * 50)
118
+ print(f"===== Application Startup at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
119
+ print("=" * 50)
120
+
121
  # ============================================================================
122
+ # REQUEST/RESPONSE MODELS
123
  # ============================================================================
124
  class ChatRequest(BaseModel):
125
  session_id: str
126
  message: str
127
  user_id: Optional[str] = None
128
+ field_id: Optional[str] = None
 
129
 
130
  class ResponseContent(BaseModel):
131
  message: str
132
+ confidence: float = 0.8
 
 
 
 
 
 
 
133
 
134
  class ChatResponse(BaseModel):
 
135
  response: ResponseContent
136
  session_id: str
137
  message_id: str
138
  timestamp: str
 
 
 
139
 
140
  class SessionRequest(BaseModel):
141
  user_id: str
 
146
  title: str
147
  created_at: str
148
 
149
+ # ============================================================================
150
+ # LLM CALL WITH FALLBACK
151
+ # ============================================================================
152
+ def get_next_api_key():
153
+ global current_key_index
154
+ current_key_index = (current_key_index + 1) % len(GEMINI_API_KEYS)
155
+ logger.info(f"Rotated to API key {current_key_index + 1}/{len(GEMINI_API_KEYS)}")
156
+ return GEMINI_API_KEYS[current_key_index]
157
 
158
+ def call_gemini_api(prompt: str) -> str:
159
+ """Call Gemini API with fallback across multiple API keys."""
160
+ global current_key_index
161
+
162
+ if not GEMINI_API_KEYS:
163
+ return "Please configure GEMINI_API_KEY for real responses."
164
+
165
+ keys_tried = 0
166
+ while keys_tried < len(GEMINI_API_KEYS):
167
+ current_key = GEMINI_API_KEYS[current_key_index]
168
+ url = f"{GEMINI_URL}?key={current_key}"
169
+
170
+ try:
171
+ response = requests.post(
172
+ url,
173
+ headers={"Content-Type": "application/json"},
174
+ json={
175
+ "contents": [{"parts": [{"text": prompt}]}],
176
+ "generationConfig": {
177
+ "temperature": 0.7,
178
+ "maxOutputTokens": 1024,
179
+ }
180
+ },
181
+ timeout=60
182
+ )
183
+
184
+ if response.status_code == 200:
185
+ data = response.json()
186
+ if "candidates" in data and len(data["candidates"]) > 0:
187
+ return data["candidates"][0]["content"]["parts"][0]["text"]
188
+ return "No response generated."
189
+
190
+ elif response.status_code in [429, 403, 500, 502, 503]:
191
+ logger.warning(f"API key {current_key_index + 1} got {response.status_code}, rotating...")
192
+ get_next_api_key()
193
+ keys_tried += 1
194
+ else:
195
+ logger.error(f"Gemini API error: {response.status_code}")
196
+ return f"API error: {response.status_code}"
197
+
198
+ except Exception as e:
199
+ logger.error(f"Gemini request error: {e}")
200
+ get_next_api_key()
201
+ keys_tried += 1
202
+
203
+ return "All API keys exhausted. Please try again later."
204
+
205
+ # ============================================================================
206
+ # SIMPLE RESPONSE GENERATOR (SINGLE LLM CALL)
207
+ # ============================================================================
208
+ def generate_simple_response(query: str, history: List[Dict] = None) -> str:
209
+ """Generate response with a single LLM call."""
210
+
211
+ # Build conversation context
212
+ history_text = ""
213
+ if history and len(history) > 0:
214
+ recent = history[-6:] # Last 3 exchanges
215
+ for msg in recent:
216
+ role = "User" if msg.get("role") == "user" else "Assistant"
217
+ history_text += f"{role}: {msg.get('content', '')[:200]}\n"
218
+
219
+ prompt = f"""{SYSTEM_PROMPT}
220
+
221
+ {f"Recent conversation:{chr(10)}{history_text}" if history_text else ""}
222
+
223
+ User Query: {query}
224
 
225
+ Provide a helpful, actionable response:"""
226
+
227
+ return call_gemini_api(prompt)
228
 
229
  # ============================================================================
230
  # API ENDPOINTS
 
233
  async def root():
234
  return {
235
  "service": "AGROW Chatbot Service",
236
+ "version": "2.0-simple",
237
+ "status": "running"
 
 
 
 
 
 
238
  }
239
 
240
  @app.get("/health")
241
  async def health():
242
  return {
243
  "status": "healthy",
244
+ "gemini_configured": GEMINI_URL is not None,
245
+ "api_keys_loaded": len(GEMINI_API_KEYS)
 
246
  }
247
 
 
248
  @app.post("/session/new", response_model=SessionResponse)
249
  async def create_session(request: SessionRequest):
250
  """Create a new chat session."""
251
  logger.info(f"Creating new session for user: {request.user_id}")
 
252
  try:
253
  session = supabase.create_session(
254
  user_id=request.user_id,
255
  title=request.title or "New Conversation"
256
  )
 
257
  return SessionResponse(
258
  session_id=session["id"],
259
  title=session["title"],
 
263
  logger.error(f"Failed to create session: {e}")
264
  raise HTTPException(500, str(e))
265
 
 
266
  @app.post("/chat", response_model=ChatResponse)
267
  async def chat(request: ChatRequest):
268
+ """Send a message and get AI response (single LLM call)."""
269
+ logger.info(f"Chat request - Session: {request.session_id}")
270
 
271
  try:
272
+ # Get conversation history
273
+ history = supabase.get_messages(request.session_id) or []
274
 
275
  # Save user message
276
  user_msg_id = supabase.add_message(
 
279
  content=request.message
280
  )
281
 
282
+ # Generate response (SINGLE LLM CALL)
283
+ logger.info("Generating response (single LLM call)")
284
+ response_text = generate_simple_response(request.message, history)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
  # Save assistant response
287
  assistant_msg_id = supabase.add_message(
288
  session_id=request.session_id,
289
  role="assistant",
290
+ content=response_text
 
 
 
 
 
 
 
 
 
 
 
291
  )
292
 
 
293
  supabase.update_session_timestamp(request.session_id)
 
294
  logger.info(f"Response generated - {len(response_text)} chars")
295
 
 
296
  return ChatResponse(
297
+ response=ResponseContent(message=response_text),
 
 
 
 
298
  session_id=request.session_id,
299
  message_id=assistant_msg_id,
300
+ timestamp=datetime.now().isoformat()
 
 
 
 
 
 
 
 
301
  )
302
 
303
  except Exception as e:
 
305
  logger.error(traceback.format_exc())
306
  raise HTTPException(500, str(e))
307
 
 
 
 
 
308
  @app.post("/chat/stream")
309
  async def chat_stream(request: ChatRequest):
310
+ """Streaming chat endpoint (also uses single LLM call, then streams)."""
 
 
 
311
  logger.info(f"Stream chat request - Session: {request.session_id}")
312
 
313
  try:
314
+ history = supabase.get_messages(request.session_id) or []
315
+
316
+ supabase.add_message(
317
  session_id=request.session_id,
318
  role="user",
319
  content=request.message
320
  )
321
 
322
+ # Generate full response first
323
+ response_text = generate_simple_response(request.message, history)
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  assistant_msg_id = supabase.add_message(
326
  session_id=request.session_id,
327
  role="assistant",
328
  content=response_text
329
  )
330
 
 
 
 
331
  supabase.update_session_timestamp(request.session_id)
332
 
333
+ # Stream response in chunks
 
 
 
 
334
  async def generate_stream():
335
+ chunk_size = 15
336
+ delay = 0.03
 
337
 
338
+ # Send metadata
339
+ yield f"data: {json.dumps({'type': 'metadata', 'session_id': request.session_id, 'message_id': assistant_msg_id})}\n\n"
 
 
 
 
 
 
 
 
340
 
341
+ # Stream text chunks
342
  for i in range(0, len(response_text), chunk_size):
343
+ chunk = response_text[i:i+chunk_size]
344
  yield f"data: {json.dumps({'type': 'chunk', 'text': chunk})}\n\n"
345
  await asyncio.sleep(delay)
346
 
347
+ # Done signal
348
  yield f"data: {json.dumps({'type': 'done', 'full_text': response_text})}\n\n"
349
 
350
+ return StreamingResponse(generate_stream(), media_type="text/event-stream")
 
 
 
 
 
 
 
 
351
 
352
  except Exception as e:
353
  logger.error(f"Stream chat error: {e}")
354
+ raise HTTPException(500, str(e))
 
 
 
 
 
355
 
356
+ @app.get("/session/{session_id}/history")
357
  async def get_history(session_id: str):
358
  """Get conversation history for a session."""
 
 
359
  try:
360
  messages = supabase.get_messages(session_id)
361
+ return {"session_id": session_id, "messages": messages}
 
 
 
 
 
 
 
 
 
 
 
 
362
  except Exception as e:
 
363
  raise HTTPException(500, str(e))
364
 
 
365
  @app.get("/sessions/{user_id}")
366
  async def list_sessions(user_id: str):
367
  """List all chat sessions for a user."""
368
  logger.info(f"Listing sessions for user: {user_id}")
 
369
  try:
370
  sessions = supabase.get_user_sessions(user_id)
371
+ return {"user_id": user_id, "sessions": sessions, "count": len(sessions)}
 
 
 
 
 
372
  except Exception as e:
 
373
  raise HTTPException(500, str(e))
374
 
 
375
  @app.delete("/session/{session_id}")
376
  async def delete_session(session_id: str):
377
+ """Delete a chat session."""
378
  logger.info(f"Deleting session: {session_id}")
 
379
  try:
380
  supabase.delete_session(session_id)
381
  return {"status": "deleted", "session_id": session_id}
382
  except Exception as e:
 
383
  raise HTTPException(500, str(e))
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  if __name__ == "__main__":
386
  import uvicorn
387
+ logger.info("Starting AGROW Chatbot Service v2.0 (Simple)")
388
  uvicorn.run(app, host="0.0.0.0", port=7860)