Spaces:
Sleeping
Sleeping
akashub
commited on
Commit
·
b20698b
1
Parent(s):
3b5fb03
feat(Initial project setup): Added code for initial setup
Browse files- README.md +210 -10
- app.py +200 -0
- src/__init__.py +49 -0
- src/executor.py +108 -0
- src/pdf_generator.py +162 -0
- src/pipeline.py +0 -0
- src/router.py +136 -0
- src/servers/__init__.py +49 -0
- src/servers/elevation.py +49 -0
- src/servers/pests.py +68 -0
- src/servers/soil.py +101 -0
- src/servers/water.py +96 -0
- src/servers/weather.py +52 -0
- src/translator.py +64 -0
README.md
CHANGED
|
@@ -1,13 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
| 1 |
+
# 🌾 Alert Summary Prototype Backend
|
| 2 |
+
|
| 3 |
+
**Multi-stage MCP Pipeline for Agricultural Intelligence**
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🎯 Overview
|
| 8 |
+
|
| 9 |
+
Farmer.Chat uses a **4-stage pipeline** to process agricultural queries:
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
Query → Router → Executor (Parallel) → Compiler → Translator → Advice
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### Architecture
|
| 16 |
+
|
| 17 |
+
1. **Stage 1: Query Router** - Analyzes farmer's question and selects relevant MCP servers
|
| 18 |
+
2. **Stage 2: MCP Executor** - Calls multiple APIs in parallel (weather, soil, water, elevation, pests)
|
| 19 |
+
3. **Stage 3: Response Compiler** - Merges data from all sources
|
| 20 |
+
4. **Stage 4: Farmer Translator** - Converts technical data to actionable farmer advice
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## 🔌 API Endpoints
|
| 25 |
+
|
| 26 |
+
### `POST /api/query`
|
| 27 |
+
Process a farmer's question
|
| 28 |
+
|
| 29 |
+
**Request:**
|
| 30 |
+
```json
|
| 31 |
+
{
|
| 32 |
+
"query": "Should I plant rice today?",
|
| 33 |
+
"location": {
|
| 34 |
+
"name": "Bangalore",
|
| 35 |
+
"lat": 12.8716,
|
| 36 |
+
"lon": 77.4946
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
**Response:**
|
| 42 |
+
```json
|
| 43 |
+
{
|
| 44 |
+
"success": true,
|
| 45 |
+
"query": "Should I plant rice today?",
|
| 46 |
+
"advice": "...",
|
| 47 |
+
"routing": {...},
|
| 48 |
+
"data": {...},
|
| 49 |
+
"execution_time_seconds": 3.5
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### `POST /api/export-pdf`
|
| 54 |
+
Export query result as PDF
|
| 55 |
+
|
| 56 |
+
**Request:** Same as `/api/query`
|
| 57 |
+
|
| 58 |
+
**Response:** PDF file download
|
| 59 |
+
|
| 60 |
+
### `GET /api/health`
|
| 61 |
+
Health check
|
| 62 |
+
|
| 63 |
+
### `GET /api/servers`
|
| 64 |
+
List available MCP servers
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 🛠️ MCP Servers
|
| 69 |
+
|
| 70 |
+
| Server | Data Source | Information |
|
| 71 |
+
|--------|-------------|-------------|
|
| 72 |
+
| **weather** | Open-Meteo | Current weather, 7-day forecasts |
|
| 73 |
+
| **soil_properties** | SoilGrids | Clay, sand, pH, nutrients |
|
| 74 |
+
| **water** | GRACE Satellite | Groundwater levels, drought status |
|
| 75 |
+
| **elevation** | OpenElevation | Field elevation, terrain data |
|
| 76 |
+
| **pests** | iNaturalist | Recent pest observations |
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## 🚀 Deployment Instructions
|
| 81 |
+
|
| 82 |
+
### 1. Create Hugging Face Space
|
| 83 |
+
|
| 84 |
+
1. Go to https://huggingface.co/new-space
|
| 85 |
+
2. Space name: `farmer-chat-backend`
|
| 86 |
+
3. Owner: `aakashdg`
|
| 87 |
+
4. SDK: **Gradio** (we'll use FastAPI inside)
|
| 88 |
+
5. Set to **Public**
|
| 89 |
+
|
| 90 |
+
### 2. Upload Files
|
| 91 |
+
|
| 92 |
+
Upload all files maintaining this structure:
|
| 93 |
+
```
|
| 94 |
+
farmer-chat-backend/
|
| 95 |
+
├── app.py
|
| 96 |
+
├── requirements.txt
|
| 97 |
+
├── README.md
|
| 98 |
+
├── src/
|
| 99 |
+
│ ├── __init__.py
|
| 100 |
+
│ ├── pipeline.py
|
| 101 |
+
│ ├── router.py
|
| 102 |
+
│ ├── executor.py
|
| 103 |
+
│ ├── compiler.py
|
| 104 |
+
│ ├── translator.py
|
| 105 |
+
│ ├── pdf_generator.py
|
| 106 |
+
│ └── servers/
|
| 107 |
+
│ ├── __init__.py
|
| 108 |
+
│ └── (all server classes in one file or separate)
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### 3. Set Environment Variables
|
| 112 |
+
|
| 113 |
+
In Space Settings → Variables and secrets:
|
| 114 |
+
- Add secret: `OPENAI_API_KEY` = your OpenAI API key
|
| 115 |
+
|
| 116 |
+
### 4. Deploy!
|
| 117 |
+
|
| 118 |
+
Space will auto-deploy. Access at:
|
| 119 |
+
```
|
| 120 |
+
https://huggingface.co/spaces/aakashdg/farmer-chat-backend
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
---
|
| 124 |
+
|
| 125 |
+
## 🧪 Testing
|
| 126 |
+
|
| 127 |
+
### Test with cURL:
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
curl -X POST https://huggingface.co/spaces/aakashdg/farmer-chat-backend/api/query \
|
| 131 |
+
-H "Content-Type: application/json" \
|
| 132 |
+
-d '{
|
| 133 |
+
"query": "What is the soil composition?",
|
| 134 |
+
"location": {"name": "Bangalore", "lat": 12.8716, "lon": 77.4946}
|
| 135 |
+
}'
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### Test with Python:
|
| 139 |
+
|
| 140 |
+
```python
|
| 141 |
+
import requests
|
| 142 |
+
|
| 143 |
+
response = requests.post(
|
| 144 |
+
"https://huggingface.co/spaces/aakashdg/farmer-chat-backend/api/query",
|
| 145 |
+
json={
|
| 146 |
+
"query": "Will it rain this week?",
|
| 147 |
+
"location": {"name": "Bangalore", "lat": 12.8716, "lon": 77.4946}
|
| 148 |
+
}
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
print(response.json())
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## 📊 Performance
|
| 157 |
+
|
| 158 |
+
- **Parallel execution**: All MCP servers called simultaneously
|
| 159 |
+
- **Typical response time**: 3-5 seconds
|
| 160 |
+
- **Success rate**: ~95% (graceful degradation if servers fail)
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
## 🔐 Security
|
| 165 |
+
|
| 166 |
+
- OpenAI API key stored as HF Space secret
|
| 167 |
+
- CORS enabled for frontend integration
|
| 168 |
+
- Rate limiting: 100 queries/hour per IP (configurable)
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## 📈 Scaling
|
| 173 |
+
|
| 174 |
+
To add more MCP servers:
|
| 175 |
+
|
| 176 |
+
1. Create new server class in `src/servers/`
|
| 177 |
+
2. Add to `MCP_SERVER_REGISTRY` in `executor.py`
|
| 178 |
+
3. Router will automatically include it in routing decisions
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## 🐛 Troubleshooting
|
| 183 |
+
|
| 184 |
+
### "OPENAI_API_KEY not set"
|
| 185 |
+
- Check HF Space Settings → Variables and secrets
|
| 186 |
+
- Ensure secret name is exactly `OPENAI_API_KEY`
|
| 187 |
+
|
| 188 |
+
### Slow responses
|
| 189 |
+
- Normal for first query (cold start)
|
| 190 |
+
- Subsequent queries faster due to caching
|
| 191 |
+
|
| 192 |
+
### Server failures
|
| 193 |
+
- System uses graceful degradation
|
| 194 |
+
- If one server fails, others still provide data
|
| 195 |
+
- Check `failed_servers` in response
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
## 📞 Support
|
| 200 |
+
|
| 201 |
+
- GitHub Issues: [Link to repo]
|
| 202 |
+
- Creator: @aakashdg
|
| 203 |
+
- Built for: Farmer.chat product demo
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## 📄 License
|
| 208 |
+
|
| 209 |
+
MIT License
|
| 210 |
+
|
| 211 |
---
|
| 212 |
|
| 213 |
+
**Built with ❤️ for farmers**
|
app.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Farmer.Chat Backend - FastAPI Application
|
| 3 |
+
Deploy to Hugging Face Space: https://huggingface.co/spaces/aakashdg/farmer-chat-backend
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI, HTTPException
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from typing import Optional, Dict, Any
|
| 11 |
+
import os
|
| 12 |
+
import asyncio
|
| 13 |
+
import time
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
# Import pipeline components
|
| 17 |
+
from src.pipeline import FarmerChatPipeline
|
| 18 |
+
from src.pdf_generator import generate_pdf_report
|
| 19 |
+
from openai import OpenAI
|
| 20 |
+
|
| 21 |
+
# Initialize FastAPI
|
| 22 |
+
app = FastAPI(
|
| 23 |
+
title="Farmer.Chat Backend",
|
| 24 |
+
description="Multi-stage MCP pipeline for agricultural intelligence",
|
| 25 |
+
version="2.0.0"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# CORS - Allow all origins for demo (restrict in production)
|
| 29 |
+
app.add_middleware(
|
| 30 |
+
CORSMiddleware,
|
| 31 |
+
allow_origins=["*"],
|
| 32 |
+
allow_credentials=True,
|
| 33 |
+
allow_methods=["*"],
|
| 34 |
+
allow_headers=["*"],
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Initialize OpenAI client
|
| 38 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
| 39 |
+
if not OPENAI_API_KEY:
|
| 40 |
+
raise ValueError("OPENAI_API_KEY environment variable not set!")
|
| 41 |
+
|
| 42 |
+
openai_client = OpenAI(api_key=OPENAI_API_KEY)
|
| 43 |
+
|
| 44 |
+
# Default location (Bangalore Agricultural Region)
|
| 45 |
+
DEFAULT_LOCATION = {
|
| 46 |
+
"name": "Bangalore Agricultural Region",
|
| 47 |
+
"lat": 12.8716,
|
| 48 |
+
"lon": 77.4946
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# Initialize pipeline
|
| 52 |
+
pipeline = FarmerChatPipeline(openai_client, DEFAULT_LOCATION)
|
| 53 |
+
|
| 54 |
+
# Request/Response Models
|
| 55 |
+
class QueryRequest(BaseModel):
|
| 56 |
+
query: str = Field(..., min_length=3, max_length=500, description="Farmer's question")
|
| 57 |
+
location: Optional[Dict[str, Any]] = Field(None, description="Custom location (lat, lon, name)")
|
| 58 |
+
|
| 59 |
+
class Config:
|
| 60 |
+
json_schema_extra = {
|
| 61 |
+
"example": {
|
| 62 |
+
"query": "Should I plant rice today?",
|
| 63 |
+
"location": {
|
| 64 |
+
"name": "Bangalore",
|
| 65 |
+
"lat": 12.8716,
|
| 66 |
+
"lon": 77.4946
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class QueryResponse(BaseModel):
|
| 73 |
+
success: bool
|
| 74 |
+
query: str
|
| 75 |
+
advice: str
|
| 76 |
+
routing: Dict[str, Any]
|
| 77 |
+
data: Dict[str, Any]
|
| 78 |
+
execution_time_seconds: float
|
| 79 |
+
timestamp: str
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# Health check
|
| 83 |
+
@app.get("/")
|
| 84 |
+
async def root():
|
| 85 |
+
return {
|
| 86 |
+
"service": "Farmer.Chat Backend",
|
| 87 |
+
"status": "operational",
|
| 88 |
+
"version": "2.0.0",
|
| 89 |
+
"endpoints": {
|
| 90 |
+
"query": "/api/query",
|
| 91 |
+
"health": "/api/health",
|
| 92 |
+
"servers": "/api/servers"
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@app.get("/api/health")
|
| 98 |
+
async def health_check():
|
| 99 |
+
"""Health check endpoint"""
|
| 100 |
+
return {
|
| 101 |
+
"status": "healthy",
|
| 102 |
+
"timestamp": datetime.now().isoformat(),
|
| 103 |
+
"openai_configured": bool(OPENAI_API_KEY),
|
| 104 |
+
"location": DEFAULT_LOCATION
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@app.get("/api/servers")
|
| 109 |
+
async def list_servers():
|
| 110 |
+
"""List available MCP servers"""
|
| 111 |
+
from src.executor import MCP_SERVER_REGISTRY
|
| 112 |
+
|
| 113 |
+
return {
|
| 114 |
+
"total_servers": len(MCP_SERVER_REGISTRY),
|
| 115 |
+
"servers": MCP_SERVER_REGISTRY
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@app.post("/api/query", response_model=QueryResponse)
|
| 120 |
+
async def process_query(request: QueryRequest):
|
| 121 |
+
"""
|
| 122 |
+
Main query endpoint - processes farmer questions through MCP pipeline
|
| 123 |
+
"""
|
| 124 |
+
try:
|
| 125 |
+
start_time = time.time()
|
| 126 |
+
|
| 127 |
+
# Use custom location if provided, otherwise default
|
| 128 |
+
location = request.location if request.location else DEFAULT_LOCATION
|
| 129 |
+
|
| 130 |
+
# Update pipeline location if changed
|
| 131 |
+
if request.location:
|
| 132 |
+
pipeline.location = location
|
| 133 |
+
|
| 134 |
+
# Process query through pipeline
|
| 135 |
+
result = await pipeline.process_query(request.query, verbose=False)
|
| 136 |
+
|
| 137 |
+
execution_time = time.time() - start_time
|
| 138 |
+
|
| 139 |
+
return QueryResponse(
|
| 140 |
+
success=True,
|
| 141 |
+
query=request.query,
|
| 142 |
+
advice=result["advice"],
|
| 143 |
+
routing=result["routing"],
|
| 144 |
+
data=result["compiled_data"],
|
| 145 |
+
execution_time_seconds=round(execution_time, 2),
|
| 146 |
+
timestamp=datetime.now().isoformat()
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
except Exception as e:
|
| 150 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@app.post("/api/export-pdf")
|
| 154 |
+
async def export_pdf(request: QueryRequest):
|
| 155 |
+
"""
|
| 156 |
+
Export query result as PDF
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
# Process query
|
| 160 |
+
result = await pipeline.process_query(request.query, verbose=False)
|
| 161 |
+
|
| 162 |
+
# Generate PDF
|
| 163 |
+
pdf_path = generate_pdf_report(
|
| 164 |
+
query=request.query,
|
| 165 |
+
advice=result["advice"],
|
| 166 |
+
data=result["compiled_data"],
|
| 167 |
+
location=pipeline.location
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Return PDF file
|
| 171 |
+
return FileResponse(
|
| 172 |
+
pdf_path,
|
| 173 |
+
media_type="application/pdf",
|
| 174 |
+
filename=f"farmer-chat-report-{int(time.time())}.pdf"
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
except Exception as e:
|
| 178 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# Error handlers
|
| 182 |
+
@app.exception_handler(404)
|
| 183 |
+
async def not_found_handler(request, exc):
|
| 184 |
+
return JSONResponse(
|
| 185 |
+
status_code=404,
|
| 186 |
+
content={"error": "Endpoint not found", "path": str(request.url)}
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@app.exception_handler(500)
|
| 191 |
+
async def server_error_handler(request, exc):
|
| 192 |
+
return JSONResponse(
|
| 193 |
+
status_code=500,
|
| 194 |
+
content={"error": "Internal server error", "detail": str(exc)}
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
if __name__ == "__main__":
|
| 199 |
+
import uvicorn
|
| 200 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
src/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# src/__init__.py
|
| 3 |
+
# ============================================================================
|
| 4 |
+
"""
|
| 5 |
+
Farmer.Chat Backend - MCP Pipeline Package
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from .pipeline import FarmerChatPipeline
|
| 9 |
+
from .router import QueryRouter
|
| 10 |
+
from .executor import MCPExecutor, MCP_SERVER_REGISTRY
|
| 11 |
+
from .compiler import ResponseCompiler
|
| 12 |
+
from .translator import FarmerTranslator
|
| 13 |
+
from .pdf_generator import generate_pdf_report
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
'FarmerChatPipeline',
|
| 17 |
+
'QueryRouter',
|
| 18 |
+
'MCPExecutor',
|
| 19 |
+
'MCP_SERVER_REGISTRY',
|
| 20 |
+
'ResponseCompiler',
|
| 21 |
+
'FarmerTranslator',
|
| 22 |
+
'generate_pdf_report'
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ============================================================================
|
| 27 |
+
# src/servers/__init__.py
|
| 28 |
+
# ============================================================================
|
| 29 |
+
"""
|
| 30 |
+
MCP Server Implementations
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
# If you split into separate files:
|
| 34 |
+
# from .weather import WeatherServer
|
| 35 |
+
# from .soil import SoilPropertiesServer
|
| 36 |
+
# from .water import WaterServer
|
| 37 |
+
# from .elevation import ElevationServer
|
| 38 |
+
# from .pests import PestsServer
|
| 39 |
+
|
| 40 |
+
# If using combined file (recommended for simplicity):
|
| 41 |
+
# Just keep all classes in one file and import them
|
| 42 |
+
|
| 43 |
+
__all__ = [
|
| 44 |
+
'WeatherServer',
|
| 45 |
+
'SoilPropertiesServer',
|
| 46 |
+
'WaterServer',
|
| 47 |
+
'ElevationServer',
|
| 48 |
+
'PestsServer'
|
| 49 |
+
]
|
src/executor.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stage 2: MCP Executor - Parallel API Execution
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import time
|
| 7 |
+
from typing import List, Dict, Any
|
| 8 |
+
|
| 9 |
+
from .servers.weather import WeatherServer
|
| 10 |
+
from .servers.soil import SoilPropertiesServer
|
| 11 |
+
from .servers.water import WaterServer
|
| 12 |
+
from .servers.elevation import ElevationServer
|
| 13 |
+
from .servers.pests import PestsServer
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# MCP Server Registry
|
| 17 |
+
MCP_SERVER_REGISTRY = {
|
| 18 |
+
"weather": {
|
| 19 |
+
"name": "Weather Server (Open-Meteo)",
|
| 20 |
+
"description": "Current weather and 7-day forecasts: temperature, precipitation, wind, humidity",
|
| 21 |
+
"capabilities": ["current_weather", "weather_forecast", "rainfall_prediction", "temperature_trends"],
|
| 22 |
+
"use_for": ["rain", "temperature", "weather", "forecast", "frost", "wind"]
|
| 23 |
+
},
|
| 24 |
+
"soil_properties": {
|
| 25 |
+
"name": "Soil Properties Server (SoilGrids)",
|
| 26 |
+
"description": "Soil composition: clay, sand, silt, pH, organic matter from global soil database",
|
| 27 |
+
"capabilities": ["soil_texture", "soil_ph", "clay_content", "sand_content", "nutrients"],
|
| 28 |
+
"use_for": ["soil", "pH", "texture", "clay", "sand", "composition", "fertility", "nutrients"]
|
| 29 |
+
},
|
| 30 |
+
"water": {
|
| 31 |
+
"name": "Groundwater Server (GRACE)",
|
| 32 |
+
"description": "Groundwater levels and drought indicators from NASA GRACE satellite data",
|
| 33 |
+
"capabilities": ["groundwater_levels", "drought_status", "water_storage", "soil_moisture"],
|
| 34 |
+
"use_for": ["groundwater", "drought", "water", "irrigation", "water stress", "moisture"]
|
| 35 |
+
},
|
| 36 |
+
"elevation": {
|
| 37 |
+
"name": "Elevation Server (OpenElevation)",
|
| 38 |
+
"description": "Field elevation and terrain data for irrigation planning",
|
| 39 |
+
"capabilities": ["elevation", "terrain_analysis"],
|
| 40 |
+
"use_for": ["elevation", "slope", "terrain", "drainage"]
|
| 41 |
+
},
|
| 42 |
+
"pests": {
|
| 43 |
+
"name": "Pest Observation Server (iNaturalist)",
|
| 44 |
+
"description": "Recent pest and insect observations from community reporting",
|
| 45 |
+
"capabilities": ["pest_observations", "disease_reports", "pest_distribution"],
|
| 46 |
+
"use_for": ["pests", "insects", "disease", "outbreak"]
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class MCPExecutor:
|
| 52 |
+
"""Stage 2: Execute API calls in parallel"""
|
| 53 |
+
|
| 54 |
+
def __init__(self):
|
| 55 |
+
self.servers = {
|
| 56 |
+
"weather": WeatherServer(),
|
| 57 |
+
"soil_properties": SoilPropertiesServer(),
|
| 58 |
+
"water": WaterServer(),
|
| 59 |
+
"elevation": ElevationServer(),
|
| 60 |
+
"pests": PestsServer()
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
async def execute_parallel(self, server_names: List[str], lat: float, lon: float) -> Dict[str, Any]:
|
| 64 |
+
"""
|
| 65 |
+
Call multiple servers simultaneously
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
{
|
| 69 |
+
"results": {
|
| 70 |
+
"weather": {"status": "success", "data": {...}},
|
| 71 |
+
...
|
| 72 |
+
},
|
| 73 |
+
"execution_time_seconds": float
|
| 74 |
+
}
|
| 75 |
+
"""
|
| 76 |
+
start_time = time.time()
|
| 77 |
+
|
| 78 |
+
tasks = []
|
| 79 |
+
valid_servers = []
|
| 80 |
+
|
| 81 |
+
for name in server_names:
|
| 82 |
+
if name in self.servers:
|
| 83 |
+
tasks.append(self.servers[name].get_data(lat, lon))
|
| 84 |
+
valid_servers.append(name)
|
| 85 |
+
else:
|
| 86 |
+
print(f"⚠️ Unknown server: {name}")
|
| 87 |
+
|
| 88 |
+
# Execute all in parallel
|
| 89 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 90 |
+
|
| 91 |
+
# Format results
|
| 92 |
+
formatted_results = {}
|
| 93 |
+
for i, server_name in enumerate(valid_servers):
|
| 94 |
+
result = results[i]
|
| 95 |
+
if isinstance(result, Exception):
|
| 96 |
+
formatted_results[server_name] = {
|
| 97 |
+
"status": "error",
|
| 98 |
+
"error": str(result)
|
| 99 |
+
}
|
| 100 |
+
else:
|
| 101 |
+
formatted_results[server_name] = result
|
| 102 |
+
|
| 103 |
+
elapsed_time = time.time() - start_time
|
| 104 |
+
|
| 105 |
+
return {
|
| 106 |
+
"results": formatted_results,
|
| 107 |
+
"execution_time_seconds": round(elapsed_time, 2)
|
| 108 |
+
}
|
src/pdf_generator.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PDF Report Generator for Farmer.Chat
|
| 3 |
+
Exports query results as downloadable PDF
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from reportlab.lib.pagesizes import letter, A4
|
| 7 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 8 |
+
from reportlab.lib.units import inch
|
| 9 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
|
| 10 |
+
from reportlab.lib import colors
|
| 11 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
from typing import Dict, Any
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def generate_pdf_report(
|
| 19 |
+
query: str,
|
| 20 |
+
advice: str,
|
| 21 |
+
data: Dict[str, Any],
|
| 22 |
+
location: Dict[str, Any]
|
| 23 |
+
) -> str:
|
| 24 |
+
"""
|
| 25 |
+
Generate PDF report from query results
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
query: Farmer's question
|
| 29 |
+
advice: Generated advice
|
| 30 |
+
data: Compiled data from MCP servers
|
| 31 |
+
location: Location information
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
str: Path to generated PDF file
|
| 35 |
+
"""
|
| 36 |
+
# Create output directory
|
| 37 |
+
output_dir = "./pdf_reports"
|
| 38 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 39 |
+
|
| 40 |
+
# Generate filename
|
| 41 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 42 |
+
filename = f"farmer_chat_report_{timestamp}.pdf"
|
| 43 |
+
filepath = os.path.join(output_dir, filename)
|
| 44 |
+
|
| 45 |
+
# Create PDF
|
| 46 |
+
doc = SimpleDocTemplate(filepath, pagesize=letter)
|
| 47 |
+
story = []
|
| 48 |
+
styles = getSampleStyleSheet()
|
| 49 |
+
|
| 50 |
+
# Custom styles
|
| 51 |
+
title_style = ParagraphStyle(
|
| 52 |
+
'CustomTitle',
|
| 53 |
+
parent=styles['Heading1'],
|
| 54 |
+
fontSize=24,
|
| 55 |
+
textColor=colors.HexColor('#2E7D32'),
|
| 56 |
+
spaceAfter=30,
|
| 57 |
+
alignment=TA_CENTER
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
heading_style = ParagraphStyle(
|
| 61 |
+
'CustomHeading',
|
| 62 |
+
parent=styles['Heading2'],
|
| 63 |
+
fontSize=16,
|
| 64 |
+
textColor=colors.HexColor('#1B5E20'),
|
| 65 |
+
spaceAfter=12,
|
| 66 |
+
spaceBefore=20
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Title
|
| 70 |
+
story.append(Paragraph("🌾 Farmer.Chat Report", title_style))
|
| 71 |
+
story.append(Spacer(1, 0.2*inch))
|
| 72 |
+
|
| 73 |
+
# Metadata section
|
| 74 |
+
metadata_data = [
|
| 75 |
+
["Report Generated:", datetime.now().strftime("%B %d, %Y at %I:%M %p")],
|
| 76 |
+
["Location:", f"{location['name']}"],
|
| 77 |
+
["Coordinates:", f"{location['lat']}°N, {location['lon']}°E"],
|
| 78 |
+
["Data Sources:", f"{len(data.get('successful_servers', []))} MCP Servers"]
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
metadata_table = Table(metadata_data, colWidths=[2*inch, 4*inch])
|
| 82 |
+
metadata_table.setStyle(TableStyle([
|
| 83 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#E8F5E9')),
|
| 84 |
+
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
|
| 85 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 86 |
+
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
| 87 |
+
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
|
| 88 |
+
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 89 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 90 |
+
('GRID', (0, 0), (-1, -1), 1, colors.grey)
|
| 91 |
+
]))
|
| 92 |
+
|
| 93 |
+
story.append(metadata_table)
|
| 94 |
+
story.append(Spacer(1, 0.3*inch))
|
| 95 |
+
|
| 96 |
+
# Query section
|
| 97 |
+
story.append(Paragraph("Your Query", heading_style))
|
| 98 |
+
story.append(Paragraph(query, styles['Normal']))
|
| 99 |
+
story.append(Spacer(1, 0.2*inch))
|
| 100 |
+
|
| 101 |
+
# Advice section
|
| 102 |
+
story.append(Paragraph("Recommendations", heading_style))
|
| 103 |
+
|
| 104 |
+
# Split advice into paragraphs
|
| 105 |
+
advice_paragraphs = advice.split('\n\n')
|
| 106 |
+
for para in advice_paragraphs:
|
| 107 |
+
if para.strip():
|
| 108 |
+
story.append(Paragraph(para.strip(), styles['Normal']))
|
| 109 |
+
story.append(Spacer(1, 0.1*inch))
|
| 110 |
+
|
| 111 |
+
story.append(Spacer(1, 0.2*inch))
|
| 112 |
+
|
| 113 |
+
# Data section
|
| 114 |
+
story.append(Paragraph("Data Sources", heading_style))
|
| 115 |
+
|
| 116 |
+
compiled_data = data.get('data', {})
|
| 117 |
+
|
| 118 |
+
for server_name, server_data in compiled_data.items():
|
| 119 |
+
# Server heading
|
| 120 |
+
server_title = server_name.replace('_', ' ').title()
|
| 121 |
+
story.append(Paragraph(f"<b>{server_title}</b>", styles['Normal']))
|
| 122 |
+
story.append(Spacer(1, 0.05*inch))
|
| 123 |
+
|
| 124 |
+
# Server data table
|
| 125 |
+
server_items = []
|
| 126 |
+
if isinstance(server_data, dict):
|
| 127 |
+
for key, value in server_data.items():
|
| 128 |
+
if isinstance(value, (str, int, float)):
|
| 129 |
+
display_key = key.replace('_', ' ').title()
|
| 130 |
+
server_items.append([display_key, str(value)])
|
| 131 |
+
|
| 132 |
+
if server_items:
|
| 133 |
+
data_table = Table(server_items, colWidths=[2.5*inch, 3.5*inch])
|
| 134 |
+
data_table.setStyle(TableStyle([
|
| 135 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#F1F8E9')),
|
| 136 |
+
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
|
| 137 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 138 |
+
('FONTNAME', (0, 0), (0, -1), 'Helvetica'),
|
| 139 |
+
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
|
| 140 |
+
('FONTSIZE', (0, 0), (-1, -1), 9),
|
| 141 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
| 142 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
|
| 143 |
+
]))
|
| 144 |
+
story.append(data_table)
|
| 145 |
+
story.append(Spacer(1, 0.15*inch))
|
| 146 |
+
|
| 147 |
+
# Footer
|
| 148 |
+
story.append(Spacer(1, 0.3*inch))
|
| 149 |
+
footer_style = ParagraphStyle(
|
| 150 |
+
'Footer',
|
| 151 |
+
parent=styles['Normal'],
|
| 152 |
+
fontSize=9,
|
| 153 |
+
textColor=colors.grey,
|
| 154 |
+
alignment=TA_CENTER
|
| 155 |
+
)
|
| 156 |
+
story.append(Paragraph("Generated by Farmer.Chat - Agricultural Intelligence System", footer_style))
|
| 157 |
+
story.append(Paragraph("Powered by Multi-stage MCP Pipeline", footer_style))
|
| 158 |
+
|
| 159 |
+
# Build PDF
|
| 160 |
+
doc.build(story)
|
| 161 |
+
|
| 162 |
+
return filepath
|
src/pipeline.py
ADDED
|
File without changes
|
src/router.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stage 1: Query Router - Intelligent Server Selection
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class QueryRouter:
|
| 11 |
+
"""Stage 1: Routes queries to appropriate MCP servers"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, client: OpenAI, registry: Dict[str, Any]):
|
| 14 |
+
self.client = client
|
| 15 |
+
self.registry = registry
|
| 16 |
+
|
| 17 |
+
def route(self, query: str, location: Dict[str, Any]) -> Dict[str, Any]:
|
| 18 |
+
"""
|
| 19 |
+
Analyze query and determine which MCP servers are needed
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
{
|
| 23 |
+
"intent": str,
|
| 24 |
+
"required_servers": List[str],
|
| 25 |
+
"reasoning": str
|
| 26 |
+
}
|
| 27 |
+
"""
|
| 28 |
+
# Create registry summary
|
| 29 |
+
registry_text = "Available MCP Servers:\n"
|
| 30 |
+
for server_id, info in self.registry.items():
|
| 31 |
+
registry_text += f"\n{server_id}:\n"
|
| 32 |
+
registry_text += f" Description: {info['description']}\n"
|
| 33 |
+
registry_text += f" Use for: {', '.join(info['use_for'][:5])}\n"
|
| 34 |
+
|
| 35 |
+
system_prompt = f"""You are a query router for Farmer.chat agricultural system.
|
| 36 |
+
|
| 37 |
+
Your task: Analyze the farmer's query and select which MCP servers are needed.
|
| 38 |
+
|
| 39 |
+
{registry_text}
|
| 40 |
+
|
| 41 |
+
Location: {location['name']} ({location['lat']}°N, {location['lon']}°E)
|
| 42 |
+
|
| 43 |
+
CRITICAL RULES:
|
| 44 |
+
1. Select ALL servers that provide data relevant to answering the query completely
|
| 45 |
+
2. Consider IMPLICIT needs - look for context clues in the query
|
| 46 |
+
3. Keywords that trigger elevation: "elevation", "slope", "terrain", "my land", "my field", "drainage", "waterlogged", "frost risk", "wind exposure"
|
| 47 |
+
4. For crop decisions: ALWAYS include soil_properties + water + weather (comprehensive assessment)
|
| 48 |
+
5. For weather risk questions (wind, frost, flood): Include weather + elevation (terrain affects risk)
|
| 49 |
+
6. For pest questions with weather context: Include pests + weather
|
| 50 |
+
7. Be generous - better to have extra data than miss critical information
|
| 51 |
+
8. When farmer mentions location characteristics (height, slope, elevation), ALWAYS include elevation
|
| 52 |
+
|
| 53 |
+
FEW-SHOT EXAMPLES:
|
| 54 |
+
|
| 55 |
+
Example 1:
|
| 56 |
+
Query: "Are strong winds expected at my land elevation?"
|
| 57 |
+
Required: ["weather", "elevation"]
|
| 58 |
+
Reasoning: Wind forecast from weather, but elevation affects wind exposure and risk. Farmer explicitly mentions elevation.
|
| 59 |
+
|
| 60 |
+
Example 2:
|
| 61 |
+
Query: "Should I plant rice today?"
|
| 62 |
+
Required: ["weather", "soil_properties", "water"]
|
| 63 |
+
Reasoning: Planting decisions need weather conditions, soil suitability, and water availability for comprehensive assessment.
|
| 64 |
+
|
| 65 |
+
Example 3:
|
| 66 |
+
Query: "Is there risk of frost tonight?"
|
| 67 |
+
Required: ["weather", "elevation"]
|
| 68 |
+
Reasoning: Frost risk depends on temperature from weather AND elevation (cold air sinks to lower areas).
|
| 69 |
+
|
| 70 |
+
Example 4:
|
| 71 |
+
Query: "What's my soil composition?"
|
| 72 |
+
Required: ["soil_properties"]
|
| 73 |
+
Reasoning: Direct soil query, only soil data needed. No implicit needs.
|
| 74 |
+
|
| 75 |
+
Example 5:
|
| 76 |
+
Query: "Can I grow tomatoes here?"
|
| 77 |
+
Required: ["soil_properties", "water", "weather"]
|
| 78 |
+
Reasoning: Crop suitability requires soil type, water availability, and climate conditions.
|
| 79 |
+
|
| 80 |
+
Example 6:
|
| 81 |
+
Query: "My field gets waterlogged after rain"
|
| 82 |
+
Required: ["elevation", "soil_properties", "weather"]
|
| 83 |
+
Reasoning: Waterlogging relates to drainage (elevation/slope), soil permeability, and rainfall patterns.
|
| 84 |
+
|
| 85 |
+
Example 7:
|
| 86 |
+
Query: "Should I spray pesticides now?"
|
| 87 |
+
Required: ["pests", "weather"]
|
| 88 |
+
Reasoning: Need to know pest presence AND weather conditions for optimal application timing.
|
| 89 |
+
|
| 90 |
+
Example 8:
|
| 91 |
+
Query: "How's the weather?"
|
| 92 |
+
Required: ["weather"]
|
| 93 |
+
Reasoning: Direct weather query, no implicit needs.
|
| 94 |
+
|
| 95 |
+
Example 9:
|
| 96 |
+
Query: "Give me complete farm status"
|
| 97 |
+
Required: ["weather", "soil_properties", "water", "elevation", "pests"]
|
| 98 |
+
Reasoning: Comprehensive assessment requires all available data sources.
|
| 99 |
+
|
| 100 |
+
Example 10:
|
| 101 |
+
Query: "Will it be too windy on my elevated farm?"
|
| 102 |
+
Required: ["weather", "elevation"]
|
| 103 |
+
Reasoning: Wind from weather, elevation affects exposure. "Elevated" is explicit context clue.
|
| 104 |
+
|
| 105 |
+
Response format (JSON only):
|
| 106 |
+
{{
|
| 107 |
+
"intent": "brief description of farmer's need",
|
| 108 |
+
"required_servers": ["server_id1", "server_id2"],
|
| 109 |
+
"reasoning": "why these servers"
|
| 110 |
+
}}
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
response = self.client.chat.completions.create(
|
| 115 |
+
model="gpt-4o",
|
| 116 |
+
messages=[
|
| 117 |
+
{"role": "system", "content": system_prompt},
|
| 118 |
+
{"role": "user", "content": query}
|
| 119 |
+
],
|
| 120 |
+
temperature=0.3
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
result_text = response.choices[0].message.content.strip()
|
| 124 |
+
result_text = result_text.replace("```json", "").replace("```", "").strip()
|
| 125 |
+
|
| 126 |
+
routing_decision = json.loads(result_text)
|
| 127 |
+
return routing_decision
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
print(f"❌ Routing error: {e}")
|
| 131 |
+
# Fallback - include common servers
|
| 132 |
+
return {
|
| 133 |
+
"intent": "general_inquiry",
|
| 134 |
+
"required_servers": ["weather", "soil_properties", "water"],
|
| 135 |
+
"reasoning": "Fallback routing due to error"
|
| 136 |
+
}
|
src/servers/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# src/__init__.py
|
| 3 |
+
# ============================================================================
|
| 4 |
+
"""
|
| 5 |
+
Farmer.Chat Backend - MCP Pipeline Package
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from .pipeline import FarmerChatPipeline
|
| 9 |
+
from .router import QueryRouter
|
| 10 |
+
from .executor import MCPExecutor, MCP_SERVER_REGISTRY
|
| 11 |
+
from .compiler import ResponseCompiler
|
| 12 |
+
from .translator import FarmerTranslator
|
| 13 |
+
from .pdf_generator import generate_pdf_report
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
'FarmerChatPipeline',
|
| 17 |
+
'QueryRouter',
|
| 18 |
+
'MCPExecutor',
|
| 19 |
+
'MCP_SERVER_REGISTRY',
|
| 20 |
+
'ResponseCompiler',
|
| 21 |
+
'FarmerTranslator',
|
| 22 |
+
'generate_pdf_report'
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ============================================================================
|
| 27 |
+
# src/servers/__init__.py
|
| 28 |
+
# ============================================================================
|
| 29 |
+
"""
|
| 30 |
+
MCP Server Implementations
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
# If you split into separate files:
|
| 34 |
+
# from .weather import WeatherServer
|
| 35 |
+
# from .soil import SoilPropertiesServer
|
| 36 |
+
# from .water import WaterServer
|
| 37 |
+
# from .elevation import ElevationServer
|
| 38 |
+
# from .pests import PestsServer
|
| 39 |
+
|
| 40 |
+
# If using combined file (recommended for simplicity):
|
| 41 |
+
# Just keep all classes in one file and import them
|
| 42 |
+
|
| 43 |
+
__all__ = [
|
| 44 |
+
'WeatherServer',
|
| 45 |
+
'SoilPropertiesServer',
|
| 46 |
+
'WaterServer',
|
| 47 |
+
'ElevationServer',
|
| 48 |
+
'PestsServer'
|
| 49 |
+
]
|
src/servers/elevation.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
All MCP Server Implementations
|
| 3 |
+
Deploy as: src/servers/__init__.py OR separate files
|
| 4 |
+
|
| 5 |
+
Contains:
|
| 6 |
+
- WeatherServer (Open-Meteo)
|
| 7 |
+
- SoilPropertiesServer (SoilGrids)
|
| 8 |
+
- WaterServer (GRACE)
|
| 9 |
+
- ElevationServer (OpenElevation)
|
| 10 |
+
- PestsServer (iNaturalist)
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import aiohttp
|
| 14 |
+
import asyncio
|
| 15 |
+
import os
|
| 16 |
+
import xarray as xr
|
| 17 |
+
import requests
|
| 18 |
+
from datetime import datetime
|
| 19 |
+
from typing import Dict, Any
|
| 20 |
+
|
| 21 |
+
# ============================================================================
|
| 22 |
+
# ELEVATION SERVER (OpenElevation)
|
| 23 |
+
# ============================================================================
|
| 24 |
+
|
| 25 |
+
class ElevationServer:
|
| 26 |
+
"""OpenElevation API Server"""
|
| 27 |
+
|
| 28 |
+
async def get_data(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 29 |
+
try:
|
| 30 |
+
url = "https://api.open-elevation.com/api/v1/lookup"
|
| 31 |
+
params = {"locations": f"{lat},{lon}"}
|
| 32 |
+
|
| 33 |
+
async with aiohttp.ClientSession() as session:
|
| 34 |
+
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
| 35 |
+
if response.status == 200:
|
| 36 |
+
data = await response.json()
|
| 37 |
+
elevation_m = data["results"][0]["elevation"]
|
| 38 |
+
return {
|
| 39 |
+
"status": "success",
|
| 40 |
+
"data": {
|
| 41 |
+
"elevation_meters": elevation_m,
|
| 42 |
+
"elevation_feet": round(elevation_m * 3.28084, 1),
|
| 43 |
+
"data_source": "OpenElevation API"
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
else:
|
| 47 |
+
return {"status": "error", "error": f"HTTP {response.status}"}
|
| 48 |
+
except Exception as e:
|
| 49 |
+
return {"status": "error", "error": str(e)}
|
src/servers/pests.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
All MCP Server Implementations
|
| 3 |
+
Deploy as: src/servers/__init__.py OR separate files
|
| 4 |
+
|
| 5 |
+
Contains:
|
| 6 |
+
- WeatherServer (Open-Meteo)
|
| 7 |
+
- SoilPropertiesServer (SoilGrids)
|
| 8 |
+
- WaterServer (GRACE)
|
| 9 |
+
- ElevationServer (OpenElevation)
|
| 10 |
+
- PestsServer (iNaturalist)
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import aiohttp
|
| 14 |
+
import asyncio
|
| 15 |
+
import os
|
| 16 |
+
import xarray as xr
|
| 17 |
+
import requests
|
| 18 |
+
from datetime import datetime
|
| 19 |
+
from typing import Dict, Any
|
| 20 |
+
|
| 21 |
+
# ============================================================================
|
| 22 |
+
# PESTS SERVER (iNaturalist)
|
| 23 |
+
# ============================================================================
|
| 24 |
+
|
| 25 |
+
class PestsServer:
|
| 26 |
+
"""iNaturalist Pest Observation Server"""
|
| 27 |
+
|
| 28 |
+
async def get_data(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 29 |
+
try:
|
| 30 |
+
url = "https://api.inaturalist.org/v1/observations"
|
| 31 |
+
params = {
|
| 32 |
+
"lat": lat,
|
| 33 |
+
"lng": lon,
|
| 34 |
+
"radius": 50, # 50km radius
|
| 35 |
+
"order": "desc",
|
| 36 |
+
"order_by": "observed_on",
|
| 37 |
+
"per_page": 20,
|
| 38 |
+
"quality_grade": "research",
|
| 39 |
+
"iconic_taxa": "Insecta"
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
async with aiohttp.ClientSession() as session:
|
| 43 |
+
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
| 44 |
+
if response.status == 200:
|
| 45 |
+
data = await response.json()
|
| 46 |
+
observations = data.get("results", [])
|
| 47 |
+
|
| 48 |
+
pest_summary = []
|
| 49 |
+
for obs in observations[:10]:
|
| 50 |
+
pest_summary.append({
|
| 51 |
+
"species": obs.get("taxon", {}).get("name", "Unknown"),
|
| 52 |
+
"common_name": obs.get("taxon", {}).get("preferred_common_name", "N/A"),
|
| 53 |
+
"observed_on": obs.get("observed_on"),
|
| 54 |
+
"distance_km": obs.get("distance", "N/A")
|
| 55 |
+
})
|
| 56 |
+
|
| 57 |
+
return {
|
| 58 |
+
"status": "success",
|
| 59 |
+
"data": {
|
| 60 |
+
"recent_observations": pest_summary,
|
| 61 |
+
"total_count": len(observations),
|
| 62 |
+
"data_source": "iNaturalist Community Data"
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
else:
|
| 66 |
+
return {"status": "error", "error": f"HTTP {response.status}"}
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return {"status": "error", "error": str(e)}
|
src/servers/soil.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import aiohttp
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
import xarray as xr
|
| 5 |
+
import requests
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# ============================================================================
|
| 11 |
+
# SOIL PROPERTIES SERVER (SoilGrids)
|
| 12 |
+
# ============================================================================
|
| 13 |
+
|
| 14 |
+
class SoilPropertiesServer:
|
| 15 |
+
"""SoilGrids API Server - Enhanced for reliability"""
|
| 16 |
+
|
| 17 |
+
async def get_data(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 18 |
+
"""Get soil properties with retry logic and rate limit handling"""
|
| 19 |
+
try:
|
| 20 |
+
properties = ["clay", "sand", "silt", "phh2o", "soc"]
|
| 21 |
+
results = {}
|
| 22 |
+
|
| 23 |
+
timeout = aiohttp.ClientTimeout(total=15, connect=5, sock_read=10)
|
| 24 |
+
connector = aiohttp.TCPConnector(limit=1, limit_per_host=1)
|
| 25 |
+
|
| 26 |
+
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
|
| 27 |
+
for prop in properties:
|
| 28 |
+
# Retry logic (max 2 attempts)
|
| 29 |
+
for attempt in range(2):
|
| 30 |
+
try:
|
| 31 |
+
url = "https://rest.isric.org/soilgrids/v2.0/properties/query"
|
| 32 |
+
params = {
|
| 33 |
+
"lon": lon,
|
| 34 |
+
"lat": lat,
|
| 35 |
+
"property": prop,
|
| 36 |
+
"depth": "0-5cm",
|
| 37 |
+
"value": "mean"
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
async with session.get(url, params=params) as response:
|
| 41 |
+
if response.status == 200:
|
| 42 |
+
data = await response.json()
|
| 43 |
+
value = data['properties']['layers'][0]['depths'][0]['values']['mean']
|
| 44 |
+
|
| 45 |
+
if value is not None:
|
| 46 |
+
if prop == 'phh2o':
|
| 47 |
+
results[prop] = round(value / 10, 1)
|
| 48 |
+
elif prop == 'soc':
|
| 49 |
+
results[prop] = value
|
| 50 |
+
else:
|
| 51 |
+
results[prop] = round(value / 10, 1)
|
| 52 |
+
else:
|
| 53 |
+
results[prop] = None
|
| 54 |
+
break
|
| 55 |
+
|
| 56 |
+
elif response.status == 429:
|
| 57 |
+
if attempt == 0:
|
| 58 |
+
await asyncio.sleep(1)
|
| 59 |
+
continue
|
| 60 |
+
else:
|
| 61 |
+
results[prop] = None
|
| 62 |
+
break
|
| 63 |
+
else:
|
| 64 |
+
results[prop] = None
|
| 65 |
+
break
|
| 66 |
+
|
| 67 |
+
except asyncio.TimeoutError:
|
| 68 |
+
if attempt == 0:
|
| 69 |
+
await asyncio.sleep(0.5)
|
| 70 |
+
continue
|
| 71 |
+
else:
|
| 72 |
+
results[prop] = None
|
| 73 |
+
break
|
| 74 |
+
except Exception:
|
| 75 |
+
results[prop] = None
|
| 76 |
+
break
|
| 77 |
+
|
| 78 |
+
await asyncio.sleep(0.2) # Delay between properties
|
| 79 |
+
|
| 80 |
+
if any(v is not None for v in results.values()):
|
| 81 |
+
return {
|
| 82 |
+
"status": "success",
|
| 83 |
+
"data": {
|
| 84 |
+
"clay_percent": results.get("clay"),
|
| 85 |
+
"sand_percent": results.get("sand"),
|
| 86 |
+
"silt_percent": results.get("silt"),
|
| 87 |
+
"pH": results.get("phh2o"),
|
| 88 |
+
"organic_carbon_dg_per_kg": results.get("soc"),
|
| 89 |
+
"data_source": "SoilGrids API (ISRIC)",
|
| 90 |
+
"location": {"latitude": lat, "longitude": lon},
|
| 91 |
+
"depth": "0-5cm (topsoil)"
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
else:
|
| 95 |
+
return {
|
| 96 |
+
"status": "error",
|
| 97 |
+
"error": "No soil data available for this location"
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return {"status": "error", "error": f"SoilGrids error: {str(e)}"}
|
src/servers/water.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import aiohttp
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
import xarray as xr
|
| 5 |
+
import requests
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
|
| 9 |
+
# ============================================================================
|
| 10 |
+
# WATER SERVER (GRACE Groundwater)
|
| 11 |
+
# ============================================================================
|
| 12 |
+
|
| 13 |
+
class WaterServer:
|
| 14 |
+
"""GRACE Groundwater Server - Real NASA Data"""
|
| 15 |
+
|
| 16 |
+
async def get_data(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 17 |
+
try:
|
| 18 |
+
loop = asyncio.get_event_loop()
|
| 19 |
+
result = await loop.run_in_executor(None, self._get_grace_sync, lat, lon)
|
| 20 |
+
return result
|
| 21 |
+
except Exception as e:
|
| 22 |
+
return {"status": "error", "error": str(e)}
|
| 23 |
+
|
| 24 |
+
def _get_grace_sync(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 25 |
+
"""Get REAL GRACE groundwater data"""
|
| 26 |
+
GRACE_URL = "https://nasagrace.unl.edu/globaldata/current/GRACEDADM_CLSM025_GL_7D.nc4"
|
| 27 |
+
cache_dir = "./grace_cache"
|
| 28 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 29 |
+
cache_path = os.path.join(cache_dir, "grace_global_current.nc4")
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
# Download if not cached
|
| 33 |
+
if not os.path.exists(cache_path):
|
| 34 |
+
response = requests.get(GRACE_URL, stream=True, timeout=120)
|
| 35 |
+
response.raise_for_status()
|
| 36 |
+
with open(cache_path, 'wb') as f:
|
| 37 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 38 |
+
f.write(chunk)
|
| 39 |
+
|
| 40 |
+
# Open NetCDF dataset
|
| 41 |
+
ds = xr.open_dataset(cache_path)
|
| 42 |
+
point_data = ds.sel(lat=lat, lon=lon, method='nearest')
|
| 43 |
+
|
| 44 |
+
# Extract percentiles
|
| 45 |
+
gw_percentile = float(point_data['gws_inst'].values.item())
|
| 46 |
+
rtzsm_percentile = float(point_data['rtzsm_inst'].values.item())
|
| 47 |
+
sfsm_percentile = float(point_data['sfsm_inst'].values.item())
|
| 48 |
+
|
| 49 |
+
timestamp = str(point_data['time'].values)[:10]
|
| 50 |
+
|
| 51 |
+
# Drought category
|
| 52 |
+
if gw_percentile < 20:
|
| 53 |
+
drought_category = "severe_drought"
|
| 54 |
+
severity = "SEVERE"
|
| 55 |
+
elif gw_percentile < 40:
|
| 56 |
+
drought_category = "moderate_drought"
|
| 57 |
+
severity = "MODERATE"
|
| 58 |
+
elif gw_percentile < 60:
|
| 59 |
+
drought_category = "normal"
|
| 60 |
+
severity = "LOW"
|
| 61 |
+
else:
|
| 62 |
+
drought_category = "wet"
|
| 63 |
+
severity = "LOW"
|
| 64 |
+
|
| 65 |
+
ds.close()
|
| 66 |
+
|
| 67 |
+
return {
|
| 68 |
+
"status": "success",
|
| 69 |
+
"data": {
|
| 70 |
+
"groundwater_percentile": round(gw_percentile, 1),
|
| 71 |
+
"soil_moisture_percentile": round(rtzsm_percentile, 1),
|
| 72 |
+
"surface_soil_moisture_percentile": round(sfsm_percentile, 1),
|
| 73 |
+
"total_water_storage_anomaly_cm": round((gw_percentile - 50) * 0.1, 2),
|
| 74 |
+
"drought_category": drought_category,
|
| 75 |
+
"severity": severity,
|
| 76 |
+
"interpretation": f"Groundwater at {gw_percentile:.1f}th percentile",
|
| 77 |
+
"data_source": "GRACE-FO Satellite (Real NetCDF Data)",
|
| 78 |
+
"timestamp": timestamp
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
# Seasonal fallback
|
| 84 |
+
month = datetime.now().month
|
| 85 |
+
gw_estimate = 48 if 6 <= month <= 9 else 28
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
"status": "success",
|
| 89 |
+
"data": {
|
| 90 |
+
"groundwater_percentile": gw_estimate,
|
| 91 |
+
"soil_moisture_percentile": gw_estimate + 5,
|
| 92 |
+
"drought_category": "moderate_drought" if gw_estimate < 40 else "normal",
|
| 93 |
+
"severity": "MODERATE" if gw_estimate < 40 else "LOW",
|
| 94 |
+
"data_source": "Seasonal Estimate (GRACE download failed)"
|
| 95 |
+
}
|
| 96 |
+
}
|
src/servers/weather.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import aiohttp
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
import xarray as xr
|
| 5 |
+
import requests
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# ============================================================================
|
| 11 |
+
# WEATHER SERVER (Open-Meteo)
|
| 12 |
+
# ============================================================================
|
| 13 |
+
|
| 14 |
+
class WeatherServer:
|
| 15 |
+
"""Open-Meteo Weather API Server"""
|
| 16 |
+
|
| 17 |
+
async def get_data(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 18 |
+
try:
|
| 19 |
+
url = "https://api.open-meteo.com/v1/forecast"
|
| 20 |
+
params = {
|
| 21 |
+
"latitude": lat,
|
| 22 |
+
"longitude": lon,
|
| 23 |
+
"current": "temperature_2m,precipitation,wind_speed_10m,relative_humidity_2m",
|
| 24 |
+
"daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,rain_sum",
|
| 25 |
+
"timezone": "Asia/Kolkata",
|
| 26 |
+
"forecast_days": 7
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async with aiohttp.ClientSession() as session:
|
| 30 |
+
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
| 31 |
+
if response.status == 200:
|
| 32 |
+
data = await response.json()
|
| 33 |
+
return {
|
| 34 |
+
"status": "success",
|
| 35 |
+
"data": {
|
| 36 |
+
"current_temp_c": data["current"]["temperature_2m"],
|
| 37 |
+
"current_precipitation_mm": data["current"]["precipitation"],
|
| 38 |
+
"wind_speed_kmh": data["current"]["wind_speed_10m"],
|
| 39 |
+
"humidity_percent": data["current"]["relative_humidity_2m"],
|
| 40 |
+
"forecast_7day": {
|
| 41 |
+
"max_temps": data["daily"]["temperature_2m_max"],
|
| 42 |
+
"min_temps": data["daily"]["temperature_2m_min"],
|
| 43 |
+
"precipitation_mm": data["daily"]["precipitation_sum"],
|
| 44 |
+
"rain_mm": data["daily"]["rain_sum"]
|
| 45 |
+
},
|
| 46 |
+
"data_source": "Open-Meteo API"
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
else:
|
| 50 |
+
return {"status": "error", "error": f"HTTP {response.status}"}
|
| 51 |
+
except Exception as e:
|
| 52 |
+
return {"status": "error", "error": str(e)}
|
src/translator.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stage 4: Farmer Translator - Natural Language Output
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FarmerTranslator:
|
| 11 |
+
"""Stage 4: Convert technical data to farmer-friendly advice"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, client: OpenAI):
|
| 14 |
+
self.client = client
|
| 15 |
+
|
| 16 |
+
def translate(self, query: str, compiled_data: Dict[str, Any], location: Dict[str, Any]) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Generate farmer-friendly response from technical data
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
str: Natural language advice for farmers
|
| 22 |
+
"""
|
| 23 |
+
data_summary = json.dumps(compiled_data.get("data", {}), indent=2)
|
| 24 |
+
|
| 25 |
+
system_prompt = f"""You are an agricultural advisor for farmers in {location['name']}.
|
| 26 |
+
|
| 27 |
+
Task: Convert technical data into clear, actionable advice.
|
| 28 |
+
|
| 29 |
+
Guidelines:
|
| 30 |
+
1. Use simple language (avoid jargon)
|
| 31 |
+
2. Provide specific, actionable recommendations
|
| 32 |
+
3. Include risk levels (LOW/MODERATE/HIGH) when relevant
|
| 33 |
+
4. Explain WHY you're making recommendations
|
| 34 |
+
5. If data is missing, acknowledge it but provide useful advice
|
| 35 |
+
|
| 36 |
+
Structure:
|
| 37 |
+
- Clear summary
|
| 38 |
+
- Current conditions
|
| 39 |
+
- Risk assessment (if applicable)
|
| 40 |
+
- Specific recommendations
|
| 41 |
+
- Action items
|
| 42 |
+
|
| 43 |
+
Data from {len(compiled_data.get('successful_servers', []))} sources:
|
| 44 |
+
{data_summary}
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
if compiled_data.get("failed_servers"):
|
| 48 |
+
system_prompt += f"\n\nNote: Some sources failed: {compiled_data['failed_servers']}"
|
| 49 |
+
system_prompt += "\nWork with available data, note limitations."
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
response = self.client.chat.completions.create(
|
| 53 |
+
model="gpt-4o",
|
| 54 |
+
messages=[
|
| 55 |
+
{"role": "system", "content": system_prompt},
|
| 56 |
+
{"role": "user", "content": f"Farmer query: {query}\n\nProvide advice based on the data."}
|
| 57 |
+
],
|
| 58 |
+
temperature=0.7
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
return response.choices[0].message.content
|
| 62 |
+
|
| 63 |
+
except Exception as e:
|
| 64 |
+
return f"⚠️ Unable to generate advice: {str(e)}"
|