Tracy André commited on
Commit
a507ec3
·
1 Parent(s): eb1a639
Files changed (2) hide show
  1. app.py +157 -259
  2. requirements.txt +1 -0
app.py CHANGED
@@ -1,297 +1,195 @@
1
  import os
2
- import json
3
- import gradio as gr
4
- from fastapi import FastAPI, Header, HTTPException, Request
5
- from fastapi.responses import JSONResponse
6
  from gradio.routes import mount_gradio_app
7
 
8
- # Import your existing Gradio app
9
  from gradio_app import create_gradio_app
 
 
10
 
11
  # ========= Configuration =========
12
  PORT = int(os.environ.get("PORT", 7860))
13
- MCP_BEARER = os.getenv("MCP_BEARER", "") # définir dans Spaces > Settings > Variables and secrets
14
 
15
- # ========= App FastAPI (parent) =========
16
- api = FastAPI(title="MCP + Gradio on HF Spaces")
 
17
 
18
- @api.get("/health")
19
- def health():
20
- return {"ok": True}
21
 
22
- def _check_auth(authorization: str | None):
23
- """Vérifie le header Authorization: Bearer <token> si MCP_BEARER est défini."""
24
- if not MCP_BEARER:
25
- return
26
- if not authorization or not authorization.startswith("Bearer "):
27
- raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
28
- token = authorization.split(" ", 1)[1]
29
- if token != MCP_BEARER:
30
- raise HTTPException(status_code=401, detail="Unauthorized")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- def handle_mcp_request(payload: dict) -> dict:
33
- """Handle MCP JSON-RPC 2.0 requests."""
34
- # Validate JSON-RPC 2.0 format
35
- if payload.get("jsonrpc") != "2.0":
36
- return {
37
- "jsonrpc": "2.0",
38
- "id": payload.get("id"),
39
- "error": {
40
- "code": -32600,
41
- "message": "Invalid Request",
42
- "data": "Missing or invalid jsonrpc version"
43
- }
44
- }
45
-
46
- request_id = payload.get("id")
47
- method = payload.get("method")
48
- params = payload.get("params", {})
49
-
50
- if not method:
51
- return {
52
- "jsonrpc": "2.0",
53
- "id": request_id,
54
- "error": {
55
- "code": -32600,
56
- "message": "Invalid Request",
57
- "data": "Missing method"
58
- }
59
- }
60
-
61
- # Handle MCP standard methods
62
- if method == "initialize":
63
- return {
64
- "jsonrpc": "2.0",
65
- "id": request_id,
66
- "result": {
67
- "protocolVersion": "2024-11-05",
68
- "capabilities": {
69
- "tools": {
70
- "listChanged": False
71
- },
72
- "resources": {
73
- "subscribe": False,
74
- "listChanged": False
75
- }
76
- },
77
- "serverInfo": {
78
- "name": "agricultural-mcp-server",
79
- "version": "1.0.0"
80
- }
81
- }
82
- }
83
-
84
- elif method == "tools/list":
85
- return {
86
- "jsonrpc": "2.0",
87
- "id": request_id,
88
- "result": {
89
- "tools": [
90
- {
91
- "name": "analyze_weed_pressure",
92
- "description": "Analyze weed pressure trends using IFT herbicide data",
93
- "inputSchema": {
94
- "type": "object",
95
- "properties": {
96
- "years": {
97
- "type": "array",
98
- "items": {"type": "integer"},
99
- "description": "Years to analyze"
100
- },
101
- "plots": {
102
- "type": "array",
103
- "items": {"type": "string"},
104
- "description": "Plot names to analyze"
105
- }
106
- }
107
- }
108
- },
109
- {
110
- "name": "predict_future_pressure",
111
- "description": "Predict future weed pressure for target years",
112
- "inputSchema": {
113
- "type": "object",
114
- "properties": {
115
- "target_years": {
116
- "type": "array",
117
- "items": {"type": "integer"},
118
- "description": "Years to predict"
119
- },
120
- "max_ift": {
121
- "type": "number",
122
- "description": "Maximum IFT threshold for sensitive crops"
123
- }
124
- }
125
- }
126
- },
127
- {
128
- "name": "analyze_crop_rotation",
129
- "description": "Analyze crop rotation impact on weed pressure",
130
- "inputSchema": {
131
- "type": "object",
132
- "properties": {}
133
- }
134
- }
135
- ]
136
- }
137
- }
138
-
139
- elif method == "tools/call":
140
- tool_name = params.get("name")
141
- tool_args = params.get("arguments", {})
142
 
143
- if tool_name == "analyze_weed_pressure":
144
- return {
145
- "jsonrpc": "2.0",
146
- "id": request_id,
147
- "result": {
148
- "content": [
149
- {
150
- "type": "text",
151
- "text": f"Analyse de la pression adventices pour les années {tool_args.get('years', [])} et parcelles {tool_args.get('plots', [])}\n\nCette fonction analyserait normalement les données IFT herbicides de votre dataset agricultural."
152
- }
153
- ]
154
- }
155
- }
156
 
157
- elif tool_name == "predict_future_pressure":
158
- return {
159
- "jsonrpc": "2.0",
160
- "id": request_id,
161
- "result": {
162
- "content": [
163
- {
164
- "type": "text",
165
- "text": f"Prédiction de pression adventices pour {tool_args.get('target_years', [])} avec seuil IFT max {tool_args.get('max_ift', 1.0)}\n\nCette fonction utiliserait vos modèles d'apprentissage automatique pour prédire les futures pressions."
166
- }
167
- ]
168
- }
169
- }
170
 
171
- elif tool_name == "analyze_crop_rotation":
172
- return {
173
- "jsonrpc": "2.0",
174
- "id": request_id,
175
- "result": {
176
- "content": [
177
- {
178
- "type": "text",
179
- "text": "Analyse de l'impact des rotations culturales sur la pression adventices\n\nCette fonction analyserait les patterns de rotation dans votre dataset."
180
- }
181
- ]
182
- }
183
- }
184
 
185
- else:
186
- return {
187
- "jsonrpc": "2.0",
188
- "id": request_id,
189
- "error": {
190
- "code": -32601,
191
- "message": "Method not found",
192
- "data": f"Unknown tool: {tool_name}"
193
- }
194
- }
195
-
196
- elif method == "resources/list":
197
- return {
198
- "jsonrpc": "2.0",
199
- "id": request_id,
200
- "result": {
201
- "resources": [
202
- {
203
- "uri": "agricultural://dataset/summary",
204
- "name": "Agricultural Dataset Summary",
205
- "description": "Summary of the Kerguéhennec experimental station dataset",
206
- "mimeType": "text/plain"
207
- }
208
- ]
209
- }
210
- }
211
-
212
- else:
213
- return {
214
- "jsonrpc": "2.0",
215
- "id": request_id,
216
- "error": {
217
- "code": -32601,
218
- "message": "Method not found",
219
- "data": f"Unknown method: {method}"
220
- }
221
- }
222
 
223
- # ========= Endpoint MCP (conforme JSON-RPC 2.0) =========
224
- @api.post("/mcp")
225
- async def mcp_endpoint(request: Request, authorization: str | None = Header(None)):
226
- _check_auth(authorization)
227
  try:
228
- payload = await request.json()
229
- except Exception:
230
- return JSONResponse(
231
- status_code=400,
232
- content={
233
- "jsonrpc": "2.0",
234
- "id": None,
235
- "error": {
236
- "code": -32700,
237
- "message": "Parse error",
238
- "data": "Invalid JSON"
239
- }
240
- }
241
- )
242
-
243
- response = handle_mcp_request(payload)
244
- return JSONResponse(response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
- # ========= UI Gradio =========
247
- # Use your existing comprehensive agricultural analysis interface
248
- demo = create_gradio_app()
249
 
250
- # Monte Gradio sous la racine "/"
251
- app = mount_gradio_app(api, demo, path="/")
 
252
 
253
- # ========= Entrée (pour exécution locale éventuelle) =========
254
  if __name__ == "__main__":
255
- # En local uniquement ; sur Spaces, le runner est géré par la plateforme.
256
  import uvicorn
257
- uvicorn.run(app, host="0.0.0.0", port=PORT)
258
 
259
- # ========= Tests curl (exemples conformes JSON-RPC 2.0) =========
260
- # Healthcheck (public, GET)
261
  # curl -s https://hackathoncra-mcp.hf.space/health
262
 
263
- # Test d'initialisation MCP
264
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
265
  # -H "Content-Type: application/json" \
266
  # -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}}}'
267
 
268
- # Liste des outils disponibles
269
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
270
  # -H "Content-Type: application/json" \
271
  # -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}'
272
 
273
- # Appel d'outil - Analyse pression adventices
274
- # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
275
- # -H "Content-Type: application/json" \
276
- # -d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "analyze_weed_pressure", "arguments": {"years": [2020, 2021, 2022], "plots": ["P1", "P2"]}}}'
277
-
278
- # Appel d'outil - Prédiction future
279
- # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
280
- # -H "Content-Type: application/json" \
281
- # -d '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "predict_future_pressure", "arguments": {"target_years": [2025, 2026], "max_ift": 1.0}}}'
282
-
283
- # Appel d'outil - Analyse rotation
284
- # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
285
- # -H "Content-Type: application/json" \
286
- # -d '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "analyze_crop_rotation", "arguments": {}}}'
287
-
288
- # Liste des ressources
289
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
290
  # -H "Content-Type: application/json" \
291
- # -d '{"jsonrpc": "2.0", "id": 6, "method": "resources/list", "params": {}}'
292
 
293
- # Avec authentification Bearer (si MCP_BEARER défini)
294
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
295
- # -H "Authorization: Bearer VOTRE_TOKEN" \
296
  # -H "Content-Type: application/json" \
297
- # -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}}}'
 
1
  import os
2
+ from contextlib import asynccontextmanager
3
+ from fastapi import FastAPI
4
+ from fastmcp import FastMCP
 
5
  from gradio.routes import mount_gradio_app
6
 
7
+ # Import your existing Gradio app and analysis tools
8
  from gradio_app import create_gradio_app
9
+ from data_loader import AgriculturalDataLoader
10
+ from analysis_tools import AgriculturalAnalyzer
11
 
12
  # ========= Configuration =========
13
  PORT = int(os.environ.get("PORT", 7860))
 
14
 
15
+ # Initialize agricultural components
16
+ data_loader = AgriculturalDataLoader()
17
+ analyzer = AgriculturalAnalyzer(data_loader)
18
 
19
+ # ========= Create FastMCP Server =========
20
+ mcp = FastMCP("Agricultural Analysis Tools")
 
21
 
22
+ @mcp.tool
23
+ def analyze_weed_pressure(years: list[int] | None = None, plots: list[str] | None = None) -> str:
24
+ """Analyze weed pressure trends using IFT herbicide data from Kerguéhennec experimental station."""
25
+ try:
26
+ trends = analyzer.analyze_weed_pressure_trends(years=years, plots=plots)
27
+ summary_stats = trends['summary']
28
+
29
+ result = f"""🌿 ANALYSE DE LA PRESSION ADVENTICES (IFT Herbicides)
30
+
31
+ 📊 Statistiques pour les années {years or 'toutes'} et parcelles {plots or 'toutes'}:
32
+ • IFT moyen: {summary_stats['mean_ift']:.2f}
33
+ • Écart-type: {summary_stats['std_ift']:.2f}
34
+ • IFT minimum: {summary_stats['min_ift']:.2f}
35
+ • IFT maximum: {summary_stats['max_ift']:.2f}
36
+ • Total applications: {summary_stats['total_applications']}
37
+ • Parcelles analysées: {summary_stats['unique_plots']}
38
+ • Cultures analysées: {summary_stats['unique_crops']}
39
+
40
+ 💡 Interprétation:
41
+ • IFT < 1.0: Pression faible (adapté aux cultures sensibles)
42
+ • IFT 1.0-2.0: Pression modérée
43
+ • IFT > 2.0: Pression élevée"""
44
+ return result
45
+ except Exception as e:
46
+ return f"❌ Erreur lors de l'analyse: {str(e)}"
47
+
48
+ @mcp.tool
49
+ def predict_future_pressure(target_years: list[int] | None = None, max_ift: float = 1.0) -> str:
50
+ """Predict future weed pressure and identify suitable plots for sensitive crops."""
51
+ try:
52
+ year_list = target_years or [2025, 2026, 2027]
53
+ predictions = analyzer.predict_weed_pressure(target_years=year_list)
54
+ model_perf = predictions['model_performance']
55
+
56
+ result = f"""🔮 PRÉDICTION DE LA PRESSION ADVENTICES
57
+
58
+ 🤖 Performance du modèle:
59
+ • R² Score: {model_perf['r2']:.3f}
60
+ • Erreur quadratique moyenne: {model_perf['mse']:.3f}
61
 
62
+ 📈 Prédictions par année:
63
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ for year in year_list:
66
+ if year in predictions['predictions']:
67
+ year_pred = predictions['predictions'][year]
68
+ result += f"\n📅 {year}:\n"
69
+ for _, row in year_pred.iterrows():
70
+ result += f"• {row['plot_name']}: IFT {row['predicted_ift']:.2f} (Risque: {row['risk_level']})\n"
 
 
 
 
 
 
 
71
 
72
+ suitable_plots = analyzer.identify_suitable_plots_for_sensitive_crops(
73
+ target_years=year_list, max_ift_threshold=max_ift
74
+ )
 
 
 
 
 
 
 
 
 
 
75
 
76
+ result += f"\n🌱 Parcelles adaptées aux cultures sensibles (IFT < {max_ift}):\n"
77
+ for year, plots in suitable_plots.items():
78
+ if plots:
79
+ result += f"• {year}: {', '.join(plots)}\n"
80
+ else:
81
+ result += f"• {year}: Aucune parcelle adaptée\n"
 
 
 
 
 
 
 
82
 
83
+ return result
84
+ except Exception as e:
85
+ return f"❌ Erreur lors de la prédiction: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ @mcp.tool
88
+ def analyze_crop_rotation() -> str:
89
+ """Analyze the impact of crop rotations on weed pressure at Kerguéhennec station."""
 
90
  try:
91
+ rotation_impact = analyzer.analyze_crop_rotation_impact()
92
+
93
+ if rotation_impact.empty:
94
+ return "📊 Pas assez de données pour analyser les rotations"
95
+
96
+ result = "🔄 IMPACT DES ROTATIONS CULTURALES\n\n🏆 Meilleures rotations (IFT moyen le plus bas):\n\n"
97
+
98
+ best_rotations = rotation_impact.head(10)
99
+ for i, (_, row) in enumerate(best_rotations.iterrows(), 1):
100
+ result += f"{i}. **{row['rotation_type']}**\n"
101
+ result += f" • IFT moyen: {row['mean_ift']:.2f}\n"
102
+ result += f" • Écart-type: {row['std_ift']:.2f}\n"
103
+ result += f" • Observations: {row['count']}\n\n"
104
+
105
+ result += "💡 Les rotations avec les IFT les plus bas sont généralement plus durables."
106
+ return result
107
+ except Exception as e:
108
+ return f"❌ Erreur lors de l'analyse des rotations: {str(e)}"
109
+
110
+ @mcp.tool
111
+ def get_dataset_summary() -> str:
112
+ """Get a comprehensive summary of the agricultural dataset from Kerguéhennec experimental station."""
113
+ try:
114
+ df = data_loader.load_all_files()
115
+ if df.empty:
116
+ return "❌ Aucune donnée disponible"
117
+
118
+ summary = f"""📊 RÉSUMÉ DU DATASET AGRICOLE - STATION DE KERGUÉHENNEC
119
+
120
+ 📈 Statistiques générales:
121
+ • Total d'enregistrements: {len(df):,}
122
+ • Parcelles uniques: {df['plot_name'].nunique()}
123
+ • Types de cultures: {df['crop_type'].nunique()}
124
+ • Années couvertes: {', '.join(map(str, sorted(df['year'].unique())))}
125
+ • Applications herbicides: {len(df[df['is_herbicide'] == True]):,}
126
+
127
+ 🌱 Top 5 des cultures:
128
+ {df['crop_type'].value_counts().head(5).to_string()}
129
+
130
+ 📍 Top 5 des parcelles:
131
+ {df['plot_name'].value_counts().head(5).to_string()}
132
+
133
+ 🏢 Source: Station Expérimentale de Kerguéhennec"""
134
+ return summary
135
+ except Exception as e:
136
+ return f"❌ Erreur lors du chargement des données: {str(e)}"
137
+
138
+ @mcp.resource("agricultural://dataset/summary")
139
+ def dataset_resource() -> str:
140
+ """Agricultural dataset summary resource."""
141
+ return get_dataset_summary()
142
+
143
+ # ========= Create MCP ASGI app =========
144
+ mcp_app = mcp.http_app(path='/mcp')
145
+
146
+ # ========= FastAPI App with Lifespan =========
147
+ @asynccontextmanager
148
+ async def lifespan(app: FastAPI):
149
+ async with mcp_app.lifespan(app):
150
+ yield
151
+
152
+ app = FastAPI(
153
+ title="Agricultural Analysis - MCP + Gradio",
154
+ description="Agricultural data analysis with MCP tools and Gradio interface",
155
+ version="1.0.0",
156
+ lifespan=lifespan
157
+ )
158
+
159
+ @app.get("/health")
160
+ def health():
161
+ """Health check endpoint."""
162
+ return {"ok": True, "service": "agricultural-mcp-server", "version": "1.0.0"}
163
 
164
+ # ========= Mount MCP Server =========
165
+ app.mount("/mcp", mcp_app)
 
166
 
167
+ # ========= Mount Gradio UI =========
168
+ demo = create_gradio_app()
169
+ gradio_app = mount_gradio_app(app, demo, path="/")
170
 
171
+ # ========= Launch Configuration =========
172
  if __name__ == "__main__":
 
173
  import uvicorn
174
+ uvicorn.run(gradio_app, host="0.0.0.0", port=PORT)
175
 
176
+ # ========= Tests FastMCP (exemples curl) =========
177
+ # Health check
178
  # curl -s https://hackathoncra-mcp.hf.space/health
179
 
180
+ # Test MCP tools avec FastMCP
181
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
182
  # -H "Content-Type: application/json" \
183
  # -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}}}'
184
 
 
185
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
186
  # -H "Content-Type: application/json" \
187
  # -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}'
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
190
  # -H "Content-Type: application/json" \
191
+ # -d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "analyze_weed_pressure", "arguments": {"years": [2020, 2021, 2022]}}}'
192
 
 
193
  # curl -s -X POST https://hackathoncra-mcp.hf.space/mcp \
 
194
  # -H "Content-Type: application/json" \
195
+ # -d '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "get_dataset_summary", "arguments": {}}}'
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  fastapi>=0.112
2
  uvicorn[standard]>=0.30
3
  gradio>=4.43
 
4
  pandas>=2.0.0
5
  numpy>=1.24.0
6
  matplotlib>=3.6.0
 
1
  fastapi>=0.112
2
  uvicorn[standard]>=0.30
3
  gradio>=4.43
4
+ fastmcp>=2.11.0
5
  pandas>=2.0.0
6
  numpy>=1.24.0
7
  matplotlib>=3.6.0