akashub commited on
Commit
6afc01a
·
1 Parent(s): 8ce5453

fix: re-adding local code files

Browse files
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)}"