Aniket2006 commited on
Commit
b3afc51
·
0 Parent(s):

Initial commit: AGROW Chatbot with Gemini LLM

Browse files
Files changed (7) hide show
  1. Dockerfile +18 -0
  2. README.md +42 -0
  3. app.py +312 -0
  4. prompts.py +109 -0
  5. requirements.txt +6 -0
  6. supabase_client.py +232 -0
  7. supabase_schema.sql +96 -0
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy application files
10
+ COPY app.py .
11
+ COPY supabase_client.py .
12
+ COPY prompts.py .
13
+
14
+ # Expose port
15
+ EXPOSE 7860
16
+
17
+ # Run the application
18
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AGROW Chatbot
3
+ emoji: 🌾
4
+ colorFrom: green
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # AGROW Agricultural Chatbot
12
+
13
+ AI-powered agricultural advisor using Gemini LLM with Supabase conversation storage.
14
+
15
+ ## Features
16
+ - Gemini 1.5 Flash for intelligent responses
17
+ - Agricultural expertise (crop health, soil, weather)
18
+ - Conversation history persistence
19
+ - Pipeline context integration
20
+
21
+ ## API Endpoints
22
+
23
+ | Endpoint | Method | Description |
24
+ |----------|--------|-------------|
25
+ | `/chat` | POST | Send message, get AI response |
26
+ | `/session/new` | POST | Create new chat session |
27
+ | `/session/{id}/history` | GET | Get conversation history |
28
+ | `/sessions/{user_id}` | GET | List user's sessions |
29
+
30
+ ## Environment Variables
31
+
32
+ - `GEMINI_API_KEY` - Google Gemini API key
33
+ - `SUPABASE_URL` - Supabase project URL
34
+ - `SUPABASE_KEY` - Supabase anon key
35
+
36
+ ## Example Request
37
+
38
+ ```bash
39
+ curl -X POST https://YOUR-SPACE.hf.space/chat \
40
+ -H "Content-Type: application/json" \
41
+ -d '{"session_id": "abc123", "message": "What is my crop health status?"}'
42
+ ```
app.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
11
+ import json
12
+ import logging
13
+ import uuid
14
+ from datetime import datetime
15
+ from typing import Optional, List, Dict, Any
16
+ import traceback
17
+
18
+ from fastapi import FastAPI, HTTPException
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from pydantic import BaseModel
21
+ import google.generativeai as genai
22
+
23
+ from supabase_client import SupabaseClient
24
+ from prompts import SYSTEM_PROMPT, build_context_prompt
25
+
26
+ # ============================================================================
27
+ # LOGGING
28
+ # ============================================================================
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format='[%(asctime)s] %(levelname)s: %(message)s',
32
+ datefmt='%H:%M:%S'
33
+ )
34
+ logger = logging.getLogger("ChatbotService")
35
+
36
+ # ============================================================================
37
+ # GEMINI SETUP
38
+ # ============================================================================
39
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
40
+ if GEMINI_API_KEY:
41
+ genai.configure(api_key=GEMINI_API_KEY)
42
+ model = genai.GenerativeModel('gemini-1.5-flash')
43
+ logger.info("Gemini API configured successfully")
44
+ else:
45
+ model = None
46
+ logger.warning("GEMINI_API_KEY not set - chatbot will return mock responses")
47
+
48
+ # Supabase client
49
+ supabase = SupabaseClient()
50
+
51
+ # ============================================================================
52
+ # FASTAPI
53
+ # ============================================================================
54
+ app = FastAPI(
55
+ title="AGROW Chatbot Service",
56
+ description="AI agricultural advisor with conversation storage",
57
+ version="1.0.0"
58
+ )
59
+
60
+ app.add_middleware(
61
+ CORSMiddleware,
62
+ allow_origins=["*"],
63
+ allow_credentials=True,
64
+ allow_methods=["*"],
65
+ allow_headers=["*"],
66
+ )
67
+
68
+ # ============================================================================
69
+ # REQUEST/RESPONSE MODELS
70
+ # ============================================================================
71
+ class ChatRequest(BaseModel):
72
+ session_id: str
73
+ message: str
74
+ user_id: Optional[str] = None
75
+ field_context: Optional[Dict[str, Any]] = None # Pipeline data
76
+
77
+ class ChatResponse(BaseModel):
78
+ response: str
79
+ session_id: str
80
+ message_id: str
81
+ context_used: List[str]
82
+ timestamp: str
83
+
84
+ class SessionRequest(BaseModel):
85
+ user_id: str
86
+ title: Optional[str] = None
87
+
88
+ class SessionResponse(BaseModel):
89
+ session_id: str
90
+ title: str
91
+ created_at: str
92
+
93
+ class MessageModel(BaseModel):
94
+ id: str
95
+ role: str
96
+ content: str
97
+ created_at: str
98
+
99
+ class HistoryResponse(BaseModel):
100
+ session_id: str
101
+ messages: List[MessageModel]
102
+
103
+ class SessionListItem(BaseModel):
104
+ id: str
105
+ title: str
106
+ created_at: str
107
+ updated_at: str
108
+ message_count: int
109
+
110
+ # ============================================================================
111
+ # HELPER FUNCTIONS
112
+ # ============================================================================
113
+ def generate_response(user_message: str, history: List[Dict], context: Optional[Dict] = None) -> tuple[str, List[str]]:
114
+ """Generate AI response using Gemini."""
115
+ context_used = []
116
+
117
+ # Build conversation history for context
118
+ conversation = []
119
+ for msg in history[-10:]: # Last 10 messages for context
120
+ role = "user" if msg.get("role") == "user" else "model"
121
+ conversation.append({"role": role, "parts": [msg.get("content", "")]})
122
+
123
+ # Build context prompt if pipeline data available
124
+ context_prompt = ""
125
+ if context:
126
+ context_prompt = build_context_prompt(context)
127
+ context_used = list(context.keys())
128
+
129
+ # Combine system prompt with context
130
+ full_system = SYSTEM_PROMPT
131
+ if context_prompt:
132
+ full_system += f"\n\n## Current Field Analysis:\n{context_prompt}"
133
+
134
+ if model is None:
135
+ # Mock response if no API key
136
+ return f"I received your question: '{user_message}'. Please configure GEMINI_API_KEY for real responses.", []
137
+
138
+ try:
139
+ # Create chat with system instruction
140
+ chat = model.start_chat(history=conversation)
141
+
142
+ # Generate response
143
+ response = chat.send_message(
144
+ f"[System: {full_system}]\n\nUser: {user_message}",
145
+ generation_config=genai.types.GenerationConfig(
146
+ temperature=0.7,
147
+ max_output_tokens=1024,
148
+ )
149
+ )
150
+
151
+ return response.text, context_used
152
+
153
+ except Exception as e:
154
+ logger.error(f"Gemini error: {e}")
155
+ return f"I apologize, but I encountered an error. Please try again. Error: {str(e)}", []
156
+
157
+ # ============================================================================
158
+ # API ENDPOINTS
159
+ # ============================================================================
160
+ @app.get("/")
161
+ async def root():
162
+ return {
163
+ "service": "AGROW Chatbot Service",
164
+ "version": "1.0.0",
165
+ "endpoints": {
166
+ "/chat": "POST - Send message, get AI response",
167
+ "/session/new": "POST - Create new chat session",
168
+ "/session/{id}/history": "GET - Get conversation history",
169
+ "/sessions/{user_id}": "GET - List user's sessions"
170
+ }
171
+ }
172
+
173
+ @app.get("/health")
174
+ async def health():
175
+ return {
176
+ "status": "healthy",
177
+ "gemini_configured": model is not None,
178
+ "supabase_configured": supabase.is_configured()
179
+ }
180
+
181
+
182
+ @app.post("/session/new", response_model=SessionResponse)
183
+ async def create_session(request: SessionRequest):
184
+ """Create a new chat session."""
185
+ logger.info(f"Creating new session for user: {request.user_id}")
186
+
187
+ try:
188
+ session = supabase.create_session(
189
+ user_id=request.user_id,
190
+ title=request.title or "New Conversation"
191
+ )
192
+
193
+ return SessionResponse(
194
+ session_id=session["id"],
195
+ title=session["title"],
196
+ created_at=session["created_at"]
197
+ )
198
+ except Exception as e:
199
+ logger.error(f"Failed to create session: {e}")
200
+ raise HTTPException(500, str(e))
201
+
202
+
203
+ @app.post("/chat", response_model=ChatResponse)
204
+ async def chat(request: ChatRequest):
205
+ """Send a message and get AI response."""
206
+ logger.info(f"Chat request - Session: {request.session_id}, Message: {request.message[:50]}...")
207
+
208
+ try:
209
+ # Load conversation history
210
+ history = supabase.get_messages(request.session_id)
211
+
212
+ # Save user message
213
+ user_msg_id = supabase.add_message(
214
+ session_id=request.session_id,
215
+ role="user",
216
+ content=request.message
217
+ )
218
+
219
+ # Generate AI response
220
+ response_text, context_used = generate_response(
221
+ request.message,
222
+ history,
223
+ request.field_context
224
+ )
225
+
226
+ # Save assistant response
227
+ assistant_msg_id = supabase.add_message(
228
+ session_id=request.session_id,
229
+ role="assistant",
230
+ content=response_text,
231
+ context_used=context_used
232
+ )
233
+
234
+ # Update session timestamp
235
+ supabase.update_session_timestamp(request.session_id)
236
+
237
+ logger.info(f"Response generated - {len(response_text)} chars")
238
+
239
+ return ChatResponse(
240
+ response=response_text,
241
+ session_id=request.session_id,
242
+ message_id=assistant_msg_id,
243
+ context_used=context_used,
244
+ timestamp=datetime.now().isoformat()
245
+ )
246
+
247
+ except Exception as e:
248
+ logger.error(f"Chat error: {e}")
249
+ logger.error(traceback.format_exc())
250
+ raise HTTPException(500, str(e))
251
+
252
+
253
+ @app.get("/session/{session_id}/history", response_model=HistoryResponse)
254
+ async def get_history(session_id: str):
255
+ """Get conversation history for a session."""
256
+ logger.info(f"Loading history for session: {session_id}")
257
+
258
+ try:
259
+ messages = supabase.get_messages(session_id)
260
+
261
+ return HistoryResponse(
262
+ session_id=session_id,
263
+ messages=[
264
+ MessageModel(
265
+ id=msg.get("id", ""),
266
+ role=msg.get("role", ""),
267
+ content=msg.get("content", ""),
268
+ created_at=msg.get("created_at", "")
269
+ )
270
+ for msg in messages
271
+ ]
272
+ )
273
+ except Exception as e:
274
+ logger.error(f"History error: {e}")
275
+ raise HTTPException(500, str(e))
276
+
277
+
278
+ @app.get("/sessions/{user_id}")
279
+ async def list_sessions(user_id: str):
280
+ """List all chat sessions for a user."""
281
+ logger.info(f"Listing sessions for user: {user_id}")
282
+
283
+ try:
284
+ sessions = supabase.get_user_sessions(user_id)
285
+
286
+ return {
287
+ "user_id": user_id,
288
+ "sessions": sessions,
289
+ "count": len(sessions)
290
+ }
291
+ except Exception as e:
292
+ logger.error(f"List sessions error: {e}")
293
+ raise HTTPException(500, str(e))
294
+
295
+
296
+ @app.delete("/session/{session_id}")
297
+ async def delete_session(session_id: str):
298
+ """Delete a chat session and its messages."""
299
+ logger.info(f"Deleting session: {session_id}")
300
+
301
+ try:
302
+ supabase.delete_session(session_id)
303
+ return {"status": "deleted", "session_id": session_id}
304
+ except Exception as e:
305
+ logger.error(f"Delete error: {e}")
306
+ raise HTTPException(500, str(e))
307
+
308
+
309
+ if __name__ == "__main__":
310
+ import uvicorn
311
+ logger.info("Starting AGROW Chatbot Service")
312
+ uvicorn.run(app, host="0.0.0.0", port=7860)
prompts.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM Prompts for Agricultural Advisor
3
+ =====================================
4
+ System prompts and context builders for Gemini.
5
+ """
6
+
7
+ SYSTEM_PROMPT = """You are AGROW AI, an expert agricultural advisor for farmers in India. You help farmers understand their crop health, soil conditions, and provide actionable recommendations.
8
+
9
+ ## Your Expertise:
10
+ - Crop stress analysis using satellite imagery
11
+ - Soil health interpretation (N, P, K, moisture)
12
+ - Vegetation indices (NDVI, NDWI, EVI, SAVI)
13
+ - Weather impact on farming
14
+ - Pest and disease identification
15
+ - Irrigation recommendations
16
+ - Fertilizer application timing
17
+ - Harvest optimization
18
+
19
+ ## Communication Style:
20
+ - Be warm, supportive, and encouraging
21
+ - Use simple language (assume farmer may not know technical terms)
22
+ - When using technical terms, explain them briefly
23
+ - Give practical, actionable advice
24
+ - Consider Indian farming context (seasons, crops, practices)
25
+ - Be concise but thorough
26
+
27
+ ## Response Format:
28
+ - Start with a direct answer to the question
29
+ - Provide 2-3 specific recommendations when relevant
30
+ - End with encouragement or a helpful tip
31
+ - Use bullet points for lists
32
+
33
+ ## Safety:
34
+ - Never recommend chemicals without proper precautions
35
+ - Suggest consulting local agricultural officers for serious issues
36
+ - Recommend soil testing before major fertilizer decisions
37
+ """
38
+
39
+
40
+ def build_context_prompt(context: dict) -> str:
41
+ """Build context prompt from pipeline analysis data."""
42
+ parts = []
43
+
44
+ # Stress Analysis
45
+ if "stress_score" in context:
46
+ score = context["stress_score"]
47
+ level = "low" if score < 0.3 else "moderate" if score < 0.6 else "high"
48
+ parts.append(f"• Crop Stress Level: {level} ({score:.2f}/1.0)")
49
+
50
+ # Vegetation Indices
51
+ if "ndvi" in context:
52
+ ndvi = context["ndvi"]
53
+ health = "excellent" if ndvi > 0.7 else "good" if ndvi > 0.5 else "concerning" if ndvi > 0.3 else "poor"
54
+ parts.append(f"• NDVI (Plant Health): {ndvi:.3f} - {health}")
55
+
56
+ if "ndwi" in context:
57
+ ndwi = context["ndwi"]
58
+ water = "adequate" if ndwi > 0 else "stressed"
59
+ parts.append(f"• NDWI (Water Content): {ndwi:.3f} - {water}")
60
+
61
+ if "evi" in context:
62
+ parts.append(f"• EVI (Vegetation Vigor): {context['evi']:.3f}")
63
+
64
+ # Soil Data
65
+ if "soil" in context:
66
+ soil = context["soil"]
67
+ parts.append(f"• Soil Moisture: {soil.get('moisture', 'N/A')}")
68
+ parts.append(f"• Soil NPK: N={soil.get('nitrogen', 'N/A')}, P={soil.get('phosphorus', 'N/A')}, K={soil.get('potassium', 'N/A')}")
69
+
70
+ # Forecast
71
+ if "forecast" in context:
72
+ fc = context["forecast"]
73
+ parts.append(f"• 20-day Forecast: {fc.get('trend', 'stable')}")
74
+
75
+ # Field Info
76
+ if "field_name" in context:
77
+ parts.append(f"• Field: {context['field_name']}")
78
+
79
+ if "crop_type" in context:
80
+ parts.append(f"• Crop: {context['crop_type']}")
81
+
82
+ if "area" in context:
83
+ parts.append(f"• Area: {context['area']} acres")
84
+
85
+ # Clusters/Zones
86
+ if "zones" in context:
87
+ zones = context["zones"]
88
+ parts.append(f"• Field Zones: {len(zones)} distinct areas identified")
89
+ for i, zone in enumerate(zones[:3]): # Max 3 zones
90
+ parts.append(f" - Zone {i+1}: {zone.get('condition', 'N/A')}")
91
+
92
+ # Weather
93
+ if "weather" in context:
94
+ w = context["weather"]
95
+ parts.append(f"• Current Weather: {w.get('condition', 'N/A')}, {w.get('temp', 'N/A')}°C")
96
+
97
+ if not parts:
98
+ return ""
99
+
100
+ return "\n".join(parts)
101
+
102
+
103
+ # Quick response templates for common queries
104
+ QUICK_RESPONSES = {
105
+ "stress": "Based on your field's stress analysis, ",
106
+ "water": "Looking at the water content indicators, ",
107
+ "fertilizer": "Considering your soil nutrient levels, ",
108
+ "harvest": "For optimal harvest timing, ",
109
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ pydantic==2.5.2
4
+ google-generativeai==0.3.2
5
+ supabase==2.3.0
6
+ python-dotenv==1.0.0
supabase_client.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Supabase Client for Chat Storage
3
+ =================================
4
+ Handles conversation persistence with:
5
+ - Session management
6
+ - Message CRUD
7
+ - History retrieval
8
+ """
9
+
10
+ import os
11
+ import uuid
12
+ from datetime import datetime
13
+ from typing import Optional, List, Dict, Any
14
+ import json
15
+
16
+ # Try to import supabase, fallback to in-memory storage
17
+ try:
18
+ from supabase import create_client, Client
19
+ SUPABASE_AVAILABLE = True
20
+ except ImportError:
21
+ SUPABASE_AVAILABLE = False
22
+ print("Warning: supabase-py not installed, using in-memory storage")
23
+
24
+
25
+ class SupabaseClient:
26
+ """Client for Supabase chat storage operations."""
27
+
28
+ def __init__(self):
29
+ self.client: Optional[Client] = None
30
+ self._memory_sessions: Dict[str, Dict] = {}
31
+ self._memory_messages: Dict[str, List[Dict]] = {}
32
+
33
+ if SUPABASE_AVAILABLE:
34
+ url = os.environ.get("SUPABASE_URL")
35
+ key = os.environ.get("SUPABASE_KEY")
36
+
37
+ if url and key:
38
+ try:
39
+ self.client = create_client(url, key)
40
+ print("Supabase client initialized successfully")
41
+ except Exception as e:
42
+ print(f"Failed to initialize Supabase: {e}")
43
+ self.client = None
44
+ else:
45
+ print("SUPABASE_URL or SUPABASE_KEY not set")
46
+
47
+ def is_configured(self) -> bool:
48
+ """Check if Supabase is properly configured."""
49
+ return self.client is not None
50
+
51
+ # =========================================================================
52
+ # SESSION OPERATIONS
53
+ # =========================================================================
54
+ def create_session(self, user_id: str, title: str = "New Conversation") -> Dict:
55
+ """Create a new chat session."""
56
+ session_id = str(uuid.uuid4())
57
+ now = datetime.now().isoformat()
58
+
59
+ session_data = {
60
+ "id": session_id,
61
+ "user_id": user_id,
62
+ "title": title,
63
+ "created_at": now,
64
+ "updated_at": now
65
+ }
66
+
67
+ if self.client:
68
+ try:
69
+ result = self.client.table("chat_sessions").insert(session_data).execute()
70
+ return result.data[0] if result.data else session_data
71
+ except Exception as e:
72
+ print(f"Supabase create session error: {e}")
73
+ # Fallback to memory
74
+
75
+ # In-memory fallback
76
+ self._memory_sessions[session_id] = session_data
77
+ self._memory_messages[session_id] = []
78
+ return session_data
79
+
80
+ def get_session(self, session_id: str) -> Optional[Dict]:
81
+ """Get session details."""
82
+ if self.client:
83
+ try:
84
+ result = self.client.table("chat_sessions").select("*").eq("id", session_id).execute()
85
+ return result.data[0] if result.data else None
86
+ except Exception as e:
87
+ print(f"Supabase get session error: {e}")
88
+
89
+ return self._memory_sessions.get(session_id)
90
+
91
+ def get_user_sessions(self, user_id: str) -> List[Dict]:
92
+ """Get all sessions for a user, ordered by most recent."""
93
+ if self.client:
94
+ try:
95
+ result = self.client.table("chat_sessions")\
96
+ .select("*, chat_messages(count)")\
97
+ .eq("user_id", user_id)\
98
+ .order("updated_at", desc=True)\
99
+ .execute()
100
+
101
+ sessions = []
102
+ for session in result.data:
103
+ msg_count = 0
104
+ if session.get("chat_messages"):
105
+ msg_count = session["chat_messages"][0].get("count", 0) if session["chat_messages"] else 0
106
+ sessions.append({
107
+ "id": session["id"],
108
+ "title": session["title"],
109
+ "created_at": session["created_at"],
110
+ "updated_at": session["updated_at"],
111
+ "message_count": msg_count
112
+ })
113
+ return sessions
114
+ except Exception as e:
115
+ print(f"Supabase get sessions error: {e}")
116
+
117
+ # In-memory fallback
118
+ return [
119
+ {**s, "message_count": len(self._memory_messages.get(s["id"], []))}
120
+ for s in self._memory_sessions.values()
121
+ if s.get("user_id") == user_id
122
+ ]
123
+
124
+ def update_session_timestamp(self, session_id: str):
125
+ """Update session's updated_at timestamp."""
126
+ now = datetime.now().isoformat()
127
+
128
+ if self.client:
129
+ try:
130
+ self.client.table("chat_sessions")\
131
+ .update({"updated_at": now})\
132
+ .eq("id", session_id)\
133
+ .execute()
134
+ except Exception as e:
135
+ print(f"Supabase update timestamp error: {e}")
136
+ else:
137
+ if session_id in self._memory_sessions:
138
+ self._memory_sessions[session_id]["updated_at"] = now
139
+
140
+ def update_session_title(self, session_id: str, title: str):
141
+ """Update session title."""
142
+ if self.client:
143
+ try:
144
+ self.client.table("chat_sessions")\
145
+ .update({"title": title, "updated_at": datetime.now().isoformat()})\
146
+ .eq("id", session_id)\
147
+ .execute()
148
+ except Exception as e:
149
+ print(f"Supabase update title error: {e}")
150
+ else:
151
+ if session_id in self._memory_sessions:
152
+ self._memory_sessions[session_id]["title"] = title
153
+
154
+ def delete_session(self, session_id: str):
155
+ """Delete session and all its messages."""
156
+ if self.client:
157
+ try:
158
+ # Delete messages first (foreign key constraint)
159
+ self.client.table("chat_messages").delete().eq("session_id", session_id).execute()
160
+ self.client.table("chat_sessions").delete().eq("id", session_id).execute()
161
+ except Exception as e:
162
+ print(f"Supabase delete session error: {e}")
163
+ else:
164
+ self._memory_sessions.pop(session_id, None)
165
+ self._memory_messages.pop(session_id, None)
166
+
167
+ # =========================================================================
168
+ # MESSAGE OPERATIONS
169
+ # =========================================================================
170
+ def add_message(
171
+ self,
172
+ session_id: str,
173
+ role: str,
174
+ content: str,
175
+ context_used: Optional[List[str]] = None
176
+ ) -> str:
177
+ """Add a message to a session."""
178
+ message_id = str(uuid.uuid4())
179
+ now = datetime.now().isoformat()
180
+
181
+ message_data = {
182
+ "id": message_id,
183
+ "session_id": session_id,
184
+ "role": role,
185
+ "content": content,
186
+ "context_used": json.dumps(context_used) if context_used else None,
187
+ "created_at": now
188
+ }
189
+
190
+ if self.client:
191
+ try:
192
+ self.client.table("chat_messages").insert(message_data).execute()
193
+ return message_id
194
+ except Exception as e:
195
+ print(f"Supabase add message error: {e}")
196
+
197
+ # In-memory fallback
198
+ if session_id not in self._memory_messages:
199
+ self._memory_messages[session_id] = []
200
+ self._memory_messages[session_id].append(message_data)
201
+ return message_id
202
+
203
+ def get_messages(self, session_id: str, limit: int = 100) -> List[Dict]:
204
+ """Get messages for a session, ordered by creation time."""
205
+ if self.client:
206
+ try:
207
+ result = self.client.table("chat_messages")\
208
+ .select("*")\
209
+ .eq("session_id", session_id)\
210
+ .order("created_at")\
211
+ .limit(limit)\
212
+ .execute()
213
+ return result.data if result.data else []
214
+ except Exception as e:
215
+ print(f"Supabase get messages error: {e}")
216
+
217
+ # In-memory fallback
218
+ messages = self._memory_messages.get(session_id, [])
219
+ return sorted(messages, key=lambda m: m.get("created_at", ""))[:limit]
220
+
221
+ def delete_message(self, message_id: str):
222
+ """Delete a specific message."""
223
+ if self.client:
224
+ try:
225
+ self.client.table("chat_messages").delete().eq("id", message_id).execute()
226
+ except Exception as e:
227
+ print(f"Supabase delete message error: {e}")
228
+ else:
229
+ for session_id, messages in self._memory_messages.items():
230
+ self._memory_messages[session_id] = [
231
+ m for m in messages if m.get("id") != message_id
232
+ ]
supabase_schema.sql ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- =============================================================================
2
+ -- AGROW Chatbot Tables
3
+ -- Run this in Supabase SQL Editor
4
+ -- =============================================================================
5
+
6
+ -- Chat Sessions Table
7
+ CREATE TABLE IF NOT EXISTS chat_sessions (
8
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
10
+ title TEXT NOT NULL DEFAULT 'New Conversation',
11
+ created_at TIMESTAMPTZ DEFAULT NOW(),
12
+ updated_at TIMESTAMPTZ DEFAULT NOW()
13
+ );
14
+
15
+ -- Chat Messages Table
16
+ CREATE TABLE IF NOT EXISTS chat_messages (
17
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
18
+ session_id UUID REFERENCES chat_sessions(id) ON DELETE CASCADE,
19
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
20
+ content TEXT NOT NULL,
21
+ context_used JSONB,
22
+ created_at TIMESTAMPTZ DEFAULT NOW()
23
+ );
24
+
25
+ -- Indexes for performance
26
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_id ON chat_sessions(user_id);
27
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_updated ON chat_sessions(updated_at DESC);
28
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
29
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created ON chat_messages(created_at);
30
+
31
+ -- RLS (Row Level Security) Policies
32
+ ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
33
+ ALTER TABLE chat_messages ENABLE ROW LEVEL SECURITY;
34
+
35
+ -- Allow users to see only their own sessions
36
+ CREATE POLICY "Users can view own sessions" ON chat_sessions
37
+ FOR SELECT USING (auth.uid() = user_id);
38
+
39
+ CREATE POLICY "Users can insert own sessions" ON chat_sessions
40
+ FOR INSERT WITH CHECK (auth.uid() = user_id);
41
+
42
+ CREATE POLICY "Users can update own sessions" ON chat_sessions
43
+ FOR UPDATE USING (auth.uid() = user_id);
44
+
45
+ CREATE POLICY "Users can delete own sessions" ON chat_sessions
46
+ FOR DELETE USING (auth.uid() = user_id);
47
+
48
+ -- Messages inherit permissions from sessions
49
+ CREATE POLICY "Users can view messages in own sessions" ON chat_messages
50
+ FOR SELECT USING (
51
+ EXISTS (
52
+ SELECT 1 FROM chat_sessions
53
+ WHERE chat_sessions.id = chat_messages.session_id
54
+ AND chat_sessions.user_id = auth.uid()
55
+ )
56
+ );
57
+
58
+ CREATE POLICY "Users can insert messages in own sessions" ON chat_messages
59
+ FOR INSERT WITH CHECK (
60
+ EXISTS (
61
+ SELECT 1 FROM chat_sessions
62
+ WHERE chat_sessions.id = chat_messages.session_id
63
+ AND chat_sessions.user_id = auth.uid()
64
+ )
65
+ );
66
+
67
+ CREATE POLICY "Users can delete messages in own sessions" ON chat_messages
68
+ FOR DELETE USING (
69
+ EXISTS (
70
+ SELECT 1 FROM chat_sessions
71
+ WHERE chat_sessions.id = chat_messages.session_id
72
+ AND chat_sessions.user_id = auth.uid()
73
+ )
74
+ );
75
+
76
+ -- Service role access (for HF Space backend)
77
+ CREATE POLICY "Service can access all sessions" ON chat_sessions
78
+ FOR ALL USING (auth.role() = 'service_role');
79
+
80
+ CREATE POLICY "Service can access all messages" ON chat_messages
81
+ FOR ALL USING (auth.role() = 'service_role');
82
+
83
+ -- Auto-update updated_at timestamp
84
+ CREATE OR REPLACE FUNCTION update_updated_at()
85
+ RETURNS TRIGGER AS $$
86
+ BEGIN
87
+ NEW.updated_at = NOW();
88
+ RETURN NEW;
89
+ END;
90
+ $$ LANGUAGE plpgsql;
91
+
92
+ DROP TRIGGER IF EXISTS update_chat_sessions_updated_at ON chat_sessions;
93
+ CREATE TRIGGER update_chat_sessions_updated_at
94
+ BEFORE UPDATE ON chat_sessions
95
+ FOR EACH ROW
96
+ EXECUTE FUNCTION update_updated_at();