Spaces:
Running
Running
akashub
commited on
Commit
·
6afc01a
1
Parent(s):
8ce5453
fix: re-adding local code files
Browse files- README_ref.md +213 -0
- app.py +1247 -0
- requirements.txt +53 -0
- src/__init__.py +37 -0
- src/compiler.py +265 -0
- src/executor.py +481 -0
- src/pdf_generator.py +162 -0
- src/pipeline.py +277 -0
- src/router.py +194 -0
- src/servers/__init__.py +18 -0
- src/servers/elevation.py +49 -0
- src/servers/pests.py +68 -0
- src/servers/soil.py +109 -0
- src/servers/water.py +96 -0
- src/servers/weather.py +52 -0
- src/translator.py +64 -0
README_ref.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,1247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 20 |
+
# # from openai import OpenAI
|
| 21 |
+
# # import httpx
|
| 22 |
+
|
| 23 |
+
# # # Initialize FastAPI
|
| 24 |
+
# # app = FastAPI(
|
| 25 |
+
# # title="Farmer.Chat Backend",
|
| 26 |
+
# # description="Multi-stage MCP pipeline for agricultural intelligence",
|
| 27 |
+
# # version="2.0.0"
|
| 28 |
+
# # )
|
| 29 |
+
|
| 30 |
+
# # # CORS - Allow all origins for demo (restrict in production)
|
| 31 |
+
# # app.add_middleware(
|
| 32 |
+
# # CORSMiddleware,
|
| 33 |
+
# # allow_origins=["*"],
|
| 34 |
+
# # allow_credentials=True,
|
| 35 |
+
# # allow_methods=["*"],
|
| 36 |
+
# # allow_headers=["*"],
|
| 37 |
+
# # )
|
| 38 |
+
|
| 39 |
+
# # # Initialize OpenAI client with FIXED httpx configuration
|
| 40 |
+
# # OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
| 41 |
+
# # if not OPENAI_API_KEY:
|
| 42 |
+
# # raise ValueError("OPENAI_API_KEY environment variable not set!")
|
| 43 |
+
|
| 44 |
+
# # # FIX: Create httpx client without proxy support
|
| 45 |
+
# # http_client = httpx.Client(
|
| 46 |
+
# # timeout=httpx.Timeout(60.0),
|
| 47 |
+
# # limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
|
| 48 |
+
# # )
|
| 49 |
+
|
| 50 |
+
# # # Initialize OpenAI with custom http client (bypasses proxy issues)
|
| 51 |
+
# # openai_client = OpenAI(
|
| 52 |
+
# # api_key=OPENAI_API_KEY,
|
| 53 |
+
# # http_client=http_client
|
| 54 |
+
# # )
|
| 55 |
+
|
| 56 |
+
# # print("✅ OpenAI client initialized with custom httpx client")
|
| 57 |
+
# # print(f" Model: gpt-4o")
|
| 58 |
+
|
| 59 |
+
# # # Default location (Bangalore Agricultural Region)
|
| 60 |
+
# # DEFAULT_LOCATION = {
|
| 61 |
+
# # "name": "Bangalore Agricultural Region",
|
| 62 |
+
# # "lat": 12.8716,
|
| 63 |
+
# # "lon": 77.4946
|
| 64 |
+
# # }
|
| 65 |
+
|
| 66 |
+
# # # Initialize pipeline
|
| 67 |
+
# # pipeline = FarmerChatPipeline(openai_client, DEFAULT_LOCATION)
|
| 68 |
+
|
| 69 |
+
# # # Request/Response Models
|
| 70 |
+
# # class QueryRequest(BaseModel):
|
| 71 |
+
# # query: str = Field(..., min_length=3, max_length=500, description="Farmer's question")
|
| 72 |
+
# # location: Optional[Dict[str, Any]] = Field(None, description="Custom location (lat, lon, name)")
|
| 73 |
+
|
| 74 |
+
# # class Config:
|
| 75 |
+
# # json_schema_extra = {
|
| 76 |
+
# # "example": {
|
| 77 |
+
# # "query": "Should I plant rice today?",
|
| 78 |
+
# # "location": {
|
| 79 |
+
# # "name": "Bangalore",
|
| 80 |
+
# # "lat": 12.8716,
|
| 81 |
+
# # "lon": 77.4946
|
| 82 |
+
# # }
|
| 83 |
+
# # }
|
| 84 |
+
# # }
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# # class QueryResponse(BaseModel):
|
| 88 |
+
# # success: bool
|
| 89 |
+
# # query: str
|
| 90 |
+
# # advice: str
|
| 91 |
+
# # routing: Dict[str, Any]
|
| 92 |
+
# # data: Dict[str, Any]
|
| 93 |
+
# # execution_time_seconds: float
|
| 94 |
+
# # timestamp: str
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# # # Health check
|
| 98 |
+
# # @app.get("/")
|
| 99 |
+
# # async def root():
|
| 100 |
+
# # return {
|
| 101 |
+
# # "service": "Farmer.Chat Backend",
|
| 102 |
+
# # "status": "operational",
|
| 103 |
+
# # "version": "2.0.0",
|
| 104 |
+
# # "endpoints": {
|
| 105 |
+
# # "query": "/api/query",
|
| 106 |
+
# # "health": "/api/health",
|
| 107 |
+
# # "servers": "/api/servers"
|
| 108 |
+
# # }
|
| 109 |
+
# # }
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# # @app.get("/api/health")
|
| 113 |
+
# # async def health_check():
|
| 114 |
+
# # """Health check endpoint"""
|
| 115 |
+
# # return {
|
| 116 |
+
# # "status": "healthy",
|
| 117 |
+
# # "timestamp": datetime.now().isoformat(),
|
| 118 |
+
# # "openai_configured": bool(OPENAI_API_KEY),
|
| 119 |
+
# # "location": DEFAULT_LOCATION
|
| 120 |
+
# # }
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# # @app.get("/api/servers")
|
| 124 |
+
# # async def list_servers():
|
| 125 |
+
# # """List available MCP servers"""
|
| 126 |
+
# # from src.executor import MCP_SERVER_REGISTRY
|
| 127 |
+
|
| 128 |
+
# # return {
|
| 129 |
+
# # "total_servers": len(MCP_SERVER_REGISTRY),
|
| 130 |
+
# # "servers": MCP_SERVER_REGISTRY
|
| 131 |
+
# # }
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# # @app.post("/api/query", response_model=QueryResponse)
|
| 135 |
+
# # async def process_query(request: QueryRequest):
|
| 136 |
+
# # """
|
| 137 |
+
# # Main query endpoint - processes farmer questions through MCP pipeline
|
| 138 |
+
# # """
|
| 139 |
+
# # try:
|
| 140 |
+
# # start_time = time.time()
|
| 141 |
+
|
| 142 |
+
# # # Use custom location if provided, otherwise default
|
| 143 |
+
# # location = request.location if request.location else DEFAULT_LOCATION
|
| 144 |
+
|
| 145 |
+
# # # Update pipeline location if changed
|
| 146 |
+
# # if request.location:
|
| 147 |
+
# # pipeline.location = location
|
| 148 |
+
|
| 149 |
+
# # # Process query through pipeline
|
| 150 |
+
# # result = await pipeline.process_query(request.query, verbose=False)
|
| 151 |
+
|
| 152 |
+
# # execution_time = time.time() - start_time
|
| 153 |
+
|
| 154 |
+
# # return QueryResponse(
|
| 155 |
+
# # success=True,
|
| 156 |
+
# # query=request.query,
|
| 157 |
+
# # advice=result["advice"],
|
| 158 |
+
# # routing=result["routing"],
|
| 159 |
+
# # data=result["compiled_data"],
|
| 160 |
+
# # execution_time_seconds=round(execution_time, 2),
|
| 161 |
+
# # timestamp=datetime.now().isoformat()
|
| 162 |
+
# # )
|
| 163 |
+
|
| 164 |
+
# # except Exception as e:
|
| 165 |
+
# # raise HTTPException(status_code=500, detail=str(e))
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# # @app.post("/api/export-pdf")
|
| 169 |
+
# # async def export_pdf(request: QueryRequest):
|
| 170 |
+
# # """
|
| 171 |
+
# # Export query result as PDF
|
| 172 |
+
# # """
|
| 173 |
+
# # try:
|
| 174 |
+
# # # Process query
|
| 175 |
+
# # result = await pipeline.process_query(request.query, verbose=False)
|
| 176 |
+
|
| 177 |
+
# # # Generate PDF
|
| 178 |
+
# # pdf_path = generate_pdf_report(
|
| 179 |
+
# # query=request.query,
|
| 180 |
+
# # advice=result["advice"],
|
| 181 |
+
# # data=result["compiled_data"],
|
| 182 |
+
# # location=pipeline.location
|
| 183 |
+
# # )
|
| 184 |
+
|
| 185 |
+
# # # Return PDF file
|
| 186 |
+
# # return FileResponse(
|
| 187 |
+
# # pdf_path,
|
| 188 |
+
# # media_type="application/pdf",
|
| 189 |
+
# # filename=f"farmer-chat-report-{int(time.time())}.pdf"
|
| 190 |
+
# # )
|
| 191 |
+
|
| 192 |
+
# # except Exception as e:
|
| 193 |
+
# # raise HTTPException(status_code=500, detail=str(e))
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# # # Error handlers
|
| 197 |
+
# # @app.exception_handler(404)
|
| 198 |
+
# # async def not_found_handler(request, exc):
|
| 199 |
+
# # return JSONResponse(
|
| 200 |
+
# # status_code=404,
|
| 201 |
+
# # content={"error": "Endpoint not found", "path": str(request.url)}
|
| 202 |
+
# # )
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# # @app.exception_handler(500)
|
| 206 |
+
# # async def server_error_handler(request, exc):
|
| 207 |
+
# # return JSONResponse(
|
| 208 |
+
# # status_code=500,
|
| 209 |
+
# # content={"error": "Internal server error", "detail": str(exc)}
|
| 210 |
+
# # )
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# # if __name__ == "__main__":
|
| 214 |
+
# # import uvicorn
|
| 215 |
+
# # uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 216 |
+
|
| 217 |
+
# """
|
| 218 |
+
# Alert Summary Generator Backend - Standalone Version
|
| 219 |
+
# FastAPI app with embedded pipeline and location data
|
| 220 |
+
# """
|
| 221 |
+
|
| 222 |
+
# from fastapi import FastAPI, HTTPException
|
| 223 |
+
# from fastapi.middleware.cors import CORSMiddleware
|
| 224 |
+
# from pydantic import BaseModel
|
| 225 |
+
# from typing import Optional, Dict, Any, List
|
| 226 |
+
# import os
|
| 227 |
+
# from datetime import datetime
|
| 228 |
+
# from openai import OpenAI
|
| 229 |
+
# import httpx
|
| 230 |
+
|
| 231 |
+
# # ============================================================================
|
| 232 |
+
# # OPENAI CLIENT SETUP
|
| 233 |
+
# # ============================================================================
|
| 234 |
+
|
| 235 |
+
# def get_openai_client():
|
| 236 |
+
# """Initialize OpenAI client with API key from environment"""
|
| 237 |
+
# api_key = os.getenv("OPENAI_API_KEY")
|
| 238 |
+
# if not api_key:
|
| 239 |
+
# raise ValueError("OPENAI_API_KEY environment variable not set")
|
| 240 |
+
# http_client = httpx.Client(
|
| 241 |
+
# timeout=httpx.Timeout(60.0, connect=10.0),
|
| 242 |
+
# limits=httpx.Limits(max_keepalive_connections=10, max_connections=20)
|
| 243 |
+
# )
|
| 244 |
+
# return OpenAI(api_key=api_key, http_client=http_client)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# # ============================================================================
|
| 248 |
+
# # PIPELINE COMPONENTS
|
| 249 |
+
# # ============================================================================
|
| 250 |
+
|
| 251 |
+
# class QueryRouter:
|
| 252 |
+
# """Routes queries to appropriate MCP servers"""
|
| 253 |
+
|
| 254 |
+
# def __init__(self, client):
|
| 255 |
+
# self.client = client
|
| 256 |
+
|
| 257 |
+
# def route_query(self, query: str, location: Dict[str, float]) -> Dict[str, Any]:
|
| 258 |
+
# """Determine which servers to call based on query"""
|
| 259 |
+
# prompt = f"""Given the farmer query: "{query}"
|
| 260 |
+
# Location: {location['latitude']}, {location['longitude']}
|
| 261 |
+
|
| 262 |
+
# Determine which data sources are needed. Return JSON:
|
| 263 |
+
# {{
|
| 264 |
+
# "weather": true/false,
|
| 265 |
+
# "soil": true/false,
|
| 266 |
+
# "water": true/false,
|
| 267 |
+
# "elevation": true/false,
|
| 268 |
+
# "pests": true/false
|
| 269 |
+
# }}"""
|
| 270 |
+
|
| 271 |
+
# response = self.client.chat.completions.create(
|
| 272 |
+
# model="gpt-4",
|
| 273 |
+
# messages=[{"role": "user", "content": prompt}],
|
| 274 |
+
# temperature=0
|
| 275 |
+
# )
|
| 276 |
+
|
| 277 |
+
# # Parse routing decision
|
| 278 |
+
# import json
|
| 279 |
+
# try:
|
| 280 |
+
# routing = json.loads(response.choices[0].message.content)
|
| 281 |
+
# except:
|
| 282 |
+
# # Default: query all servers for alerts
|
| 283 |
+
# routing = {
|
| 284 |
+
# "weather": True,
|
| 285 |
+
# "soil": True,
|
| 286 |
+
# "water": True,
|
| 287 |
+
# "elevation": True,
|
| 288 |
+
# "pests": True
|
| 289 |
+
# }
|
| 290 |
+
|
| 291 |
+
# return routing
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
# class MCPExecutor:
|
| 295 |
+
# """Executes parallel calls to MCP servers"""
|
| 296 |
+
|
| 297 |
+
# def __init__(self, client):
|
| 298 |
+
# self.client = client
|
| 299 |
+
|
| 300 |
+
# def execute_parallel(self, routing: Dict[str, bool], location: Dict[str, float]) -> Dict[str, str]:
|
| 301 |
+
# """Execute MCP server calls in parallel"""
|
| 302 |
+
# results = {}
|
| 303 |
+
|
| 304 |
+
# # Simulate MCP server calls (replace with actual server calls)
|
| 305 |
+
# if routing.get("weather"):
|
| 306 |
+
# results["weather"] = f"Weather data for {location['latitude']}, {location['longitude']}"
|
| 307 |
+
|
| 308 |
+
# if routing.get("soil"):
|
| 309 |
+
# results["soil"] = f"Soil data for {location['latitude']}, {location['longitude']}"
|
| 310 |
+
|
| 311 |
+
# if routing.get("water"):
|
| 312 |
+
# results["water"] = f"Water availability for {location['latitude']}, {location['longitude']}"
|
| 313 |
+
|
| 314 |
+
# if routing.get("elevation"):
|
| 315 |
+
# results["elevation"] = f"Elevation data for {location['latitude']}, {location['longitude']}"
|
| 316 |
+
|
| 317 |
+
# if routing.get("pests"):
|
| 318 |
+
# results["pests"] = f"Pest risk data for {location['latitude']}, {location['longitude']}"
|
| 319 |
+
|
| 320 |
+
# return results
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
# class ResponseCompiler:
|
| 324 |
+
# """Compiles MCP results into coherent response"""
|
| 325 |
+
|
| 326 |
+
# def __init__(self, client):
|
| 327 |
+
# self.client = client
|
| 328 |
+
|
| 329 |
+
# def compile_response(self, query: str, mcp_results: Dict[str, str], location: Dict[str, float]) -> str:
|
| 330 |
+
# """Compile MCP results into final response"""
|
| 331 |
+
|
| 332 |
+
# # Format MCP results for context
|
| 333 |
+
# context = "\n\n".join([f"{k.upper()}: {v}" for k, v in mcp_results.items()])
|
| 334 |
+
|
| 335 |
+
# prompt = f"""You are an agricultural assistant. Compile this data into a comprehensive alert summary.
|
| 336 |
+
|
| 337 |
+
# FARMER QUERY: {query}
|
| 338 |
+
# LOCATION: {location['latitude']}, {location['longitude']}
|
| 339 |
+
|
| 340 |
+
# DATA FROM SOURCES:
|
| 341 |
+
# {context}
|
| 342 |
+
|
| 343 |
+
# Provide a comprehensive agricultural alert covering:
|
| 344 |
+
# 1. Current weather conditions and forecast
|
| 345 |
+
# 2. Soil health and recommendations
|
| 346 |
+
# 3. Water availability status
|
| 347 |
+
# 4. Elevation/topography considerations
|
| 348 |
+
# 5. Pest risks and preventive measures
|
| 349 |
+
|
| 350 |
+
# Be specific, actionable, and farmer-friendly."""
|
| 351 |
+
|
| 352 |
+
# response = self.client.chat.completions.create(
|
| 353 |
+
# model="gpt-4",
|
| 354 |
+
# messages=[{"role": "user", "content": prompt}],
|
| 355 |
+
# temperature=0.7
|
| 356 |
+
# )
|
| 357 |
+
|
| 358 |
+
# return response.choices[0].message.content
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
# class HindiTranslator:
|
| 362 |
+
# """Translates responses to Hindi"""
|
| 363 |
+
|
| 364 |
+
# def __init__(self, client):
|
| 365 |
+
# self.client = client
|
| 366 |
+
|
| 367 |
+
# def translate(self, text: str, target_lang: str = "en") -> str:
|
| 368 |
+
# """Translate text if needed"""
|
| 369 |
+
# if target_lang == "en":
|
| 370 |
+
# return text
|
| 371 |
+
|
| 372 |
+
# # Add Hindi translation logic here
|
| 373 |
+
# return text
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
# class FarmerChatPipeline:
|
| 377 |
+
# """Main pipeline orchestrating all stages"""
|
| 378 |
+
|
| 379 |
+
# def __init__(self, client, location: Dict[str, float]):
|
| 380 |
+
# self.client = client
|
| 381 |
+
# self.location = location
|
| 382 |
+
# self.router = QueryRouter(client)
|
| 383 |
+
# self.executor = MCPExecutor(client)
|
| 384 |
+
# self.compiler = ResponseCompiler(client)
|
| 385 |
+
# self.translator = HindiTranslator(client)
|
| 386 |
+
|
| 387 |
+
# def process_query(self, query: str, language: str = "en") -> str:
|
| 388 |
+
# """Process query through full pipeline"""
|
| 389 |
+
|
| 390 |
+
# # Stage 1: Route query
|
| 391 |
+
# routing = self.router.route_query(query, self.location)
|
| 392 |
+
|
| 393 |
+
# # Stage 2: Execute MCP calls
|
| 394 |
+
# mcp_results = self.executor.execute_parallel(routing, self.location)
|
| 395 |
+
|
| 396 |
+
# # Stage 3: Compile response
|
| 397 |
+
# response = self.compiler.compile_response(query, mcp_results, self.location)
|
| 398 |
+
|
| 399 |
+
# # Stage 4: Translate if needed
|
| 400 |
+
# final_response = self.translator.translate(response, language)
|
| 401 |
+
|
| 402 |
+
# return final_response
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
# # ============================================================================
|
| 406 |
+
# # BIHAR LOCATION DATA
|
| 407 |
+
# # ============================================================================
|
| 408 |
+
|
| 409 |
+
# BIHAR_DATA = {
|
| 410 |
+
# "Araria": ["Araria", "Forbesganj", "Jokihat", "Raniganj"],
|
| 411 |
+
# "Arwal": ["Arwal", "Kaler", "Karpi", "Kurtha"],
|
| 412 |
+
# "Aurangabad": ["Aurangabad", "Daudnagar", "Obra", "Nabinagar"],
|
| 413 |
+
# "Banka": ["Banka", "Amarpur", "Barahat", "Belhar"],
|
| 414 |
+
# "Begusarai": ["Begusarai", "Bakhri", "Barauni", "Teghra"],
|
| 415 |
+
# "Bhagalpur": ["Bhagalpur", "Sabour", "Nathnagar", "Kahalgaon"],
|
| 416 |
+
# "Bhojpur": ["Arrah", "Jagdishpur", "Piro", "Shahpur"],
|
| 417 |
+
# "Buxar": ["Buxar", "Dumraon", "Chausa", "Simri"],
|
| 418 |
+
# "Darbhanga": ["Darbhanga", "Baheri", "Jale", "Benipur"],
|
| 419 |
+
# "East Champaran": ["Motihari", "Raxaul", "Chakia", "Dhaka"],
|
| 420 |
+
# "Gaya": ["Gaya", "Bodh Gaya", "Tekari", "Sherghati"],
|
| 421 |
+
# "Gopalganj": ["Gopalganj", "Barauli", "Baikunthpur", "Kateya"],
|
| 422 |
+
# "Jamui": ["Jamui", "Jhajha", "Sikandra", "Sono"],
|
| 423 |
+
# "Jehanabad": ["Jehanabad", "Ghoshi", "Makhdumpur", "Modanganj"],
|
| 424 |
+
# "Kaimur": ["Bhabua", "Mohania", "Ramgarh", "Chainpur"],
|
| 425 |
+
# "Katihar": ["Katihar", "Barsoi", "Manihari", "Pranpur"],
|
| 426 |
+
# "Khagaria": ["Khagaria", "Parbatta", "Alauli", "Beldaur"],
|
| 427 |
+
# "Kishanganj": ["Kishanganj", "Bahadurganj", "Thakurganj", "Dighalbank"],
|
| 428 |
+
# "Lakhisarai": ["Lakhisarai", "Halsi", "Suryagarha", "Pipariya"],
|
| 429 |
+
# "Madhepura": ["Madhepura", "Udakishanganj", "Murliganj", "Alamnagar"],
|
| 430 |
+
# "Madhubani": ["Madhubani", "Jhanjharpur", "Benipatti", "Jainagar"],
|
| 431 |
+
# "Munger": ["Munger", "Jamalpur", "Asarganj", "Tarapur"],
|
| 432 |
+
# "Muzaffarpur": ["Muzaffarpur", "Sitamarhi", "Minapur", "Bochaha"],
|
| 433 |
+
# "Nalanda": ["Bihar Sharif", "Rajgir", "Hilsa", "Biharsharif"],
|
| 434 |
+
# "Nawada": ["Nawada", "Rajauli", "Akbarpur", "Hisua"],
|
| 435 |
+
# "Patna": ["Patna", "Danapur", "Fatuha", "Khagaul"],
|
| 436 |
+
# "Purnia": ["Purnia", "Dhamdaha", "Kasba", "Banmankhi"],
|
| 437 |
+
# "Rohtas": ["Sasaram", "Dehri", "Bikramganj", "Nasriganj"],
|
| 438 |
+
# "Saharsa": ["Saharsa", "Sonbarsa", "Simri Bakhtiarpur", "Mahishi"],
|
| 439 |
+
# "Samastipur": ["Samastipur", "Rosera", "Dalsinghsarai", "Pusa"],
|
| 440 |
+
# "Saran": ["Chapra", "Marhaura", "Amnour", "Sonepur"],
|
| 441 |
+
# "Sheikhpura": ["Sheikhpura", "Barbigha", "Ariari", "Shekhopur"],
|
| 442 |
+
# "Sheohar": ["Sheohar", "Dumri Katsari", "Piprahi", "Tariyani"],
|
| 443 |
+
# "Sitamarhi": ["Sitamarhi", "Pupri", "Belsand", "Bathnaha"],
|
| 444 |
+
# "Siwan": ["Siwan", "Maharajganj", "Mairwa", "Darauli"],
|
| 445 |
+
# "Supaul": ["Supaul", "Nirmali", "Triveniganj", "Chhatapur"],
|
| 446 |
+
# "Vaishali": ["Hajipur", "Mahua", "Lalganj", "Desri"],
|
| 447 |
+
# "West Champaran": ["Bettiah", "Bagaha", "Narkatiaganj", "Lauriya"]
|
| 448 |
+
# }
|
| 449 |
+
|
| 450 |
+
# LOCATIONS = {
|
| 451 |
+
# "Araria": {"latitude": 26.1523, "longitude": 87.5167},
|
| 452 |
+
# "Forbesganj": {"latitude": 26.3023, "longitude": 87.2664},
|
| 453 |
+
# "Jokihat": {"latitude": 25.8998, "longitude": 87.2686},
|
| 454 |
+
# "Raniganj": {"latitude": 26.0537, "longitude": 87.5333},
|
| 455 |
+
# "Arwal": {"latitude": 25.2560, "longitude": 84.6819},
|
| 456 |
+
# "Kaler": {"latitude": 25.1960, "longitude": 84.6219},
|
| 457 |
+
# "Karpi": {"latitude": 25.2360, "longitude": 84.7019},
|
| 458 |
+
# "Kurtha": {"latitude": 25.3160, "longitude": 84.6619},
|
| 459 |
+
# "Aurangabad": {"latitude": 24.7521, "longitude": 84.3742},
|
| 460 |
+
# "Daudnagar": {"latitude": 25.0337, "longitude": 84.4007},
|
| 461 |
+
# "Obra": {"latitude": 24.9923, "longitude": 84.4342},
|
| 462 |
+
# "Nabinagar": {"latitude": 24.6087, "longitude": 84.1269},
|
| 463 |
+
# "Banka": {"latitude": 24.8893, "longitude": 86.9220},
|
| 464 |
+
# "Amarpur": {"latitude": 25.0393, "longitude": 86.9020},
|
| 465 |
+
# "Barahat": {"latitude": 24.8393, "longitude": 87.0020},
|
| 466 |
+
# "Belhar": {"latitude": 24.9393, "longitude": 86.9620},
|
| 467 |
+
# "Begusarai": {"latitude": 25.4182, "longitude": 86.1347},
|
| 468 |
+
# "Bakhri": {"latitude": 25.4582, "longitude": 86.0547},
|
| 469 |
+
# "Barauni": {"latitude": 25.4751, "longitude": 86.0458},
|
| 470 |
+
# "Teghra": {"latitude": 25.5082, "longitude": 85.9347},
|
| 471 |
+
# "Bhagalpur": {"latitude": 25.2425, "longitude": 86.9842},
|
| 472 |
+
# "Sabour": {"latitude": 25.2375, "longitude": 87.0542},
|
| 473 |
+
# "Nathnagar": {"latitude": 25.1225, "longitude": 87.0042},
|
| 474 |
+
# "Kahalgaon": {"latitude": 25.1925, "longitude": 87.2142},
|
| 475 |
+
# "Arrah": {"latitude": 25.5560, "longitude": 84.6631},
|
| 476 |
+
# "Jagdishpur": {"latitude": 25.4660, "longitude": 84.4231},
|
| 477 |
+
# "Piro": {"latitude": 25.3260, "longitude": 84.4031},
|
| 478 |
+
# "Shahpur": {"latitude": 25.6060, "longitude": 84.4031},
|
| 479 |
+
# "Buxar": {"latitude": 25.5641, "longitude": 83.9778},
|
| 480 |
+
# "Dumraon": {"latitude": 25.5541, "longitude": 84.1478},
|
| 481 |
+
# "Chausa": {"latitude": 25.5241, "longitude": 83.9178},
|
| 482 |
+
# "Simri": {"latitude": 25.6141, "longitude": 84.0478},
|
| 483 |
+
# "Darbhanga": {"latitude": 26.1542, "longitude": 85.8978},
|
| 484 |
+
# "Baheri": {"latitude": 26.0442, "longitude": 85.8378},
|
| 485 |
+
# "Jale": {"latitude": 26.2042, "longitude": 85.8578},
|
| 486 |
+
# "Benipur": {"latitude": 26.1142, "longitude": 85.9478},
|
| 487 |
+
# "Motihari": {"latitude": 26.6484, "longitude": 84.9194},
|
| 488 |
+
# "Raxaul": {"latitude": 26.9784, "longitude": 84.8494},
|
| 489 |
+
# "Chakia": {"latitude": 26.4184, "longitude": 85.0494},
|
| 490 |
+
# "Dhaka": {"latitude": 26.6784, "longitude": 85.1694},
|
| 491 |
+
# "Gaya": {"latitude": 24.7955, "longitude": 85.0002},
|
| 492 |
+
# "Bodh Gaya": {"latitude": 24.6955, "longitude": 84.9902},
|
| 493 |
+
# "Tekari": {"latitude": 24.9455, "longitude": 85.0402},
|
| 494 |
+
# "Sherghati": {"latitude": 24.5655, "longitude": 84.7902},
|
| 495 |
+
# "Gopalganj": {"latitude": 26.4685, "longitude": 84.4388},
|
| 496 |
+
# "Barauli": {"latitude": 26.3785, "longitude": 84.5788},
|
| 497 |
+
# "Baikunthpur": {"latitude": 26.5285, "longitude": 84.3588},
|
| 498 |
+
# "Kateya": {"latitude": 26.4285, "longitude": 84.6388},
|
| 499 |
+
# "Jamui": {"latitude": 24.9272, "longitude": 86.2231},
|
| 500 |
+
# "Jhajha": {"latitude": 24.7772, "longitude": 86.3731},
|
| 501 |
+
# "Sikandra": {"latitude": 24.9672, "longitude": 86.0631},
|
| 502 |
+
# "Sono": {"latitude": 24.8372, "longitude": 86.1431},
|
| 503 |
+
# "Jehanabad": {"latitude": 25.2078, "longitude": 84.9869},
|
| 504 |
+
# "Ghoshi": {"latitude": 25.1478, "longitude": 84.9269},
|
| 505 |
+
# "Makhdumpur": {"latitude": 25.2478, "longitude": 85.0469},
|
| 506 |
+
# "Modanganj": {"latitude": 25.2678, "longitude": 84.9069},
|
| 507 |
+
# "Bhabua": {"latitude": 25.0405, "longitude": 83.6074},
|
| 508 |
+
# "Mohania": {"latitude": 25.1305, "longitude": 83.4774},
|
| 509 |
+
# "Ramgarh": {"latitude": 24.9505, "longitude": 83.6874},
|
| 510 |
+
# "Chainpur": {"latitude": 25.2005, "longitude": 83.7474},
|
| 511 |
+
# "Katihar": {"latitude": 25.5394, "longitude": 87.5839},
|
| 512 |
+
# "Barsoi": {"latitude": 25.3794, "longitude": 87.8839},
|
| 513 |
+
# "Manihari": {"latitude": 25.3394, "longitude": 87.6239},
|
| 514 |
+
# "Pranpur": {"latitude": 25.6894, "longitude": 87.7239},
|
| 515 |
+
# "Khagaria": {"latitude": 25.5022, "longitude": 86.4665},
|
| 516 |
+
# "Parbatta": {"latitude": 25.5422, "longitude": 86.5865},
|
| 517 |
+
# "Alauli": {"latitude": 25.4622, "longitude": 86.3465},
|
| 518 |
+
# "Beldaur": {"latitude": 25.5622, "longitude": 86.4265},
|
| 519 |
+
# "Kishanganj": {"latitude": 26.1046, "longitude": 87.9475},
|
| 520 |
+
# "Bahadurganj": {"latitude": 26.2646, "longitude": 88.1175},
|
| 521 |
+
# "Thakurganj": {"latitude": 26.0446, "longitude": 87.8275},
|
| 522 |
+
# "Dighalbank": {"latitude": 25.9046, "longitude": 87.9875},
|
| 523 |
+
# "Lakhisarai": {"latitude": 25.1678, "longitude": 86.0927},
|
| 524 |
+
# "Halsi": {"latitude": 25.2278, "longitude": 86.0327},
|
| 525 |
+
# "Suryagarha": {"latitude": 25.1078, "longitude": 86.1527},
|
| 526 |
+
# "Pipariya": {"latitude": 25.2078, "longitude": 86.1327},
|
| 527 |
+
# "Madhepura": {"latitude": 25.9207, "longitude": 86.7940},
|
| 528 |
+
# "Udakishanganj": {"latitude": 25.9807, "longitude": 86.6740},
|
| 529 |
+
# "Murliganj": {"latitude": 25.8907, "longitude": 86.9940},
|
| 530 |
+
# "Alamnagar": {"latitude": 25.9607, "longitude": 86.7340},
|
| 531 |
+
# "Madhubani": {"latitude": 26.3561, "longitude": 86.0644},
|
| 532 |
+
# "Jhanjharpur": {"latitude": 26.2661, "longitude": 86.2844},
|
| 533 |
+
# "Benipatti": {"latitude": 26.5961, "longitude": 86.1444},
|
| 534 |
+
# "Jainagar": {"latitude": 26.2061, "longitude": 86.1644},
|
| 535 |
+
# "Munger": {"latitude": 25.3753, "longitude": 86.4734},
|
| 536 |
+
# "Jamalpur": {"latitude": 25.3153, "longitude": 86.4934},
|
| 537 |
+
# "Asarganj": {"latitude": 25.1453, "longitude": 86.6834},
|
| 538 |
+
# "Tarapur": {"latitude": 25.0253, "longitude": 86.6334},
|
| 539 |
+
# "Muzaffarpur": {"latitude": 26.1225, "longitude": 85.3906},
|
| 540 |
+
# "Sitamarhi": {"latitude": 26.5925, "longitude": 85.4806},
|
| 541 |
+
# "Minapur": {"latitude": 26.0625, "longitude": 85.2906},
|
| 542 |
+
# "Bochaha": {"latitude": 26.0025, "longitude": 85.5306},
|
| 543 |
+
# "Bihar Sharif": {"latitude": 25.1979, "longitude": 85.5238},
|
| 544 |
+
# "Rajgir": {"latitude": 25.0279, "longitude": 85.4238},
|
| 545 |
+
# "Hilsa": {"latitude": 25.3179, "longitude": 85.2838},
|
| 546 |
+
# "Biharsharif": {"latitude": 25.1979, "longitude": 85.5238},
|
| 547 |
+
# "Nawada": {"latitude": 24.8834, "longitude": 85.5387},
|
| 548 |
+
# "Rajauli": {"latitude": 25.0634, "longitude": 85.6387},
|
| 549 |
+
# "Akbarpur": {"latitude": 24.8234, "longitude": 85.4587},
|
| 550 |
+
# "Hisua": {"latitude": 24.8334, "longitude": 85.4187},
|
| 551 |
+
# "Patna": {"latitude": 25.5941, "longitude": 85.1376},
|
| 552 |
+
# "Danapur": {"latitude": 25.6341, "longitude": 85.0476},
|
| 553 |
+
# "Fatuha": {"latitude": 25.5041, "longitude": 85.3076},
|
| 554 |
+
# "Khagaul": {"latitude": 25.5741, "longitude": 85.0476},
|
| 555 |
+
# "Purnia": {"latitude": 25.7771, "longitude": 87.4753},
|
| 556 |
+
# "Dhamdaha": {"latitude": 25.8871, "longitude": 87.5853},
|
| 557 |
+
# "Kasba": {"latitude": 25.8471, "longitude": 87.5353},
|
| 558 |
+
# "Banmankhi": {"latitude": 25.8871, "longitude": 87.1953},
|
| 559 |
+
# "Sasaram": {"latitude": 24.9520, "longitude": 84.0328},
|
| 560 |
+
# "Dehri": {"latitude": 24.9020, "longitude": 84.1828},
|
| 561 |
+
# "Bikramganj": {"latitude": 25.2120, "longitude": 84.2628},
|
| 562 |
+
# "Nasriganj": {"latitude": 25.0520, "longitude": 84.1228},
|
| 563 |
+
# "Saharsa": {"latitude": 25.8769, "longitude": 86.5956},
|
| 564 |
+
# "Sonbarsa": {"latitude": 25.9269, "longitude": 86.7356},
|
| 565 |
+
# "Simri Bakhtiarpur": {"latitude": 25.9569, "longitude": 86.3556},
|
| 566 |
+
# "Mahishi": {"latitude": 25.9969, "longitude": 86.4756},
|
| 567 |
+
# "Samastipur": {"latitude": 25.8647, "longitude": 85.7817},
|
| 568 |
+
# "Rosera": {"latitude": 25.7947, "longitude": 85.9317},
|
| 569 |
+
# "Dalsinghsarai": {"latitude": 25.6647, "longitude": 85.8317},
|
| 570 |
+
# "Pusa": {"latitude": 25.9847, "longitude": 85.6717},
|
| 571 |
+
# "Chapra": {"latitude": 25.7805, "longitude": 84.7477},
|
| 572 |
+
# "Marhaura": {"latitude": 25.9705, "longitude": 84.8677},
|
| 573 |
+
# "Amnour": {"latitude": 25.8905, "longitude": 84.9077},
|
| 574 |
+
# "Sonepur": {"latitude": 25.6905, "longitude": 85.1777},
|
| 575 |
+
# "Sheikhpura": {"latitude": 25.1391, "longitude": 85.8354},
|
| 576 |
+
# "Barbigha": {"latitude": 25.2191, "longitude": 85.7354},
|
| 577 |
+
# "Ariari": {"latitude": 25.0591, "longitude": 85.9554},
|
| 578 |
+
# "Shekhopur": {"latitude": 25.1391, "longitude": 85.8354},
|
| 579 |
+
# "Sheohar": {"latitude": 26.5184, "longitude": 85.2959},
|
| 580 |
+
# "Dumri Katsari": {"latitude": 26.5784, "longitude": 85.1959},
|
| 581 |
+
# "Piprahi": {"latitude": 26.4684, "longitude": 85.4159},
|
| 582 |
+
# "Tariyani": {"latitude": 26.5584, "longitude": 85.2359},
|
| 583 |
+
# "Sitamarhi": {"latitude": 26.5925, "longitude": 85.4806},
|
| 584 |
+
# "Pupri": {"latitude": 26.4725, "longitude": 85.7006},
|
| 585 |
+
# "Belsand": {"latitude": 26.4425, "longitude": 85.4006},
|
| 586 |
+
# "Bathnaha": {"latitude": 26.5925, "longitude": 85.5306},
|
| 587 |
+
# "Siwan": {"latitude": 26.2195, "longitude": 84.3564},
|
| 588 |
+
# "Maharajganj": {"latitude": 26.1095, "longitude": 84.5064},
|
| 589 |
+
# "Mairwa": {"latitude": 26.2295, "longitude": 84.1664},
|
| 590 |
+
# "Darauli": {"latitude": 26.1595, "longitude": 84.1464},
|
| 591 |
+
# "Supaul": {"latitude": 26.1260, "longitude": 86.6050},
|
| 592 |
+
# "Nirmali": {"latitude": 26.3160, "longitude": 86.5850},
|
| 593 |
+
# "Triveniganj": {"latitude": 26.2160, "longitude": 87.0250},
|
| 594 |
+
# "Chhatapur": {"latitude": 26.2160, "longitude": 86.9050},
|
| 595 |
+
# "Hajipur": {"latitude": 25.6851, "longitude": 85.2095},
|
| 596 |
+
# "Mahua": {"latitude": 25.9651, "longitude": 85.2895},
|
| 597 |
+
# "Lalganj": {"latitude": 25.8751, "longitude": 85.1695},
|
| 598 |
+
# "Desri": {"latitude": 25.6051, "longitude": 85.4895},
|
| 599 |
+
# "Bettiah": {"latitude": 26.8022, "longitude": 84.5025},
|
| 600 |
+
# "Bagaha": {"latitude": 27.0922, "longitude": 84.0925},
|
| 601 |
+
# "Narkatiaganj": {"latitude": 26.4322, "longitude": 84.7925},
|
| 602 |
+
# "Lauriya": {"latitude": 26.9822, "longitude": 84.3125}
|
| 603 |
+
# }
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
# # ============================================================================
|
| 607 |
+
# # FASTAPI APP
|
| 608 |
+
# # ============================================================================
|
| 609 |
+
|
| 610 |
+
# app = FastAPI(title="Farmer.chat Alert Summary API")
|
| 611 |
+
|
| 612 |
+
# # CORS middleware
|
| 613 |
+
# app.add_middleware(
|
| 614 |
+
# CORSMiddleware,
|
| 615 |
+
# allow_origins=["*"],
|
| 616 |
+
# allow_credentials=True,
|
| 617 |
+
# allow_methods=["*"],
|
| 618 |
+
# allow_headers=["*"],
|
| 619 |
+
# )
|
| 620 |
+
|
| 621 |
+
# # Initialize pipeline
|
| 622 |
+
# openai_client = None
|
| 623 |
+
# pipeline = None
|
| 624 |
+
|
| 625 |
+
# @app.on_event("startup")
|
| 626 |
+
# async def startup_event():
|
| 627 |
+
# """Initialize OpenAI client and pipeline on startup"""
|
| 628 |
+
# global openai_client, pipeline
|
| 629 |
+
# try:
|
| 630 |
+
# openai_client = get_openai_client()
|
| 631 |
+
# DEFAULT_LOCATION = {"latitude": 25.5941, "longitude": 85.1376}
|
| 632 |
+
# pipeline = FarmerChatPipeline(openai_client, DEFAULT_LOCATION)
|
| 633 |
+
# print("✓ Pipeline initialized successfully")
|
| 634 |
+
# except Exception as e:
|
| 635 |
+
# print(f"✗ Pipeline initialization failed: {e}")
|
| 636 |
+
# raise
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
# # ============================================================================
|
| 640 |
+
# # API MODELS
|
| 641 |
+
# # ============================================================================
|
| 642 |
+
|
| 643 |
+
# class LocationRequest(BaseModel):
|
| 644 |
+
# location_name: str
|
| 645 |
+
# district: Optional[str] = None
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
# class AlertResponse(BaseModel):
|
| 649 |
+
# location: str
|
| 650 |
+
# coordinates: Dict[str, float]
|
| 651 |
+
# district: Optional[str] = None
|
| 652 |
+
# alert_summary: str
|
| 653 |
+
# timestamp: str
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
# # ============================================================================
|
| 657 |
+
# # API ENDPOINTS
|
| 658 |
+
# # ============================================================================
|
| 659 |
+
|
| 660 |
+
# @app.get("/")
|
| 661 |
+
# async def root():
|
| 662 |
+
# """Root endpoint"""
|
| 663 |
+
# return {
|
| 664 |
+
# "message": "Farmer.chat Alert Summary API",
|
| 665 |
+
# "version": "1.0.0",
|
| 666 |
+
# "endpoints": {
|
| 667 |
+
# "/locations": "GET - List all districts and villages",
|
| 668 |
+
# "/generate-alert": "POST - Generate alert for location",
|
| 669 |
+
# "/health": "GET - Health check"
|
| 670 |
+
# }
|
| 671 |
+
# }
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
# @app.get("/health")
|
| 675 |
+
# async def health_check():
|
| 676 |
+
# """Health check endpoint"""
|
| 677 |
+
# return {
|
| 678 |
+
# "status": "healthy",
|
| 679 |
+
# "pipeline": "initialized" if pipeline else "not_initialized",
|
| 680 |
+
# "timestamp": datetime.now().isoformat()
|
| 681 |
+
# }
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
# @app.get("/locations")
|
| 685 |
+
# async def get_locations():
|
| 686 |
+
# """Get all districts and villages"""
|
| 687 |
+
# return {"districts": BIHAR_DATA}
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
# @app.post("/generate-alert", response_model=AlertResponse)
|
| 691 |
+
# async def generate_alert(request: LocationRequest):
|
| 692 |
+
# """Generate alert summary for selected location"""
|
| 693 |
+
|
| 694 |
+
# if not pipeline:
|
| 695 |
+
# raise HTTPException(status_code=500, detail="Pipeline not initialized")
|
| 696 |
+
|
| 697 |
+
# # Find location coordinates
|
| 698 |
+
# location_name = request.location_name.strip()
|
| 699 |
+
# coordinates = None
|
| 700 |
+
|
| 701 |
+
# # Case-insensitive search
|
| 702 |
+
# for loc_key, loc_coords in LOCATIONS.items():
|
| 703 |
+
# if loc_key.lower() == location_name.lower():
|
| 704 |
+
# coordinates = loc_coords
|
| 705 |
+
# break
|
| 706 |
+
|
| 707 |
+
# if not coordinates:
|
| 708 |
+
# raise HTTPException(
|
| 709 |
+
# status_code=404,
|
| 710 |
+
# detail=f"Location '{location_name}' not found in database"
|
| 711 |
+
# )
|
| 712 |
+
|
| 713 |
+
# # Update pipeline location
|
| 714 |
+
# pipeline.location = coordinates
|
| 715 |
+
|
| 716 |
+
# # Create comprehensive query for all MCP servers
|
| 717 |
+
# query = f"""Generate a comprehensive agricultural alert summary for {location_name} covering:
|
| 718 |
+
# 1. Current weather conditions and 7-day forecast
|
| 719 |
+
# 2. Soil health analysis and fertilizer recommendations
|
| 720 |
+
# 3. Groundwater availability and irrigation status
|
| 721 |
+
# 4. Elevation and topography considerations for farming
|
| 722 |
+
# 5. Current pest risks and preventive measures
|
| 723 |
+
|
| 724 |
+
# Provide actionable insights for farmers."""
|
| 725 |
+
|
| 726 |
+
# try:
|
| 727 |
+
# # Generate alert
|
| 728 |
+
# alert_summary = pipeline.process_query(query)
|
| 729 |
+
|
| 730 |
+
# return AlertResponse(
|
| 731 |
+
# location=location_name,
|
| 732 |
+
# coordinates=coordinates,
|
| 733 |
+
# district=request.district,
|
| 734 |
+
# alert_summary=alert_summary,
|
| 735 |
+
# timestamp=datetime.now().isoformat()
|
| 736 |
+
# )
|
| 737 |
+
|
| 738 |
+
# except Exception as e:
|
| 739 |
+
# raise HTTPException(
|
| 740 |
+
# status_code=500,
|
| 741 |
+
# detail=f"Error generating alert: {str(e)}"
|
| 742 |
+
# )
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
# # ============================================================================
|
| 746 |
+
# # RUN SERVER
|
| 747 |
+
# # ============================================================================
|
| 748 |
+
|
| 749 |
+
# if __name__ == "__main__":
|
| 750 |
+
# import uvicorn
|
| 751 |
+
# port = int(os.getenv("PORT", 7860))
|
| 752 |
+
# uvicorn.run(app, host="0.0.0.0", port=port)
|
| 753 |
+
|
| 754 |
+
|
| 755 |
+
|
| 756 |
+
"""
|
| 757 |
+
Farmer.chat Alert Summary Generator - FastAPI Application
|
| 758 |
+
Modular architecture with alert-focused intelligence
|
| 759 |
+
"""
|
| 760 |
+
|
| 761 |
+
from fastapi import FastAPI, HTTPException
|
| 762 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 763 |
+
from pydantic import BaseModel
|
| 764 |
+
from typing import Optional, Dict
|
| 765 |
+
from datetime import datetime
|
| 766 |
+
import os
|
| 767 |
+
|
| 768 |
+
# Import pipeline components
|
| 769 |
+
from src.pipeline import FarmerChatPipeline
|
| 770 |
+
|
| 771 |
+
# Import MCP servers (assuming they exist in src/servers/)
|
| 772 |
+
from src.servers.weather import WeatherServer
|
| 773 |
+
from src.servers.soil import SoilPropertiesServer
|
| 774 |
+
from src.servers.water import WaterServer
|
| 775 |
+
from src.servers.elevation import ElevationServer
|
| 776 |
+
from src.servers.pests import PestsServer
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
# ============================================================================
|
| 780 |
+
# PYDANTIC MODELS
|
| 781 |
+
# ============================================================================
|
| 782 |
+
|
| 783 |
+
class LocationRequest(BaseModel):
|
| 784 |
+
"""Request model for alert generation"""
|
| 785 |
+
location_name: str
|
| 786 |
+
district: Optional[str] = None
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
class AlertResponse(BaseModel):
|
| 790 |
+
"""Response model for generated alerts"""
|
| 791 |
+
location: str
|
| 792 |
+
coordinates: Dict[str, float]
|
| 793 |
+
district: Optional[str]
|
| 794 |
+
alert_summary: str
|
| 795 |
+
timestamp: str
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
class QueryRequest(BaseModel):
|
| 799 |
+
"""Request model for specific queries"""
|
| 800 |
+
query: str
|
| 801 |
+
location_name: Optional[str] = None
|
| 802 |
+
district: Optional[str] = None
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
# ============================================================================
|
| 806 |
+
# LOCATION DATA
|
| 807 |
+
# ============================================================================
|
| 808 |
+
|
| 809 |
+
BIHAR_DATA = {
|
| 810 |
+
"Araria": ["Araria", "Forbesganj", "Jokihat", "Raniganj"],
|
| 811 |
+
"Arwal": ["Arwal", "Kaler", "Karpi", "Kurtha"],
|
| 812 |
+
"Aurangabad": ["Aurangabad", "Daudnagar", "Obra", "Nabinagar"],
|
| 813 |
+
"Banka": ["Banka", "Amarpur", "Barahat", "Belhar"],
|
| 814 |
+
"Begusarai": ["Begusarai", "Bakhri", "Barauni", "Teghra"],
|
| 815 |
+
"Bhagalpur": ["Bhagalpur", "Sabour", "Nathnagar", "Kahalgaon"],
|
| 816 |
+
"Bhojpur": ["Arrah", "Jagdishpur", "Piro", "Shahpur"],
|
| 817 |
+
"Buxar": ["Buxar", "Dumraon", "Chausa", "Simri"],
|
| 818 |
+
"Darbhanga": ["Darbhanga", "Baheri", "Jale", "Benipur"],
|
| 819 |
+
"East Champaran": ["Motihari", "Raxaul", "Chakia", "Dhaka"],
|
| 820 |
+
"Gaya": ["Gaya", "Bodh Gaya", "Tekari", "Sherghati"],
|
| 821 |
+
"Gopalganj": ["Gopalganj", "Barauli", "Baikunthpur", "Kateya"],
|
| 822 |
+
"Jamui": ["Jamui", "Jhajha", "Sikandra", "Sono"],
|
| 823 |
+
"Jehanabad": ["Jehanabad", "Ghoshi", "Makhdumpur", "Modanganj"],
|
| 824 |
+
"Kaimur": ["Bhabua", "Mohania", "Ramgarh", "Chainpur"],
|
| 825 |
+
"Katihar": ["Katihar", "Barsoi", "Manihari", "Pranpur"],
|
| 826 |
+
"Khagaria": ["Khagaria", "Parbatta", "Alauli", "Beldaur"],
|
| 827 |
+
"Kishanganj": ["Kishanganj", "Bahadurganj", "Thakurganj", "Dighalbank"],
|
| 828 |
+
"Lakhisarai": ["Lakhisarai", "Halsi", "Suryagarha", "Pipariya"],
|
| 829 |
+
"Madhepura": ["Madhepura", "Udakishanganj", "Murliganj", "Alamnagar"],
|
| 830 |
+
"Madhubani": ["Madhubani", "Jhanjharpur", "Benipatti", "Jainagar"],
|
| 831 |
+
"Munger": ["Munger", "Jamalpur", "Asarganj", "Tarapur"],
|
| 832 |
+
"Muzaffarpur": ["Muzaffarpur", "Sitamarhi", "Minapur", "Bochaha"],
|
| 833 |
+
"Nalanda": ["Bihar Sharif", "Rajgir", "Hilsa", "Biharsharif"],
|
| 834 |
+
"Nawada": ["Nawada", "Rajauli", "Akbarpur", "Hisua"],
|
| 835 |
+
"Patna": ["Patna", "Danapur", "Fatuha", "Khagaul"],
|
| 836 |
+
"Purnia": ["Purnia", "Dhamdaha", "Kasba", "Banmankhi"],
|
| 837 |
+
"Rohtas": ["Sasaram", "Dehri", "Bikramganj", "Nasriganj"],
|
| 838 |
+
"Saharsa": ["Saharsa", "Sonbarsa", "Simri Bakhtiarpur", "Mahishi"],
|
| 839 |
+
"Samastipur": ["Samastipur", "Rosera", "Dalsinghsarai", "Pusa"],
|
| 840 |
+
"Saran": ["Chapra", "Marhaura", "Amnour", "Sonepur"],
|
| 841 |
+
"Sheikhpura": ["Sheikhpura", "Barbigha", "Ariari", "Shekhopur"],
|
| 842 |
+
"Sheohar": ["Sheohar", "Dumri Katsari", "Piprahi", "Tariyani"],
|
| 843 |
+
"Sitamarhi": ["Sitamarhi", "Pupri", "Belsand", "Bathnaha"],
|
| 844 |
+
"Siwan": ["Siwan", "Maharajganj", "Mairwa", "Darauli"],
|
| 845 |
+
"Supaul": ["Supaul", "Nirmali", "Triveniganj", "Chhatapur"],
|
| 846 |
+
"Vaishali": ["Hajipur", "Mahua", "Lalganj", "Desri"],
|
| 847 |
+
"West Champaran": ["Bettiah", "Bagaha", "Narkatiaganj", "Lauriya"]
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
LOCATIONS = {
|
| 851 |
+
"Araria": {"latitude": 26.1523, "longitude": 87.5167},
|
| 852 |
+
"Forbesganj": {"latitude": 26.3023, "longitude": 87.2664},
|
| 853 |
+
"Jokihat": {"latitude": 25.8998, "longitude": 87.2686},
|
| 854 |
+
"Raniganj": {"latitude": 26.0537, "longitude": 87.5333},
|
| 855 |
+
"Arwal": {"latitude": 25.2560, "longitude": 84.6819},
|
| 856 |
+
"Kaler": {"latitude": 25.1960, "longitude": 84.6219},
|
| 857 |
+
"Karpi": {"latitude": 25.2360, "longitude": 84.7019},
|
| 858 |
+
"Kurtha": {"latitude": 25.3160, "longitude": 84.6619},
|
| 859 |
+
"Aurangabad": {"latitude": 24.7521, "longitude": 84.3742},
|
| 860 |
+
"Daudnagar": {"latitude": 25.0337, "longitude": 84.4007},
|
| 861 |
+
"Obra": {"latitude": 24.9923, "longitude": 84.4342},
|
| 862 |
+
"Nabinagar": {"latitude": 24.6087, "longitude": 84.1269},
|
| 863 |
+
"Banka": {"latitude": 24.8893, "longitude": 86.9220},
|
| 864 |
+
"Amarpur": {"latitude": 25.0393, "longitude": 86.9020},
|
| 865 |
+
"Barahat": {"latitude": 24.8393, "longitude": 87.0020},
|
| 866 |
+
"Belhar": {"latitude": 24.9393, "longitude": 86.9620},
|
| 867 |
+
"Begusarai": {"latitude": 25.4182, "longitude": 86.1347},
|
| 868 |
+
"Bakhri": {"latitude": 25.4582, "longitude": 86.0547},
|
| 869 |
+
"Barauni": {"latitude": 25.4751, "longitude": 86.0458},
|
| 870 |
+
"Teghra": {"latitude": 25.5082, "longitude": 85.9347},
|
| 871 |
+
"Bhagalpur": {"latitude": 25.2425, "longitude": 86.9842},
|
| 872 |
+
"Sabour": {"latitude": 25.2375, "longitude": 87.0542},
|
| 873 |
+
"Nathnagar": {"latitude": 25.1225, "longitude": 87.0042},
|
| 874 |
+
"Kahalgaon": {"latitude": 25.1925, "longitude": 87.2142},
|
| 875 |
+
"Arrah": {"latitude": 25.5560, "longitude": 84.6631},
|
| 876 |
+
"Jagdishpur": {"latitude": 25.4660, "longitude": 84.4231},
|
| 877 |
+
"Piro": {"latitude": 25.3260, "longitude": 84.4031},
|
| 878 |
+
"Shahpur": {"latitude": 25.6060, "longitude": 84.4031},
|
| 879 |
+
"Buxar": {"latitude": 25.5641, "longitude": 83.9778},
|
| 880 |
+
"Dumraon": {"latitude": 25.5541, "longitude": 84.1478},
|
| 881 |
+
"Chausa": {"latitude": 25.5241, "longitude": 83.9178},
|
| 882 |
+
"Simri": {"latitude": 25.6141, "longitude": 84.0478},
|
| 883 |
+
"Darbhanga": {"latitude": 26.1542, "longitude": 85.8978},
|
| 884 |
+
"Baheri": {"latitude": 26.0442, "longitude": 85.8378},
|
| 885 |
+
"Jale": {"latitude": 26.2042, "longitude": 85.8578},
|
| 886 |
+
"Benipur": {"latitude": 26.1142, "longitude": 85.9478},
|
| 887 |
+
"Motihari": {"latitude": 26.6484, "longitude": 84.9194},
|
| 888 |
+
"Raxaul": {"latitude": 26.9784, "longitude": 84.8494},
|
| 889 |
+
"Chakia": {"latitude": 26.4184, "longitude": 85.0494},
|
| 890 |
+
"Dhaka": {"latitude": 26.6784, "longitude": 85.1694},
|
| 891 |
+
"Gaya": {"latitude": 24.7955, "longitude": 85.0002},
|
| 892 |
+
"Bodh Gaya": {"latitude": 24.6955, "longitude": 84.9902},
|
| 893 |
+
"Tekari": {"latitude": 24.9455, "longitude": 85.0402},
|
| 894 |
+
"Sherghati": {"latitude": 24.5655, "longitude": 84.7902},
|
| 895 |
+
"Gopalganj": {"latitude": 26.4685, "longitude": 84.4388},
|
| 896 |
+
"Barauli": {"latitude": 26.3785, "longitude": 84.5788},
|
| 897 |
+
"Baikunthpur": {"latitude": 26.5285, "longitude": 84.3588},
|
| 898 |
+
"Kateya": {"latitude": 26.4285, "longitude": 84.6388},
|
| 899 |
+
"Jamui": {"latitude": 24.9272, "longitude": 86.2231},
|
| 900 |
+
"Jhajha": {"latitude": 24.7772, "longitude": 86.3731},
|
| 901 |
+
"Sikandra": {"latitude": 24.9672, "longitude": 86.0631},
|
| 902 |
+
"Sono": {"latitude": 24.8372, "longitude": 86.1431},
|
| 903 |
+
"Jehanabad": {"latitude": 25.2078, "longitude": 84.9869},
|
| 904 |
+
"Ghoshi": {"latitude": 25.1478, "longitude": 84.9269},
|
| 905 |
+
"Makhdumpur": {"latitude": 25.2478, "longitude": 85.0469},
|
| 906 |
+
"Modanganj": {"latitude": 25.2678, "longitude": 84.9069},
|
| 907 |
+
"Bhabua": {"latitude": 25.0405, "longitude": 83.6074},
|
| 908 |
+
"Mohania": {"latitude": 25.1305, "longitude": 83.4774},
|
| 909 |
+
"Ramgarh": {"latitude": 24.9505, "longitude": 83.6874},
|
| 910 |
+
"Chainpur": {"latitude": 25.2005, "longitude": 83.7474},
|
| 911 |
+
"Katihar": {"latitude": 25.5394, "longitude": 87.5839},
|
| 912 |
+
"Barsoi": {"latitude": 25.3794, "longitude": 87.8839},
|
| 913 |
+
"Manihari": {"latitude": 25.3394, "longitude": 87.6239},
|
| 914 |
+
"Pranpur": {"latitude": 25.6894, "longitude": 87.7239},
|
| 915 |
+
"Khagaria": {"latitude": 25.5022, "longitude": 86.4665},
|
| 916 |
+
"Parbatta": {"latitude": 25.5422, "longitude": 86.5865},
|
| 917 |
+
"Alauli": {"latitude": 25.4622, "longitude": 86.3465},
|
| 918 |
+
"Beldaur": {"latitude": 25.5622, "longitude": 86.4265},
|
| 919 |
+
"Kishanganj": {"latitude": 26.1046, "longitude": 87.9475},
|
| 920 |
+
"Bahadurganj": {"latitude": 26.2646, "longitude": 88.1175},
|
| 921 |
+
"Thakurganj": {"latitude": 26.0446, "longitude": 87.8275},
|
| 922 |
+
"Dighalbank": {"latitude": 25.9046, "longitude": 87.9875},
|
| 923 |
+
"Lakhisarai": {"latitude": 25.1678, "longitude": 86.0927},
|
| 924 |
+
"Halsi": {"latitude": 25.2278, "longitude": 86.0327},
|
| 925 |
+
"Suryagarha": {"latitude": 25.1078, "longitude": 86.1527},
|
| 926 |
+
"Pipariya": {"latitude": 25.2078, "longitude": 86.1327},
|
| 927 |
+
"Madhepura": {"latitude": 25.9207, "longitude": 86.7940},
|
| 928 |
+
"Udakishanganj": {"latitude": 25.9807, "longitude": 86.6740},
|
| 929 |
+
"Murliganj": {"latitude": 25.8907, "longitude": 86.9940},
|
| 930 |
+
"Alamnagar": {"latitude": 25.9607, "longitude": 86.7340},
|
| 931 |
+
"Madhubani": {"latitude": 26.3561, "longitude": 86.0644},
|
| 932 |
+
"Jhanjharpur": {"latitude": 26.2661, "longitude": 86.2844},
|
| 933 |
+
"Benipatti": {"latitude": 26.5961, "longitude": 86.1444},
|
| 934 |
+
"Jainagar": {"latitude": 26.2061, "longitude": 86.1644},
|
| 935 |
+
"Munger": {"latitude": 25.3753, "longitude": 86.4734},
|
| 936 |
+
"Jamalpur": {"latitude": 25.3153, "longitude": 86.4934},
|
| 937 |
+
"Asarganj": {"latitude": 25.1453, "longitude": 86.6834},
|
| 938 |
+
"Tarapur": {"latitude": 25.0253, "longitude": 86.6334},
|
| 939 |
+
"Muzaffarpur": {"latitude": 26.1225, "longitude": 85.3906},
|
| 940 |
+
"Sitamarhi": {"latitude": 26.5925, "longitude": 85.4806},
|
| 941 |
+
"Minapur": {"latitude": 26.0625, "longitude": 85.2906},
|
| 942 |
+
"Bochaha": {"latitude": 26.0025, "longitude": 85.5306},
|
| 943 |
+
"Bihar Sharif": {"latitude": 25.1979, "longitude": 85.5238},
|
| 944 |
+
"Rajgir": {"latitude": 25.0279, "longitude": 85.4238},
|
| 945 |
+
"Hilsa": {"latitude": 25.3179, "longitude": 85.2838},
|
| 946 |
+
"Biharsharif": {"latitude": 25.1979, "longitude": 85.5238},
|
| 947 |
+
"Nawada": {"latitude": 24.8834, "longitude": 85.5387},
|
| 948 |
+
"Rajauli": {"latitude": 25.0634, "longitude": 85.6387},
|
| 949 |
+
"Akbarpur": {"latitude": 24.8234, "longitude": 85.4587},
|
| 950 |
+
"Hisua": {"latitude": 24.8334, "longitude": 85.4187},
|
| 951 |
+
"Patna": {"latitude": 25.5941, "longitude": 85.1376},
|
| 952 |
+
"Danapur": {"latitude": 25.6341, "longitude": 85.0476},
|
| 953 |
+
"Fatuha": {"latitude": 25.5041, "longitude": 85.3076},
|
| 954 |
+
"Khagaul": {"latitude": 25.5741, "longitude": 85.0476},
|
| 955 |
+
"Purnia": {"latitude": 25.7771, "longitude": 87.4753},
|
| 956 |
+
"Dhamdaha": {"latitude": 25.8871, "longitude": 87.5853},
|
| 957 |
+
"Kasba": {"latitude": 25.8471, "longitude": 87.5353},
|
| 958 |
+
"Banmankhi": {"latitude": 25.8871, "longitude": 87.1953},
|
| 959 |
+
"Sasaram": {"latitude": 24.9520, "longitude": 84.0328},
|
| 960 |
+
"Dehri": {"latitude": 24.9020, "longitude": 84.1828},
|
| 961 |
+
"Bikramganj": {"latitude": 25.2120, "longitude": 84.2628},
|
| 962 |
+
"Nasriganj": {"latitude": 25.0520, "longitude": 84.1228},
|
| 963 |
+
"Saharsa": {"latitude": 25.8769, "longitude": 86.5956},
|
| 964 |
+
"Sonbarsa": {"latitude": 25.9269, "longitude": 86.7356},
|
| 965 |
+
"Simri Bakhtiarpur": {"latitude": 25.9569, "longitude": 86.3556},
|
| 966 |
+
"Mahishi": {"latitude": 25.9969, "longitude": 86.4756},
|
| 967 |
+
"Samastipur": {"latitude": 25.8647, "longitude": 85.7817},
|
| 968 |
+
"Rosera": {"latitude": 25.7947, "longitude": 85.9317},
|
| 969 |
+
"Dalsinghsarai": {"latitude": 25.6647, "longitude": 85.8317},
|
| 970 |
+
"Pusa": {"latitude": 25.9847, "longitude": 85.6717},
|
| 971 |
+
"Chapra": {"latitude": 25.7805, "longitude": 84.7477},
|
| 972 |
+
"Marhaura": {"latitude": 25.9705, "longitude": 84.8677},
|
| 973 |
+
"Amnour": {"latitude": 25.8905, "longitude": 84.9077},
|
| 974 |
+
"Sonepur": {"latitude": 25.6905, "longitude": 85.1777},
|
| 975 |
+
"Sheikhpura": {"latitude": 25.1391, "longitude": 85.8354},
|
| 976 |
+
"Barbigha": {"latitude": 25.2191, "longitude": 85.7354},
|
| 977 |
+
"Ariari": {"latitude": 25.0591, "longitude": 85.9554},
|
| 978 |
+
"Shekhopur": {"latitude": 25.1391, "longitude": 85.8354},
|
| 979 |
+
"Sheohar": {"latitude": 26.5184, "longitude": 85.2959},
|
| 980 |
+
"Dumri Katsari": {"latitude": 26.5784, "longitude": 85.1959},
|
| 981 |
+
"Piprahi": {"latitude": 26.4684, "longitude": 85.4159},
|
| 982 |
+
"Tariyani": {"latitude": 26.5584, "longitude": 85.2359},
|
| 983 |
+
"Sitamarhi": {"latitude": 26.5925, "longitude": 85.4806},
|
| 984 |
+
"Pupri": {"latitude": 26.4725, "longitude": 85.7006},
|
| 985 |
+
"Belsand": {"latitude": 26.4425, "longitude": 85.4006},
|
| 986 |
+
"Bathnaha": {"latitude": 26.5925, "longitude": 85.5306},
|
| 987 |
+
"Siwan": {"latitude": 26.2195, "longitude": 84.3564},
|
| 988 |
+
"Maharajganj": {"latitude": 26.1095, "longitude": 84.5064},
|
| 989 |
+
"Mairwa": {"latitude": 26.2295, "longitude": 84.1664},
|
| 990 |
+
"Darauli": {"latitude": 26.1595, "longitude": 84.1464},
|
| 991 |
+
"Supaul": {"latitude": 26.1260, "longitude": 86.6050},
|
| 992 |
+
"Nirmali": {"latitude": 26.3160, "longitude": 86.5850},
|
| 993 |
+
"Triveniganj": {"latitude": 26.2160, "longitude": 87.0250},
|
| 994 |
+
"Chhatapur": {"latitude": 26.2160, "longitude": 86.9050},
|
| 995 |
+
"Hajipur": {"latitude": 25.6851, "longitude": 85.2095},
|
| 996 |
+
"Mahua": {"latitude": 25.9651, "longitude": 85.2895},
|
| 997 |
+
"Lalganj": {"latitude": 25.8751, "longitude": 85.1695},
|
| 998 |
+
"Desri": {"latitude": 25.6051, "longitude": 85.4895},
|
| 999 |
+
"Bettiah": {"latitude": 26.8022, "longitude": 84.5025},
|
| 1000 |
+
"Bagaha": {"latitude": 27.0922, "longitude": 84.0925},
|
| 1001 |
+
"Narkatiaganj": {"latitude": 26.4322, "longitude": 84.7925},
|
| 1002 |
+
"Lauriya": {"latitude": 26.9822, "longitude": 84.3125}
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
|
| 1006 |
+
# ============================================================================
|
| 1007 |
+
# FASTAPI APPLICATION
|
| 1008 |
+
# ============================================================================
|
| 1009 |
+
|
| 1010 |
+
app = FastAPI(
|
| 1011 |
+
title="Farmer.chat Alert Summary API",
|
| 1012 |
+
description="Agricultural intelligence system with alert-focused MCP pipeline",
|
| 1013 |
+
version="2.0.0"
|
| 1014 |
+
)
|
| 1015 |
+
|
| 1016 |
+
# CORS configuration
|
| 1017 |
+
app.add_middleware(
|
| 1018 |
+
CORSMiddleware,
|
| 1019 |
+
allow_origins=["*"],
|
| 1020 |
+
allow_credentials=True,
|
| 1021 |
+
allow_methods=["*"],
|
| 1022 |
+
allow_headers=["*"],
|
| 1023 |
+
)
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
+
# ============================================================================
|
| 1027 |
+
# GLOBAL STATE
|
| 1028 |
+
# ============================================================================
|
| 1029 |
+
|
| 1030 |
+
pipeline: Optional[FarmerChatPipeline] = None
|
| 1031 |
+
servers: Dict = {}
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
# ============================================================================
|
| 1035 |
+
# STARTUP & SHUTDOWN
|
| 1036 |
+
# ============================================================================
|
| 1037 |
+
|
| 1038 |
+
@app.on_event("startup")
|
| 1039 |
+
async def startup_event():
|
| 1040 |
+
"""Initialize MCP servers and pipeline on startup"""
|
| 1041 |
+
global pipeline, servers
|
| 1042 |
+
|
| 1043 |
+
try:
|
| 1044 |
+
print("\n" + "="*60)
|
| 1045 |
+
print("Initializing Farmer.chat Alert System")
|
| 1046 |
+
print("="*60 + "\n")
|
| 1047 |
+
|
| 1048 |
+
# Initialize MCP servers
|
| 1049 |
+
print("Initializing MCP servers...")
|
| 1050 |
+
|
| 1051 |
+
# Note: Your servers don't need arguments in their constructors
|
| 1052 |
+
# WaterServer hardcodes cache_dir internally
|
| 1053 |
+
servers = {
|
| 1054 |
+
"weather": WeatherServer(),
|
| 1055 |
+
"soil": SoilPropertiesServer(),
|
| 1056 |
+
"water": WaterServer(),
|
| 1057 |
+
"elevation": ElevationServer(),
|
| 1058 |
+
"pests": PestsServer()
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
print("✓ All MCP servers initialized successfully\n")
|
| 1062 |
+
|
| 1063 |
+
# Initialize pipeline with default location (Patna)
|
| 1064 |
+
default_location = LOCATIONS.get("patna", {"latitude": 25.6093, "longitude": 85.1235})
|
| 1065 |
+
|
| 1066 |
+
pipeline = FarmerChatPipeline(
|
| 1067 |
+
servers=servers,
|
| 1068 |
+
location=default_location
|
| 1069 |
+
)
|
| 1070 |
+
|
| 1071 |
+
print("\n" + "="*60)
|
| 1072 |
+
print("✓ Farmer.chat Alert System Ready")
|
| 1073 |
+
print("="*60 + "\n")
|
| 1074 |
+
|
| 1075 |
+
except Exception as e:
|
| 1076 |
+
print(f"✗ Pipeline initialization failed: {str(e)}")
|
| 1077 |
+
raise
|
| 1078 |
+
|
| 1079 |
+
|
| 1080 |
+
@app.on_event("shutdown")
|
| 1081 |
+
async def shutdown_event():
|
| 1082 |
+
"""Cleanup on shutdown"""
|
| 1083 |
+
print("\nShutting down Farmer.chat Alert System...")
|
| 1084 |
+
|
| 1085 |
+
|
| 1086 |
+
# ============================================================================
|
| 1087 |
+
# API ENDPOINTS
|
| 1088 |
+
# ============================================================================
|
| 1089 |
+
|
| 1090 |
+
@app.get("/")
|
| 1091 |
+
async def root():
|
| 1092 |
+
"""Root endpoint with API information"""
|
| 1093 |
+
return {
|
| 1094 |
+
"service": "Farmer.chat Alert Summary Generator",
|
| 1095 |
+
"version": "2.0.0",
|
| 1096 |
+
"status": "operational",
|
| 1097 |
+
"architecture": "Modular MCP Pipeline",
|
| 1098 |
+
"endpoints": {
|
| 1099 |
+
"locations": "/locations - Get all available locations",
|
| 1100 |
+
"generate_alert": "/generate-alert - Generate alert summary for location",
|
| 1101 |
+
"query": "/query - Process specific farmer query",
|
| 1102 |
+
"health": "/health - Health check",
|
| 1103 |
+
"server_status": "/server-status - Check MCP server status"
|
| 1104 |
+
}
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
|
| 1108 |
+
@app.get("/health")
|
| 1109 |
+
async def health_check():
|
| 1110 |
+
"""Health check endpoint"""
|
| 1111 |
+
if pipeline is None:
|
| 1112 |
+
raise HTTPException(status_code=503, detail="Pipeline not initialized")
|
| 1113 |
+
|
| 1114 |
+
return {
|
| 1115 |
+
"status": "healthy",
|
| 1116 |
+
"pipeline": "operational",
|
| 1117 |
+
"servers": len(servers),
|
| 1118 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
|
| 1122 |
+
@app.get("/server-status")
|
| 1123 |
+
async def server_status():
|
| 1124 |
+
"""Get status of all MCP servers"""
|
| 1125 |
+
if pipeline is None:
|
| 1126 |
+
raise HTTPException(status_code=503, detail="Pipeline not initialized")
|
| 1127 |
+
|
| 1128 |
+
status = pipeline.get_server_status()
|
| 1129 |
+
return {
|
| 1130 |
+
"servers": status,
|
| 1131 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
|
| 1135 |
+
@app.get("/locations")
|
| 1136 |
+
async def get_locations():
|
| 1137 |
+
"""Get all available districts and villages"""
|
| 1138 |
+
return {
|
| 1139 |
+
"districts": BIHAR_DATA,
|
| 1140 |
+
"total_locations": len(LOCATIONS),
|
| 1141 |
+
"coverage": "Bihar, India"
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
|
| 1145 |
+
@app.post("/generate-alert", response_model=AlertResponse)
|
| 1146 |
+
async def generate_alert(request: LocationRequest):
|
| 1147 |
+
"""
|
| 1148 |
+
Generate comprehensive alert summary for a location.
|
| 1149 |
+
|
| 1150 |
+
Queries all MCP servers and compiles an actionable alert summary
|
| 1151 |
+
focusing on critical information and recommendations.
|
| 1152 |
+
"""
|
| 1153 |
+
|
| 1154 |
+
if pipeline is None:
|
| 1155 |
+
raise HTTPException(status_code=503, detail="Pipeline not initialized")
|
| 1156 |
+
|
| 1157 |
+
# Lookup location coordinates
|
| 1158 |
+
location_key = request.location_name.lower().strip()
|
| 1159 |
+
|
| 1160 |
+
if location_key not in LOCATIONS:
|
| 1161 |
+
# Try case-insensitive partial match
|
| 1162 |
+
matches = [k for k in LOCATIONS.keys() if location_key in k.lower()]
|
| 1163 |
+
if matches:
|
| 1164 |
+
location_key = matches[0]
|
| 1165 |
+
else:
|
| 1166 |
+
raise HTTPException(
|
| 1167 |
+
status_code=404,
|
| 1168 |
+
detail=f"Location '{request.location_name}' not found in database"
|
| 1169 |
+
)
|
| 1170 |
+
|
| 1171 |
+
coordinates = LOCATIONS[location_key]
|
| 1172 |
+
|
| 1173 |
+
try:
|
| 1174 |
+
# Generate alert using pipeline
|
| 1175 |
+
result = pipeline.generate_alert(
|
| 1176 |
+
location=coordinates,
|
| 1177 |
+
location_name=request.location_name
|
| 1178 |
+
)
|
| 1179 |
+
|
| 1180 |
+
return AlertResponse(
|
| 1181 |
+
location=request.location_name,
|
| 1182 |
+
coordinates=coordinates,
|
| 1183 |
+
district=request.district,
|
| 1184 |
+
alert_summary=result["alert_summary"],
|
| 1185 |
+
timestamp=datetime.utcnow().isoformat()
|
| 1186 |
+
)
|
| 1187 |
+
|
| 1188 |
+
except Exception as e:
|
| 1189 |
+
raise HTTPException(
|
| 1190 |
+
status_code=500,
|
| 1191 |
+
detail=f"Failed to generate alert: {str(e)}"
|
| 1192 |
+
)
|
| 1193 |
+
|
| 1194 |
+
|
| 1195 |
+
@app.post("/query")
|
| 1196 |
+
async def process_query(request: QueryRequest):
|
| 1197 |
+
"""
|
| 1198 |
+
Process a specific farmer query.
|
| 1199 |
+
|
| 1200 |
+
Queries all MCP servers and returns a response focusing on
|
| 1201 |
+
information relevant to the question.
|
| 1202 |
+
"""
|
| 1203 |
+
|
| 1204 |
+
if pipeline is None:
|
| 1205 |
+
raise HTTPException(status_code=503, detail="Pipeline not initialized")
|
| 1206 |
+
|
| 1207 |
+
# Determine location
|
| 1208 |
+
location = None
|
| 1209 |
+
if request.location_name:
|
| 1210 |
+
location_key = request.location_name.lower().strip()
|
| 1211 |
+
if location_key in LOCATIONS:
|
| 1212 |
+
location = LOCATIONS[location_key]
|
| 1213 |
+
else:
|
| 1214 |
+
matches = [k for k in LOCATIONS.keys() if location_key in k.lower()]
|
| 1215 |
+
if matches:
|
| 1216 |
+
location = LOCATIONS[matches[0]]
|
| 1217 |
+
|
| 1218 |
+
try:
|
| 1219 |
+
# Process query through pipeline
|
| 1220 |
+
result = pipeline.process_query(
|
| 1221 |
+
query=request.query,
|
| 1222 |
+
location=location
|
| 1223 |
+
)
|
| 1224 |
+
|
| 1225 |
+
return {
|
| 1226 |
+
"query": request.query,
|
| 1227 |
+
"response": result["response"],
|
| 1228 |
+
"location": result["location"],
|
| 1229 |
+
"servers_queried": list(result["mcp_results"].keys()),
|
| 1230 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
except Exception as e:
|
| 1234 |
+
raise HTTPException(
|
| 1235 |
+
status_code=500,
|
| 1236 |
+
detail=f"Failed to process query: {str(e)}"
|
| 1237 |
+
)
|
| 1238 |
+
|
| 1239 |
+
|
| 1240 |
+
# ============================================================================
|
| 1241 |
+
# MAIN (for local testing)
|
| 1242 |
+
# ============================================================================
|
| 1243 |
+
|
| 1244 |
+
if __name__ == "__main__":
|
| 1245 |
+
import uvicorn
|
| 1246 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 1247 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Farmer.Chat Backend Dependencies]
|
| 2 |
+
|
| 3 |
+
# # Web Framework
|
| 4 |
+
# fastapi==0.109.0
|
| 5 |
+
# uvicorn[standard]==0.27.0
|
| 6 |
+
# python-multipart==0.0.6
|
| 7 |
+
|
| 8 |
+
# openai==1.3.0
|
| 9 |
+
# httpx==0.24.1
|
| 10 |
+
# httpcore==0.17.3
|
| 11 |
+
|
| 12 |
+
# # Async HTTP
|
| 13 |
+
# aiohttp==3.9.1
|
| 14 |
+
# aiofiles==23.2.1
|
| 15 |
+
|
| 16 |
+
# # Data Processing
|
| 17 |
+
# pandas==2.1.4
|
| 18 |
+
# numpy==1.26.3
|
| 19 |
+
# xarray==2023.12.0
|
| 20 |
+
# netCDF4==1.6.5
|
| 21 |
+
|
| 22 |
+
# # PDF Generation
|
| 23 |
+
# reportlab==4.0.9
|
| 24 |
+
# pillow==10.2.0
|
| 25 |
+
|
| 26 |
+
# # Utilities
|
| 27 |
+
# python-dotenv==1.0.0
|
| 28 |
+
# pydantic==2.5.3
|
| 29 |
+
# pydantic-settings==2.1.0
|
| 30 |
+
|
| 31 |
+
# # Requests - updated for compatibility
|
| 32 |
+
# requests==2.32.3
|
| 33 |
+
|
| 34 |
+
# earthengine-api==0.1.384
|
| 35 |
+
# google-auth==2.27.0
|
| 36 |
+
# google-auth-httplib2==0.2.0
|
| 37 |
+
|
| 38 |
+
fastapi==0.104.1
|
| 39 |
+
uvicorn==0.24.0
|
| 40 |
+
pydantic==2.5.0
|
| 41 |
+
openai==1.3.0
|
| 42 |
+
httpx==0.24.1
|
| 43 |
+
python-multipart==0.0.6
|
| 44 |
+
|
| 45 |
+
# MCP Server dependencies
|
| 46 |
+
requests==2.31.0
|
| 47 |
+
numpy==1.24.3
|
| 48 |
+
xarray==2023.1.0
|
| 49 |
+
netCDF4==1.6.4
|
| 50 |
+
earthengine-api==0.1.374
|
| 51 |
+
scipy==1.10.1
|
| 52 |
+
rasterio==1.3.9
|
| 53 |
+
h5netcdf==1.2.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# """
|
| 2 |
+
# Farmer.Chat Backend - MCP Pipeline Package
|
| 3 |
+
# """
|
| 4 |
+
|
| 5 |
+
# # Import main components
|
| 6 |
+
# from .pipeline import FarmerChatPipeline
|
| 7 |
+
# from .router import QueryRouter
|
| 8 |
+
# from .executor import MCPExecutor, MCP_SERVER_REGISTRY
|
| 9 |
+
# from .compiler import ResponseCompiler
|
| 10 |
+
# from .translator import FarmerTranslator
|
| 11 |
+
# from .pdf_generator import generate_pdf_report
|
| 12 |
+
|
| 13 |
+
# __all__ = [
|
| 14 |
+
# 'FarmerChatPipeline',
|
| 15 |
+
# 'QueryRouter',
|
| 16 |
+
# 'MCPExecutor',
|
| 17 |
+
# 'MCP_SERVER_REGISTRY',
|
| 18 |
+
# 'ResponseCompiler',
|
| 19 |
+
# 'FarmerTranslator',
|
| 20 |
+
# 'generate_pdf_report'
|
| 21 |
+
# ]
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
Farmer.chat Alert System - Modular Components
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from .pipeline import FarmerChatPipeline
|
| 28 |
+
from .router import QueryRouter
|
| 29 |
+
from .executor import MCPExecutor
|
| 30 |
+
from .compiler import ResponseCompiler
|
| 31 |
+
|
| 32 |
+
__all__ = [
|
| 33 |
+
'FarmerChatPipeline',
|
| 34 |
+
'QueryRouter',
|
| 35 |
+
'MCPExecutor',
|
| 36 |
+
'ResponseCompiler'
|
| 37 |
+
]
|
src/compiler.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# """
|
| 2 |
+
# Stage 3: Response Compiler - Data Fusion
|
| 3 |
+
# """
|
| 4 |
+
|
| 5 |
+
# from typing import Dict, Any, List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# class ResponseCompiler:
|
| 9 |
+
# """Stage 3: Compile results from multiple servers"""
|
| 10 |
+
|
| 11 |
+
# def compile(self, raw_results: Dict[str, Any]) -> Dict[str, Any]:
|
| 12 |
+
# """
|
| 13 |
+
# Merge results into structured format
|
| 14 |
+
|
| 15 |
+
# Args:
|
| 16 |
+
# raw_results: Dictionary containing results from MCPExecutor
|
| 17 |
+
# {
|
| 18 |
+
# "results": {
|
| 19 |
+
# "weather": {"status": "success", "data": {...}},
|
| 20 |
+
# "soil_properties": {"status": "success", "data": {...}},
|
| 21 |
+
# ...
|
| 22 |
+
# },
|
| 23 |
+
# "execution_time_seconds": 3.5
|
| 24 |
+
# }
|
| 25 |
+
|
| 26 |
+
# Returns:
|
| 27 |
+
# {
|
| 28 |
+
# "successful_servers": List[str],
|
| 29 |
+
# "failed_servers": List[dict],
|
| 30 |
+
# "data": Dict[str, Any],
|
| 31 |
+
# "execution_time": float,
|
| 32 |
+
# "completeness": str
|
| 33 |
+
# }
|
| 34 |
+
# """
|
| 35 |
+
# results_dict = raw_results.get("results", {})
|
| 36 |
+
|
| 37 |
+
# successful = []
|
| 38 |
+
# failed = []
|
| 39 |
+
# compiled_data = {}
|
| 40 |
+
|
| 41 |
+
# for server_name, result in results_dict.items():
|
| 42 |
+
# if result.get("status") == "success":
|
| 43 |
+
# successful.append(server_name)
|
| 44 |
+
# compiled_data[server_name] = result.get("data", {})
|
| 45 |
+
# else:
|
| 46 |
+
# failed.append({
|
| 47 |
+
# "server": server_name,
|
| 48 |
+
# "error": result.get("error", "Unknown error")
|
| 49 |
+
# })
|
| 50 |
+
|
| 51 |
+
# return {
|
| 52 |
+
# "successful_servers": successful,
|
| 53 |
+
# "failed_servers": failed,
|
| 54 |
+
# "data": compiled_data,
|
| 55 |
+
# "execution_time": raw_results.get("execution_time_seconds", 0),
|
| 56 |
+
# "completeness": f"{len(successful)}/{len(results_dict)} servers"
|
| 57 |
+
# }
|
| 58 |
+
|
| 59 |
+
"""
|
| 60 |
+
Response Compiler - Stage 3
|
| 61 |
+
Compiles MCP results with focus on ALERTING INFORMATION ONLY
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
from typing import Dict, Any
|
| 65 |
+
from openai import OpenAI
|
| 66 |
+
import httpx
|
| 67 |
+
import os
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class ResponseCompiler:
|
| 71 |
+
"""
|
| 72 |
+
Compiles MCP server results into alert-focused responses.
|
| 73 |
+
|
| 74 |
+
KEY PRINCIPLE: All MCPs are queried, but the compiler extracts ONLY
|
| 75 |
+
the alerting, concerning, or actionable information. Normal/good status
|
| 76 |
+
is minimized or omitted entirely.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
def __init__(self):
|
| 80 |
+
"""Initialize compiler with OpenAI client"""
|
| 81 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 82 |
+
if not api_key:
|
| 83 |
+
raise ValueError("OPENAI_API_KEY environment variable not set")
|
| 84 |
+
|
| 85 |
+
# Create custom httpx client without proxies parameter (compatibility fix)
|
| 86 |
+
http_client = httpx.Client(
|
| 87 |
+
timeout=httpx.Timeout(60.0, connect=10.0),
|
| 88 |
+
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20)
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
self.client = OpenAI(api_key=api_key, http_client=http_client)
|
| 92 |
+
|
| 93 |
+
def compile_alert_summary(
|
| 94 |
+
self,
|
| 95 |
+
mcp_results: Dict[str, Any],
|
| 96 |
+
location: Dict[str, float],
|
| 97 |
+
location_name: str = ""
|
| 98 |
+
) -> str:
|
| 99 |
+
"""
|
| 100 |
+
Compile MCP results into alert summary focusing ONLY on concerning information.
|
| 101 |
+
|
| 102 |
+
This is where the intelligence lives - not in routing. All MCP servers are
|
| 103 |
+
queried, but we extract only what farmers need to act on.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
mcp_results: Results from all MCP servers
|
| 107 |
+
location: Dict with 'latitude' and 'longitude' keys
|
| 108 |
+
location_name: Optional human-readable location name
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
Alert summary highlighting only actionable concerns
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
# Build comprehensive context from all MCP data
|
| 115 |
+
context_parts = []
|
| 116 |
+
|
| 117 |
+
for server_name, result in mcp_results.items():
|
| 118 |
+
if result["status"] == "success" and result["data"]:
|
| 119 |
+
context_parts.append(f"=== {server_name.upper()} DATA ===\n{result['data']}")
|
| 120 |
+
|
| 121 |
+
if not context_parts:
|
| 122 |
+
return "Unable to generate alert summary - no data available from MCP servers."
|
| 123 |
+
|
| 124 |
+
full_context = "\n\n".join(context_parts)
|
| 125 |
+
location_str = f"{location_name} ({location['latitude']:.4f}°N, {location['longitude']:.4f}°E)"
|
| 126 |
+
|
| 127 |
+
# THE KEY PROMPT: Extract only alerting information
|
| 128 |
+
prompt = f"""You are an agricultural alert analyst. Your task is to analyze comprehensive agricultural data and extract ONLY the alerting, concerning, or time-sensitive information.
|
| 129 |
+
|
| 130 |
+
LOCATION: {location_str}
|
| 131 |
+
|
| 132 |
+
COMPREHENSIVE DATA FROM ALL MONITORING SYSTEMS:
|
| 133 |
+
{full_context}
|
| 134 |
+
|
| 135 |
+
YOUR TASK:
|
| 136 |
+
Generate a concise ALERT SUMMARY that includes ONLY:
|
| 137 |
+
|
| 138 |
+
1. **CRITICAL ALERTS** - Immediate threats requiring urgent action:
|
| 139 |
+
- Extreme weather conditions (heat waves, storms, frost)
|
| 140 |
+
- Active pest/disease outbreaks
|
| 141 |
+
- Severe water scarcity or excess
|
| 142 |
+
- Soil contamination or extreme deficiencies
|
| 143 |
+
|
| 144 |
+
2. **IMPORTANT WARNINGS** - Developing issues requiring attention:
|
| 145 |
+
- Concerning trends (declining water table, degrading soil)
|
| 146 |
+
- Moderate pest pressure building up
|
| 147 |
+
- Suboptimal weather patterns affecting crops
|
| 148 |
+
- Nutrient imbalances needing correction
|
| 149 |
+
|
| 150 |
+
3. **ACTIONABLE RECOMMENDATIONS** - What farmers should do:
|
| 151 |
+
- Specific actions with timing
|
| 152 |
+
- Preventive measures
|
| 153 |
+
- Mitigation strategies
|
| 154 |
+
|
| 155 |
+
CRITICAL RULES:
|
| 156 |
+
- OMIT all normal/good status information unless it provides important context
|
| 157 |
+
- If weather is normal → DON'T mention it or say "Weather: Normal" briefly
|
| 158 |
+
- If soil is healthy → SKIP or say "Soil: No concerns" briefly
|
| 159 |
+
- If no pest activity → SKIP or say "Pests: No threats detected" briefly
|
| 160 |
+
- FOCUS on deviations from normal, risks, and time-sensitive items
|
| 161 |
+
- Use specific numbers/dates only when they convey urgency
|
| 162 |
+
- Maximum 400 words total
|
| 163 |
+
- If everything is fine, say so clearly upfront then provide brief context
|
| 164 |
+
|
| 165 |
+
Structure:
|
| 166 |
+
1. Status Line: "CRITICAL ALERTS DETECTED" or "NO CRITICAL ALERTS - FAVORABLE CONDITIONS"
|
| 167 |
+
2. Critical Alerts section (if any)
|
| 168 |
+
3. Important Warnings section (if any)
|
| 169 |
+
4. Recommended Actions (always include if alerts/warnings exist)
|
| 170 |
+
5. Add raw API output in JSON format at end for reference.
|
| 171 |
+
|
| 172 |
+
Be direct. Skip pleasantries. Farmers need to know what matters."""
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
response = self.client.chat.completions.create(
|
| 176 |
+
model="gpt-4o",
|
| 177 |
+
messages=[
|
| 178 |
+
{
|
| 179 |
+
"role": "system",
|
| 180 |
+
"content": "You are an expert agricultural alert analyst. Extract ONLY concerning, alerting, or actionable information. Omit normal status unless contextually necessary."
|
| 181 |
+
},
|
| 182 |
+
{"role": "user", "content": prompt}
|
| 183 |
+
],
|
| 184 |
+
temperature=0.2,
|
| 185 |
+
max_tokens=1000
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
return response.choices[0].message.content.strip()
|
| 189 |
+
|
| 190 |
+
except Exception as e:
|
| 191 |
+
print(f"⚠️ Compilation error: {e}")
|
| 192 |
+
return self._create_fallback_summary(mcp_results, location_str)
|
| 193 |
+
|
| 194 |
+
def compile_response(
|
| 195 |
+
self,
|
| 196 |
+
query: str,
|
| 197 |
+
mcp_results: Dict[str, Any],
|
| 198 |
+
location: Dict[str, float]
|
| 199 |
+
) -> str:
|
| 200 |
+
"""
|
| 201 |
+
Compile MCP results into a response for a specific query.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
query: User's original query
|
| 205 |
+
mcp_results: Results from MCP servers
|
| 206 |
+
location: Dict with 'latitude' and 'longitude' keys
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
Compiled response text focusing on query-relevant information
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
# Format MCP results for context
|
| 213 |
+
context_parts = []
|
| 214 |
+
for server_name, result in mcp_results.items():
|
| 215 |
+
if result["status"] == "success" and result["data"]:
|
| 216 |
+
context_parts.append(f"{server_name.upper()}: {result['data']}")
|
| 217 |
+
|
| 218 |
+
context = "\n\n".join(context_parts)
|
| 219 |
+
|
| 220 |
+
prompt = f"""Answer this farmer's question using the provided data, focusing on actionable insights.
|
| 221 |
+
|
| 222 |
+
QUESTION: {query}
|
| 223 |
+
LOCATION: {location['latitude']:.4f}°N, {location['longitude']:.4f}°E
|
| 224 |
+
|
| 225 |
+
AVAILABLE DATA:
|
| 226 |
+
{context}
|
| 227 |
+
|
| 228 |
+
Provide a focused answer that:
|
| 229 |
+
1. Directly addresses the question
|
| 230 |
+
2. Highlights any concerning information relevant to the query
|
| 231 |
+
3. Gives specific recommendations
|
| 232 |
+
4. Keeps explanations brief and practical
|
| 233 |
+
5. Omits irrelevant normal/good status information
|
| 234 |
+
6. Add Raw API Output from all MCP Servers at the end for reference.
|
| 235 |
+
|
| 236 |
+
Be conversational but professional. Skip unnecessary background unless it aids understanding."""
|
| 237 |
+
|
| 238 |
+
try:
|
| 239 |
+
response = self.client.chat.completions.create(
|
| 240 |
+
model="gpt-4o",
|
| 241 |
+
messages=[
|
| 242 |
+
{"role": "system", "content": "You are a knowledgeable agricultural advisor providing practical guidance to farmers."},
|
| 243 |
+
{"role": "user", "content": prompt}
|
| 244 |
+
],
|
| 245 |
+
temperature=0.5,
|
| 246 |
+
max_tokens=800
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
return response.choices[0].message.content.strip()
|
| 250 |
+
|
| 251 |
+
except Exception as e:
|
| 252 |
+
print(f"⚠️ Compilation error: {e}")
|
| 253 |
+
return f"Error compiling response: {str(e)}"
|
| 254 |
+
|
| 255 |
+
def _create_fallback_summary(self, mcp_results: Dict[str, Any], location_str: str) -> str:
|
| 256 |
+
"""Create basic fallback summary if LLM compilation fails"""
|
| 257 |
+
|
| 258 |
+
summary_parts = [f"Alert Summary for {location_str}\n\n"]
|
| 259 |
+
|
| 260 |
+
for server_name, result in mcp_results.items():
|
| 261 |
+
if result["status"] == "success" and result["data"]:
|
| 262 |
+
summary_parts.append(f"{server_name.upper()}:")
|
| 263 |
+
summary_parts.append(str(result["data"])[:300] + "...\n")
|
| 264 |
+
|
| 265 |
+
return "\n".join(summary_parts)
|
src/executor.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# }
|
| 109 |
+
|
| 110 |
+
# """
|
| 111 |
+
# MCP Executor - Stage 2
|
| 112 |
+
# Executes parallel calls to MCP servers based on routing decisions
|
| 113 |
+
# """
|
| 114 |
+
|
| 115 |
+
# from typing import Dict, Any
|
| 116 |
+
# from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 117 |
+
# import asyncio
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# class MCPExecutor:
|
| 121 |
+
# """
|
| 122 |
+
# Executes MCP server calls based on routing decisions.
|
| 123 |
+
# Integrates with existing server implementations in src/servers/
|
| 124 |
+
# Handles both sync and async server methods.
|
| 125 |
+
# """
|
| 126 |
+
|
| 127 |
+
# def __init__(self, servers: Dict[str, Any]):
|
| 128 |
+
# """
|
| 129 |
+
# Initialize executor with MCP server instances.
|
| 130 |
+
|
| 131 |
+
# Args:
|
| 132 |
+
# servers: Dict mapping server names to initialized server objects
|
| 133 |
+
# e.g., {"weather": WeatherServer(), "soil": SoilPropertiesServer(), ...}
|
| 134 |
+
# """
|
| 135 |
+
# self.servers = servers
|
| 136 |
+
|
| 137 |
+
# def execute_parallel(self, routing: Dict[str, bool], location: Dict[str, float]) -> Dict[str, Any]:
|
| 138 |
+
# """
|
| 139 |
+
# Execute MCP server calls in parallel based on routing.
|
| 140 |
+
|
| 141 |
+
# Args:
|
| 142 |
+
# routing: Simple dict with server names as keys and True/False as values
|
| 143 |
+
# location: Dict with 'latitude' and 'longitude' keys
|
| 144 |
+
|
| 145 |
+
# Returns:
|
| 146 |
+
# Dict mapping server names to their results with metadata
|
| 147 |
+
# """
|
| 148 |
+
# results = {}
|
| 149 |
+
# tasks = []
|
| 150 |
+
|
| 151 |
+
# # Prepare tasks for servers marked for querying
|
| 152 |
+
# for server_name, should_query in routing.items():
|
| 153 |
+
# if should_query and server_name in self.servers:
|
| 154 |
+
# tasks.append({
|
| 155 |
+
# "server_name": server_name,
|
| 156 |
+
# "server": self.servers[server_name],
|
| 157 |
+
# "location": location
|
| 158 |
+
# })
|
| 159 |
+
|
| 160 |
+
# # Execute in parallel using ThreadPoolExecutor
|
| 161 |
+
# with ThreadPoolExecutor(max_workers=5) as executor:
|
| 162 |
+
# futures = {
|
| 163 |
+
# executor.submit(self._call_server_sync, task): task
|
| 164 |
+
# for task in tasks
|
| 165 |
+
# }
|
| 166 |
+
|
| 167 |
+
# for future in as_completed(futures):
|
| 168 |
+
# task = futures[future]
|
| 169 |
+
# server_name = task["server_name"]
|
| 170 |
+
|
| 171 |
+
# try:
|
| 172 |
+
# result = future.result(timeout=30)
|
| 173 |
+
# results[server_name] = {
|
| 174 |
+
# "data": result,
|
| 175 |
+
# "status": "success"
|
| 176 |
+
# }
|
| 177 |
+
# print(f"✓ {server_name.upper()}: Retrieved successfully")
|
| 178 |
+
|
| 179 |
+
# except Exception as e:
|
| 180 |
+
# results[server_name] = {
|
| 181 |
+
# "data": None,
|
| 182 |
+
# "status": "error",
|
| 183 |
+
# "error": str(e)
|
| 184 |
+
# }
|
| 185 |
+
# print(f"✗ {server_name.upper()}: Error - {str(e)}")
|
| 186 |
+
|
| 187 |
+
# return results
|
| 188 |
+
|
| 189 |
+
# def _call_server_sync(self, task: Dict[str, Any]) -> Any:
|
| 190 |
+
# """
|
| 191 |
+
# Call individual MCP server, handling both sync and async methods.
|
| 192 |
+
|
| 193 |
+
# Args:
|
| 194 |
+
# task: Dict containing server, location, and metadata
|
| 195 |
+
|
| 196 |
+
# Returns:
|
| 197 |
+
# Server response data
|
| 198 |
+
# """
|
| 199 |
+
# server = task["server"]
|
| 200 |
+
# location = task["location"]
|
| 201 |
+
|
| 202 |
+
# # Try async method first (most of your servers use async)
|
| 203 |
+
# if hasattr(server, 'get_data'):
|
| 204 |
+
# method = getattr(server, 'get_data')
|
| 205 |
+
|
| 206 |
+
# # Check if it's async
|
| 207 |
+
# if asyncio.iscoroutinefunction(method):
|
| 208 |
+
# # Run async method in new event loop
|
| 209 |
+
# try:
|
| 210 |
+
# loop = asyncio.new_event_loop()
|
| 211 |
+
# asyncio.set_event_loop(loop)
|
| 212 |
+
# result = loop.run_until_complete(
|
| 213 |
+
# method(location['latitude'], location['longitude'])
|
| 214 |
+
# )
|
| 215 |
+
# loop.close()
|
| 216 |
+
# return result
|
| 217 |
+
# except Exception as e:
|
| 218 |
+
# raise Exception(f"Async execution failed: {str(e)}")
|
| 219 |
+
# else:
|
| 220 |
+
# # Sync method
|
| 221 |
+
# return method(location['latitude'], location['longitude'])
|
| 222 |
+
|
| 223 |
+
# # Fallback to other method names
|
| 224 |
+
# elif hasattr(server, 'query'):
|
| 225 |
+
# return server.query(location)
|
| 226 |
+
# elif hasattr(server, 'fetch_data'):
|
| 227 |
+
# return server.fetch_data(location['latitude'], location['longitude'])
|
| 228 |
+
# else:
|
| 229 |
+
# raise AttributeError(f"Server {task['server_name']} has no compatible query method")
|
| 230 |
+
|
| 231 |
+
# def execute_sequential(self, routing: Dict[str, bool], location: Dict[str, float]) -> Dict[str, Any]:
|
| 232 |
+
# """
|
| 233 |
+
# Execute MCP server calls sequentially (fallback if parallel fails).
|
| 234 |
+
|
| 235 |
+
# Args:
|
| 236 |
+
# routing: Simple dict with server names as keys and True/False as values
|
| 237 |
+
# location: Dict with 'latitude' and 'longitude' keys
|
| 238 |
+
|
| 239 |
+
# Returns:
|
| 240 |
+
# Dict mapping server names to their results
|
| 241 |
+
# """
|
| 242 |
+
# results = {}
|
| 243 |
+
|
| 244 |
+
# for server_name, should_query in routing.items():
|
| 245 |
+
# if should_query and server_name in self.servers:
|
| 246 |
+
# try:
|
| 247 |
+
# task = {
|
| 248 |
+
# "server_name": server_name,
|
| 249 |
+
# "server": self.servers[server_name],
|
| 250 |
+
# "location": location
|
| 251 |
+
# }
|
| 252 |
+
|
| 253 |
+
# result = self._call_server_sync(task)
|
| 254 |
+
# results[server_name] = {
|
| 255 |
+
# "data": result,
|
| 256 |
+
# "status": "success"
|
| 257 |
+
# }
|
| 258 |
+
# print(f"✓ {server_name.upper()}: Retrieved successfully")
|
| 259 |
+
|
| 260 |
+
# except Exception as e:
|
| 261 |
+
# results[server_name] = {
|
| 262 |
+
# "data": None,
|
| 263 |
+
# "status": "error",
|
| 264 |
+
# "error": str(e)
|
| 265 |
+
# }
|
| 266 |
+
# print(f"✗ {server_name.upper()}: Error - {str(e)}")
|
| 267 |
+
|
| 268 |
+
# return results
|
| 269 |
+
|
| 270 |
+
# return results
|
| 271 |
+
|
| 272 |
+
"""
|
| 273 |
+
MCP Executor - Stage 2
|
| 274 |
+
Executes parallel calls to MCP servers based on routing decisions
|
| 275 |
+
FIXED: Simpler async handling to prevent deadlocks
|
| 276 |
+
"""
|
| 277 |
+
|
| 278 |
+
from typing import Dict, Any
|
| 279 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 280 |
+
import asyncio
|
| 281 |
+
import inspect
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
class MCPExecutor:
|
| 285 |
+
"""
|
| 286 |
+
Executes MCP server calls based on routing decisions.
|
| 287 |
+
Integrates with existing server implementations in src/servers/
|
| 288 |
+
Handles both sync and async server methods safely.
|
| 289 |
+
"""
|
| 290 |
+
|
| 291 |
+
def __init__(self, servers: Dict[str, Any]):
|
| 292 |
+
"""
|
| 293 |
+
Initialize executor with MCP server instances.
|
| 294 |
+
|
| 295 |
+
Args:
|
| 296 |
+
servers: Dict mapping server names to initialized server objects
|
| 297 |
+
e.g., {"weather": WeatherServer(), "soil": SoilPropertiesServer(), ...}
|
| 298 |
+
"""
|
| 299 |
+
self.servers = servers
|
| 300 |
+
|
| 301 |
+
def execute_parallel(self, routing: Dict[str, bool], location: Dict[str, float]) -> Dict[str, Any]:
|
| 302 |
+
"""
|
| 303 |
+
Execute MCP server calls in parallel based on routing.
|
| 304 |
+
|
| 305 |
+
Args:
|
| 306 |
+
routing: Simple dict with server names as keys and True/False as values
|
| 307 |
+
location: Dict with 'latitude' and 'longitude' keys
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
Dict mapping server names to their results with metadata
|
| 311 |
+
"""
|
| 312 |
+
results = {}
|
| 313 |
+
|
| 314 |
+
# For async servers, we need to run them differently
|
| 315 |
+
# Separate sync and async servers
|
| 316 |
+
sync_tasks = []
|
| 317 |
+
async_tasks = []
|
| 318 |
+
|
| 319 |
+
for server_name, should_query in routing.items():
|
| 320 |
+
if should_query and server_name in self.servers:
|
| 321 |
+
server = self.servers[server_name]
|
| 322 |
+
task = {
|
| 323 |
+
"server_name": server_name,
|
| 324 |
+
"server": server,
|
| 325 |
+
"location": location
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
# Check if server method is async
|
| 329 |
+
if hasattr(server, 'get_data'):
|
| 330 |
+
method = getattr(server, 'get_data')
|
| 331 |
+
if inspect.iscoroutinefunction(method):
|
| 332 |
+
async_tasks.append(task)
|
| 333 |
+
else:
|
| 334 |
+
sync_tasks.append(task)
|
| 335 |
+
else:
|
| 336 |
+
sync_tasks.append(task)
|
| 337 |
+
|
| 338 |
+
# Execute sync servers in parallel with ThreadPoolExecutor
|
| 339 |
+
if sync_tasks:
|
| 340 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
| 341 |
+
futures = {
|
| 342 |
+
executor.submit(self._call_sync_server, task): task
|
| 343 |
+
for task in sync_tasks
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
for future in as_completed(futures):
|
| 347 |
+
task = futures[future]
|
| 348 |
+
server_name = task["server_name"]
|
| 349 |
+
|
| 350 |
+
try:
|
| 351 |
+
result = future.result(timeout=30)
|
| 352 |
+
results[server_name] = {
|
| 353 |
+
"data": result,
|
| 354 |
+
"status": "success"
|
| 355 |
+
}
|
| 356 |
+
print(f"✓ {server_name.upper()}: Retrieved successfully")
|
| 357 |
+
except Exception as e:
|
| 358 |
+
results[server_name] = {
|
| 359 |
+
"data": None,
|
| 360 |
+
"status": "error",
|
| 361 |
+
"error": str(e)
|
| 362 |
+
}
|
| 363 |
+
print(f"✗ {server_name.upper()}: Error - {str(e)}")
|
| 364 |
+
|
| 365 |
+
# Execute async servers together in single event loop
|
| 366 |
+
if async_tasks:
|
| 367 |
+
try:
|
| 368 |
+
async_results = asyncio.run(self._execute_async_batch(async_tasks))
|
| 369 |
+
results.update(async_results)
|
| 370 |
+
except Exception as e:
|
| 371 |
+
# If batch fails, mark all as failed
|
| 372 |
+
for task in async_tasks:
|
| 373 |
+
results[task["server_name"]] = {
|
| 374 |
+
"data": None,
|
| 375 |
+
"status": "error",
|
| 376 |
+
"error": f"Async batch execution failed: {str(e)}"
|
| 377 |
+
}
|
| 378 |
+
print(f"✗ {task['server_name'].upper()}: Async batch error")
|
| 379 |
+
|
| 380 |
+
return results
|
| 381 |
+
|
| 382 |
+
async def _execute_async_batch(self, tasks: list) -> Dict[str, Any]:
|
| 383 |
+
"""
|
| 384 |
+
Execute multiple async server calls concurrently in a single event loop.
|
| 385 |
+
This is safer than creating multiple event loops.
|
| 386 |
+
"""
|
| 387 |
+
results = {}
|
| 388 |
+
|
| 389 |
+
# Create async tasks for all servers
|
| 390 |
+
async_calls = []
|
| 391 |
+
for task in tasks:
|
| 392 |
+
async_calls.append(self._call_async_server(task))
|
| 393 |
+
|
| 394 |
+
# Execute all async calls concurrently
|
| 395 |
+
task_results = await asyncio.gather(*async_calls, return_exceptions=True)
|
| 396 |
+
|
| 397 |
+
# Process results
|
| 398 |
+
for task, result in zip(tasks, task_results):
|
| 399 |
+
server_name = task["server_name"]
|
| 400 |
+
|
| 401 |
+
if isinstance(result, Exception):
|
| 402 |
+
results[server_name] = {
|
| 403 |
+
"data": None,
|
| 404 |
+
"status": "error",
|
| 405 |
+
"error": str(result)
|
| 406 |
+
}
|
| 407 |
+
print(f"✗ {server_name.upper()}: Error - {str(result)}")
|
| 408 |
+
else:
|
| 409 |
+
results[server_name] = {
|
| 410 |
+
"data": result,
|
| 411 |
+
"status": "success"
|
| 412 |
+
}
|
| 413 |
+
print(f"✓ {server_name.upper()}: Retrieved successfully")
|
| 414 |
+
|
| 415 |
+
return results
|
| 416 |
+
|
| 417 |
+
async def _call_async_server(self, task: Dict[str, Any]) -> Any:
|
| 418 |
+
"""Call individual async MCP server"""
|
| 419 |
+
server = task["server"]
|
| 420 |
+
location = task["location"]
|
| 421 |
+
|
| 422 |
+
if hasattr(server, 'get_data'):
|
| 423 |
+
return await server.get_data(location['latitude'], location['longitude'])
|
| 424 |
+
else:
|
| 425 |
+
raise AttributeError(f"Server {task['server_name']} has no get_data method")
|
| 426 |
+
|
| 427 |
+
def _call_sync_server(self, task: Dict[str, Any]) -> Any:
|
| 428 |
+
"""Call individual sync MCP server"""
|
| 429 |
+
server = task["server"]
|
| 430 |
+
location = task["location"]
|
| 431 |
+
|
| 432 |
+
if hasattr(server, 'get_data'):
|
| 433 |
+
return server.get_data(location['latitude'], location['longitude'])
|
| 434 |
+
elif hasattr(server, 'query'):
|
| 435 |
+
return server.query(location)
|
| 436 |
+
elif hasattr(server, 'fetch_data'):
|
| 437 |
+
return server.fetch_data(location['latitude'], location['longitude'])
|
| 438 |
+
else:
|
| 439 |
+
raise AttributeError(f"Server {task['server_name']} has no compatible query method")
|
| 440 |
+
|
| 441 |
+
def execute_sequential(self, routing: Dict[str, bool], location: Dict[str, float]) -> Dict[str, Any]:
|
| 442 |
+
"""
|
| 443 |
+
Execute MCP server calls sequentially (fallback if parallel fails).
|
| 444 |
+
"""
|
| 445 |
+
results = {}
|
| 446 |
+
|
| 447 |
+
for server_name, should_query in routing.items():
|
| 448 |
+
if should_query and server_name in self.servers:
|
| 449 |
+
try:
|
| 450 |
+
server = self.servers[server_name]
|
| 451 |
+
|
| 452 |
+
# Check if async
|
| 453 |
+
if hasattr(server, 'get_data') and inspect.iscoroutinefunction(server.get_data):
|
| 454 |
+
# Run async method
|
| 455 |
+
result = asyncio.run(server.get_data(location['latitude'], location['longitude']))
|
| 456 |
+
else:
|
| 457 |
+
# Run sync method
|
| 458 |
+
task = {
|
| 459 |
+
"server_name": server_name,
|
| 460 |
+
"server": server,
|
| 461 |
+
"location": location
|
| 462 |
+
}
|
| 463 |
+
result = self._call_sync_server(task)
|
| 464 |
+
|
| 465 |
+
results[server_name] = {
|
| 466 |
+
"data": result,
|
| 467 |
+
"status": "success"
|
| 468 |
+
}
|
| 469 |
+
print(f"✓ {server_name.upper()}: Retrieved successfully")
|
| 470 |
+
|
| 471 |
+
except Exception as e:
|
| 472 |
+
results[server_name] = {
|
| 473 |
+
"data": None,
|
| 474 |
+
"status": "error",
|
| 475 |
+
"error": str(e)
|
| 476 |
+
}
|
| 477 |
+
print(f"✗ {server_name.upper()}: Error - {str(e)}")
|
| 478 |
+
|
| 479 |
+
return results
|
| 480 |
+
|
| 481 |
+
return results
|
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
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# """
|
| 2 |
+
# Complete Multi-Stage MCP Pipeline
|
| 3 |
+
# Orchestrates: Router → Executor → Compiler → Translator
|
| 4 |
+
# """
|
| 5 |
+
|
| 6 |
+
# import time
|
| 7 |
+
# from typing import Dict, Any
|
| 8 |
+
# from openai import OpenAI
|
| 9 |
+
|
| 10 |
+
# from .router import QueryRouter
|
| 11 |
+
# from .executor import MCPExecutor, MCP_SERVER_REGISTRY
|
| 12 |
+
# from .compiler import ResponseCompiler
|
| 13 |
+
# from .translator import FarmerTranslator
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# class FarmerChatPipeline:
|
| 17 |
+
# """Complete multi-stage MCP pipeline"""
|
| 18 |
+
|
| 19 |
+
# def __init__(self, openai_client: OpenAI, location: Dict[str, Any]):
|
| 20 |
+
# self.location = location
|
| 21 |
+
# self.router = QueryRouter(openai_client, MCP_SERVER_REGISTRY)
|
| 22 |
+
# self.executor = MCPExecutor()
|
| 23 |
+
# self.compiler = ResponseCompiler()
|
| 24 |
+
# self.translator = FarmerTranslator(openai_client)
|
| 25 |
+
|
| 26 |
+
# async def process_query(self, query: str, verbose: bool = False) -> Dict[str, Any]:
|
| 27 |
+
# """
|
| 28 |
+
# Process farmer query through complete pipeline
|
| 29 |
+
|
| 30 |
+
# Returns:
|
| 31 |
+
# {
|
| 32 |
+
# "query": str,
|
| 33 |
+
# "routing": dict,
|
| 34 |
+
# "compiled_data": dict,
|
| 35 |
+
# "advice": str,
|
| 36 |
+
# "pipeline_time_seconds": float
|
| 37 |
+
# }
|
| 38 |
+
# """
|
| 39 |
+
# if verbose:
|
| 40 |
+
# print(f"\n🌾 Processing: {query}")
|
| 41 |
+
# print(f"📍 Location: {self.location['name']}")
|
| 42 |
+
|
| 43 |
+
# pipeline_start = time.time()
|
| 44 |
+
|
| 45 |
+
# # STAGE 1: Query Routing
|
| 46 |
+
# if verbose:
|
| 47 |
+
# print("🎯 Stage 1: Routing...")
|
| 48 |
+
|
| 49 |
+
# routing = self.router.route(query, self.location)
|
| 50 |
+
|
| 51 |
+
# if verbose:
|
| 52 |
+
# print(f" → Servers: {', '.join(routing['required_servers'])}")
|
| 53 |
+
|
| 54 |
+
# # STAGE 2: MCP Execution (Parallel)
|
| 55 |
+
# if verbose:
|
| 56 |
+
# print("⚙️ Stage 2: Executing MCP servers...")
|
| 57 |
+
|
| 58 |
+
# raw_results = await self.executor.execute_parallel(
|
| 59 |
+
# routing['required_servers'],
|
| 60 |
+
# self.location['lat'],
|
| 61 |
+
# self.location['lon']
|
| 62 |
+
# )
|
| 63 |
+
|
| 64 |
+
# if verbose:
|
| 65 |
+
# print(f" → Completed in {raw_results['execution_time_seconds']}s")
|
| 66 |
+
|
| 67 |
+
# # STAGE 3: Response Compilation
|
| 68 |
+
# if verbose:
|
| 69 |
+
# print("🔗 Stage 3: Compiling results...")
|
| 70 |
+
|
| 71 |
+
# compiled = self.compiler.compile(raw_results)
|
| 72 |
+
|
| 73 |
+
# if verbose:
|
| 74 |
+
# print(f" → {compiled['completeness']}")
|
| 75 |
+
|
| 76 |
+
# # STAGE 4: Farmer Translation
|
| 77 |
+
# if verbose:
|
| 78 |
+
# print("🌾 Stage 4: Generating advice...")
|
| 79 |
+
|
| 80 |
+
# farmer_advice = self.translator.translate(query, compiled, self.location)
|
| 81 |
+
|
| 82 |
+
# pipeline_time = time.time() - pipeline_start
|
| 83 |
+
|
| 84 |
+
# if verbose:
|
| 85 |
+
# print(f"✅ Complete! Total: {pipeline_time:.2f}s\n")
|
| 86 |
+
|
| 87 |
+
# return {
|
| 88 |
+
# "query": query,
|
| 89 |
+
# "routing": routing,
|
| 90 |
+
# "compiled_data": compiled,
|
| 91 |
+
# "advice": farmer_advice,
|
| 92 |
+
# "pipeline_time_seconds": round(pipeline_time, 2)
|
| 93 |
+
# }
|
| 94 |
+
|
| 95 |
+
"""
|
| 96 |
+
Farmer.chat Pipeline - Main Orchestrator
|
| 97 |
+
Coordinates Router → Executor → Compiler stages for alert generation
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
from typing import Dict, Any, Optional
|
| 101 |
+
from .router import QueryRouter
|
| 102 |
+
from .executor import MCPExecutor
|
| 103 |
+
from .compiler import ResponseCompiler
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class FarmerChatPipeline:
|
| 107 |
+
"""
|
| 108 |
+
Main pipeline orchestrating the complete alert generation workflow.
|
| 109 |
+
|
| 110 |
+
Architecture:
|
| 111 |
+
Stage 1: QueryRouter - Determines which MCP servers to query with priorities
|
| 112 |
+
Stage 2: MCPExecutor - Executes parallel calls to selected MCP servers
|
| 113 |
+
Stage 3: ResponseCompiler - Compiles results into actionable alert summary
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(self, servers: Dict[str, Any], location: Dict[str, float]):
|
| 117 |
+
"""
|
| 118 |
+
Initialize pipeline with MCP servers and default location.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
servers: Dict mapping server names to initialized server instances
|
| 122 |
+
location: Default location dict with 'latitude' and 'longitude' keys
|
| 123 |
+
"""
|
| 124 |
+
self.servers = servers
|
| 125 |
+
self.location = location
|
| 126 |
+
|
| 127 |
+
# Initialize pipeline stages
|
| 128 |
+
self.router = QueryRouter()
|
| 129 |
+
self.executor = MCPExecutor(servers)
|
| 130 |
+
self.compiler = ResponseCompiler()
|
| 131 |
+
|
| 132 |
+
print(f"✓ Pipeline initialized for location: {location['latitude']:.4f}°N, {location['longitude']:.4f}°E")
|
| 133 |
+
|
| 134 |
+
def generate_alert(
|
| 135 |
+
self,
|
| 136 |
+
location: Optional[Dict[str, float]] = None,
|
| 137 |
+
location_name: str = ""
|
| 138 |
+
) -> Dict[str, Any]:
|
| 139 |
+
"""
|
| 140 |
+
Generate comprehensive alert summary for a location.
|
| 141 |
+
|
| 142 |
+
Always queries ALL MCP servers. The compiler extracts only
|
| 143 |
+
the alerting/concerning information from the comprehensive data.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
location: Optional location override. Uses default if not provided.
|
| 147 |
+
location_name: Human-readable location name for context
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
Dict containing:
|
| 151 |
+
- alert_summary: Compiled alert text (only concerning info)
|
| 152 |
+
- location: Location coordinates used
|
| 153 |
+
- mcp_results: Raw results from each MCP server
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
# Use provided location or default
|
| 157 |
+
query_location = location or self.location
|
| 158 |
+
|
| 159 |
+
print(f"\n{'='*60}")
|
| 160 |
+
print(f"Generating Alert Summary for {location_name or 'Location'}")
|
| 161 |
+
print(f"Coordinates: {query_location['latitude']:.4f}°N, {query_location['longitude']:.4f}°E")
|
| 162 |
+
print(f"{'='*60}\n")
|
| 163 |
+
|
| 164 |
+
# Stage 1: Route - Always query all servers for alerts
|
| 165 |
+
print("Stage 1: Routing to all MCP servers...")
|
| 166 |
+
routing = self.router.route_alert_query(query_location)
|
| 167 |
+
print(f"✓ Routing complete: All 5 servers will be queried")
|
| 168 |
+
|
| 169 |
+
# Stage 2: Execute - Query all MCP servers in parallel
|
| 170 |
+
print("\nStage 2: Executing parallel MCP server calls...")
|
| 171 |
+
mcp_results = self.executor.execute_parallel(routing, query_location)
|
| 172 |
+
|
| 173 |
+
success_count = sum(1 for r in mcp_results.values() if r.get("status") == "success")
|
| 174 |
+
print(f"✓ Execution complete: {success_count}/{len(mcp_results)} servers responded successfully")
|
| 175 |
+
|
| 176 |
+
# Stage 3: Compile - Extract ONLY alerting information
|
| 177 |
+
print("\nStage 3: Compiling alert summary (extracting concerning info only)...")
|
| 178 |
+
alert_summary = self.compiler.compile_alert_summary(
|
| 179 |
+
mcp_results,
|
| 180 |
+
query_location,
|
| 181 |
+
location_name
|
| 182 |
+
)
|
| 183 |
+
print("✓ Alert summary generated (focusing on alerts/concerns only)")
|
| 184 |
+
|
| 185 |
+
print(f"\n{'='*60}\n")
|
| 186 |
+
|
| 187 |
+
return {
|
| 188 |
+
"alert_summary": alert_summary,
|
| 189 |
+
"location": query_location,
|
| 190 |
+
"location_name": location_name,
|
| 191 |
+
"mcp_results": mcp_results
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
def process_query(
|
| 195 |
+
self,
|
| 196 |
+
query: str,
|
| 197 |
+
location: Optional[Dict[str, float]] = None
|
| 198 |
+
) -> Dict[str, Any]:
|
| 199 |
+
"""
|
| 200 |
+
Process a specific farmer query through the pipeline.
|
| 201 |
+
|
| 202 |
+
For now, also queries all servers to ensure comprehensive data.
|
| 203 |
+
The compiler extracts query-relevant information.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
query: Farmer's question or request
|
| 207 |
+
location: Optional location override
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
Dict containing:
|
| 211 |
+
- response: Compiled response text
|
| 212 |
+
- location: Location coordinates used
|
| 213 |
+
- mcp_results: Raw results from MCP servers
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
# Use provided location or default
|
| 217 |
+
query_location = location or self.location
|
| 218 |
+
|
| 219 |
+
print(f"\n{'='*60}")
|
| 220 |
+
print(f"Processing Query: {query}")
|
| 221 |
+
print(f"Location: {query_location['latitude']:.4f}°N, {query_location['longitude']:.4f}°E")
|
| 222 |
+
print(f"{'='*60}\n")
|
| 223 |
+
|
| 224 |
+
# Stage 1: Route - For now, query all servers
|
| 225 |
+
print("Stage 1: Routing to all MCP servers...")
|
| 226 |
+
routing = self.router.route_query(query, query_location)
|
| 227 |
+
print(f"✓ Routing complete: All servers will be queried")
|
| 228 |
+
|
| 229 |
+
# Stage 2: Execute MCP calls
|
| 230 |
+
print("\nStage 2: Executing MCP server calls...")
|
| 231 |
+
mcp_results = self.executor.execute_parallel(routing, query_location)
|
| 232 |
+
|
| 233 |
+
success_count = sum(1 for r in mcp_results.values() if r.get("status") == "success")
|
| 234 |
+
print(f"✓ Execution complete: {success_count}/{len(mcp_results)} servers responded")
|
| 235 |
+
|
| 236 |
+
# Stage 3: Compile response
|
| 237 |
+
print("\nStage 3: Compiling response...")
|
| 238 |
+
response = self.compiler.compile_response(query, mcp_results, query_location)
|
| 239 |
+
print("✓ Response compiled")
|
| 240 |
+
|
| 241 |
+
print(f"\n{'='*60}\n")
|
| 242 |
+
|
| 243 |
+
return {
|
| 244 |
+
"response": response,
|
| 245 |
+
"location": query_location,
|
| 246 |
+
"mcp_results": mcp_results
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def update_location(self, location: Dict[str, float]):
|
| 250 |
+
"""
|
| 251 |
+
Update default location for pipeline.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
location: New default location dict with 'latitude' and 'longitude' keys
|
| 255 |
+
"""
|
| 256 |
+
self.location = location
|
| 257 |
+
print(f"✓ Default location updated to: {location['latitude']:.4f}°N, {location['longitude']:.4f}°E")
|
| 258 |
+
|
| 259 |
+
def get_server_status(self) -> Dict[str, str]:
|
| 260 |
+
"""
|
| 261 |
+
Get status of all MCP servers.
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
Dict mapping server names to "available" or "unavailable"
|
| 265 |
+
"""
|
| 266 |
+
status = {}
|
| 267 |
+
for server_name, server in self.servers.items():
|
| 268 |
+
try:
|
| 269 |
+
# Try to check if server is responsive
|
| 270 |
+
if hasattr(server, 'health_check'):
|
| 271 |
+
status[server_name] = "available" if server.health_check() else "unavailable"
|
| 272 |
+
else:
|
| 273 |
+
status[server_name] = "available" # Assume available if no health check
|
| 274 |
+
except:
|
| 275 |
+
status[server_name] = "unavailable"
|
| 276 |
+
|
| 277 |
+
return status
|
src/router.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# }
|
| 137 |
+
|
| 138 |
+
"""
|
| 139 |
+
Query Router - Stage 1
|
| 140 |
+
Simple router that always queries all MCP servers for alert generation.
|
| 141 |
+
The intelligence is in the compiler, not the router.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
from typing import Dict, Any
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class QueryRouter:
|
| 148 |
+
"""
|
| 149 |
+
Router for alert generation system.
|
| 150 |
+
|
| 151 |
+
For alert generation, ALWAYS queries all MCP servers since we need
|
| 152 |
+
comprehensive data to identify potential issues. The compiler handles
|
| 153 |
+
the intelligence of extracting only alerting/concerning information.
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
def __init__(self):
|
| 157 |
+
"""Initialize router - no LLM needed for simple all-server routing"""
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
def route_alert_query(self, location: Dict[str, float]) -> Dict[str, bool]:
|
| 161 |
+
"""
|
| 162 |
+
Route for alert generation - always query ALL servers.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
location: Dict with 'latitude' and 'longitude' keys
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
Dict with all servers set to True
|
| 169 |
+
"""
|
| 170 |
+
return {
|
| 171 |
+
"weather": True,
|
| 172 |
+
"soil": True,
|
| 173 |
+
"water": True,
|
| 174 |
+
"elevation": True,
|
| 175 |
+
"pests": True
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
def route_query(self, query: str, location: Dict[str, float]) -> Dict[str, bool]:
|
| 179 |
+
"""
|
| 180 |
+
Route a general query - for now, also queries all servers.
|
| 181 |
+
|
| 182 |
+
In the future, could use LLM to determine which servers are relevant
|
| 183 |
+
to the specific query. But for alert generation, we always want all data.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
query: User's query
|
| 187 |
+
location: Dict with 'latitude' and 'longitude' keys
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
Dict indicating which servers to query
|
| 191 |
+
"""
|
| 192 |
+
# For now, query all servers for any query
|
| 193 |
+
# The compiler will extract relevant information
|
| 194 |
+
return self.route_alert_query(location)
|
src/servers/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Server Implementations
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# Import all server classes
|
| 6 |
+
from .weather import WeatherServer
|
| 7 |
+
from .soil import SoilPropertiesServer
|
| 8 |
+
from .water import WaterServer
|
| 9 |
+
from .elevation import ElevationServer
|
| 10 |
+
from .pests import PestsServer
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
'WeatherServer',
|
| 14 |
+
'SoilPropertiesServer',
|
| 15 |
+
'WaterServer',
|
| 16 |
+
'ElevationServer',
|
| 17 |
+
'PestsServer'
|
| 18 |
+
]
|
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,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Soil Properties Server using Google Earth Engine with lazy initialization"""
|
| 2 |
+
|
| 3 |
+
import ee
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SoilPropertiesServer:
|
| 10 |
+
"""Google Earth Engine Soil Server with lazy initialization"""
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
"""Initialize server (GEE will be initialized on first use)"""
|
| 14 |
+
self._gee_initialized = False
|
| 15 |
+
|
| 16 |
+
self.layers = {
|
| 17 |
+
"clay": "OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02",
|
| 18 |
+
"sand": "OpenLandMap/SOL/SOL_SAND-WFRACTION_USDA-3A1A1A_M/v02",
|
| 19 |
+
"phh2o": "OpenLandMap/SOL/SOL_PH-H2O_USDA-4C1A2A_M/v02",
|
| 20 |
+
"soc": "OpenLandMap/SOL/SOL_ORGANIC-CARBON_USDA-6A1C_M/v02"
|
| 21 |
+
}
|
| 22 |
+
self.depth_band = "b0"
|
| 23 |
+
|
| 24 |
+
def _initialize_gee(self):
|
| 25 |
+
"""Initialize GEE lazily (only when first request is made)"""
|
| 26 |
+
if self._gee_initialized:
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
# Check if service account key is provided
|
| 31 |
+
service_account_key = os.environ.get('GEE_SERVICE_ACCOUNT_KEY')
|
| 32 |
+
|
| 33 |
+
if service_account_key:
|
| 34 |
+
# Parse JSON key
|
| 35 |
+
credentials_dict = json.loads(service_account_key)
|
| 36 |
+
credentials = ee.ServiceAccountCredentials(
|
| 37 |
+
credentials_dict['client_email'],
|
| 38 |
+
key_data=service_account_key
|
| 39 |
+
)
|
| 40 |
+
ee.Initialize(credentials)
|
| 41 |
+
print("✅ GEE initialized with service account (lazy)")
|
| 42 |
+
else:
|
| 43 |
+
# Fallback to default credentials (for local development)
|
| 44 |
+
ee.Initialize(project='MCPprototypeGEE')
|
| 45 |
+
print("✅ GEE initialized with default credentials (lazy)")
|
| 46 |
+
|
| 47 |
+
self._gee_initialized = True
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"⚠️ GEE lazy initialization failed: {str(e)}")
|
| 51 |
+
raise
|
| 52 |
+
|
| 53 |
+
async def get_data(self, lat: float, lon: float) -> Dict[str, Any]:
|
| 54 |
+
"""Get soil properties at coordinate"""
|
| 55 |
+
|
| 56 |
+
# Initialize GEE on first request (lazy loading)
|
| 57 |
+
if not self._gee_initialized:
|
| 58 |
+
self._initialize_gee()
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
point = ee.Geometry.Point([lon, lat])
|
| 62 |
+
results = {}
|
| 63 |
+
|
| 64 |
+
for prop_name, image_id in self.layers.items():
|
| 65 |
+
try:
|
| 66 |
+
image = ee.Image(image_id).select(self.depth_band)
|
| 67 |
+
value = image.sample(point, 250).first().get(self.depth_band).getInfo()
|
| 68 |
+
|
| 69 |
+
if value is not None:
|
| 70 |
+
if prop_name in ['clay', 'sand']:
|
| 71 |
+
results[prop_name] = round(value / 10, 1)
|
| 72 |
+
elif prop_name == 'phh2o':
|
| 73 |
+
results[prop_name] = round(value / 10, 1)
|
| 74 |
+
elif prop_name == 'soc':
|
| 75 |
+
results[prop_name] = round(value / 10, 1)
|
| 76 |
+
else:
|
| 77 |
+
results[prop_name] = None
|
| 78 |
+
except:
|
| 79 |
+
results[prop_name] = None
|
| 80 |
+
|
| 81 |
+
if not any(v is not None for v in results.values()):
|
| 82 |
+
return {
|
| 83 |
+
"status": "error",
|
| 84 |
+
"error": "No soil data available"
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
silt = None
|
| 88 |
+
if results.get("clay") and results.get("sand"):
|
| 89 |
+
silt = round(100 - results["clay"] - results["sand"], 1)
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
"status": "success",
|
| 93 |
+
"data": {
|
| 94 |
+
"clay_percent": results.get("clay"),
|
| 95 |
+
"sand_percent": results.get("sand"),
|
| 96 |
+
"silt_percent": silt,
|
| 97 |
+
"pH": results.get("phh2o"),
|
| 98 |
+
"organic_carbon_g_kg": results.get("soc"),
|
| 99 |
+
"data_source": "Google Earth Engine (OpenLandMap)",
|
| 100 |
+
"location": {"latitude": lat, "longitude": lon},
|
| 101 |
+
"depth": "0-5cm"
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
return {
|
| 107 |
+
"status": "error",
|
| 108 |
+
"error": str(e)
|
| 109 |
+
}
|
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)}"
|