Spaces:
Sleeping
Sleeping
Commit ·
b3afc51
0
Parent(s):
Initial commit: AGROW Chatbot with Gemini LLM
Browse files- Dockerfile +18 -0
- README.md +42 -0
- app.py +312 -0
- prompts.py +109 -0
- requirements.txt +6 -0
- supabase_client.py +232 -0
- 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();
|