mcp / app.py
Tracy André
updated
a764c8f
raw
history blame
8.33 kB
import os
import gradio as gr
from fastapi import FastAPI
from gradio.routes import mount_gradio_app
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
# ==== FastMCP ====
from fastmcp import FastMCP
# Import your existing Gradio app and analysis tools
from gradio_app import create_gradio_app
from data_loader import AgriculturalDataLoader
from analysis_tools import AgriculturalAnalyzer
# --------- Config ---------
PORT = int(os.environ.get("PORT", 7860))
MCP_BEARER = os.getenv("MCP_BEARER", "") # ajouter dans Settings > Variables & secrets si besoin
# Initialize agricultural components
data_loader = AgriculturalDataLoader()
analyzer = AgriculturalAnalyzer(data_loader)
# --------- Auth middleware (optionnel) ---------
class BearerAuthMiddleware(BaseHTTPMiddleware):
def __init__(self, app, token: str | None):
super().__init__(app)
self.token = token
async def dispatch(self, request: Request, call_next):
if self.token:
auth = request.headers.get("authorization", "")
if not auth.startswith("Bearer ") or auth.split(" ", 1)[1] != self.token:
return JSONResponse({"detail": "Unauthorized"}, status_code=401)
return await call_next(request)
# --------- Déclare le serveur MCP ---------
mcp = FastMCP("Agricultural Analysis Tools")
# --------- Outils MCP agricoles ---------
@mcp.tool
def analyze_weed_pressure(years: list[int] | None = None, plots: list[str] | None = None) -> str:
"""Analyze weed pressure trends using IFT herbicide data from Kerguéhennec experimental station."""
try:
trends = analyzer.analyze_weed_pressure_trends(years=years, plots=plots)
summary_stats = trends['summary']
result = f"""🌿 ANALYSE DE LA PRESSION ADVENTICES (IFT Herbicides)
📊 Statistiques pour les années {years or 'toutes'} et parcelles {plots or 'toutes'}:
• IFT moyen: {summary_stats['mean_ift']:.2f}
• Écart-type: {summary_stats['std_ift']:.2f}
• IFT minimum: {summary_stats['min_ift']:.2f}
• IFT maximum: {summary_stats['max_ift']:.2f}
• Total applications: {summary_stats['total_applications']}
• Parcelles analysées: {summary_stats['unique_plots']}
• Cultures analysées: {summary_stats['unique_crops']}
💡 Interprétation:
• IFT < 1.0: Pression faible (adapté aux cultures sensibles)
• IFT 1.0-2.0: Pression modérée
• IFT > 2.0: Pression élevée"""
return result
except Exception as e:
return f"❌ Erreur lors de l'analyse: {str(e)}"
@mcp.tool
def predict_future_pressure(target_years: list[int] | None = None, max_ift: float = 1.0) -> str:
"""Predict future weed pressure and identify suitable plots for sensitive crops."""
try:
year_list = target_years or [2025, 2026, 2027]
predictions = analyzer.predict_weed_pressure(target_years=year_list)
model_perf = predictions['model_performance']
result = f"""🔮 PRÉDICTION DE LA PRESSION ADVENTICES
🤖 Performance du modèle:
• R² Score: {model_perf['r2']:.3f}
• Erreur quadratique moyenne: {model_perf['mse']:.3f}
📈 Prédictions par année:
"""
for year in year_list:
if year in predictions['predictions']:
year_pred = predictions['predictions'][year]
result += f"\n📅 {year}:\n"
for _, row in year_pred.iterrows():
result += f"• {row['plot_name']}: IFT {row['predicted_ift']:.2f} (Risque: {row['risk_level']})\n"
suitable_plots = analyzer.identify_suitable_plots_for_sensitive_crops(
target_years=year_list, max_ift_threshold=max_ift
)
result += f"\n🌱 Parcelles adaptées aux cultures sensibles (IFT < {max_ift}):\n"
for year, plots in suitable_plots.items():
if plots:
result += f"• {year}: {', '.join(plots)}\n"
else:
result += f"• {year}: Aucune parcelle adaptée\n"
return result
except Exception as e:
return f"❌ Erreur lors de la prédiction: {str(e)}"
@mcp.tool
def analyze_crop_rotation() -> str:
"""Analyze the impact of crop rotations on weed pressure at Kerguéhennec station."""
try:
rotation_impact = analyzer.analyze_crop_rotation_impact()
if rotation_impact.empty:
return "📊 Pas assez de données pour analyser les rotations"
result = "🔄 IMPACT DES ROTATIONS CULTURALES\n\n🏆 Meilleures rotations (IFT moyen le plus bas):\n\n"
best_rotations = rotation_impact.head(10)
for i, (_, row) in enumerate(best_rotations.iterrows(), 1):
result += f"{i}. **{row['rotation_type']}**\n"
result += f" • IFT moyen: {row['mean_ift']:.2f}\n"
result += f" • Écart-type: {row['std_ift']:.2f}\n"
result += f" • Observations: {row['count']}\n\n"
result += "💡 Les rotations avec les IFT les plus bas sont généralement plus durables."
return result
except Exception as e:
return f"❌ Erreur lors de l'analyse des rotations: {str(e)}"
@mcp.tool
def get_dataset_summary() -> str:
"""Get a comprehensive summary of the agricultural dataset from Kerguéhennec experimental station."""
try:
df = data_loader.load_all_files()
if df.empty:
return "❌ Aucune donnée disponible"
summary = f"""📊 RÉSUMÉ DU DATASET AGRICOLE - STATION DE KERGUÉHENNEC
📈 Statistiques générales:
• Total d'enregistrements: {len(df):,}
• Parcelles uniques: {df['plot_name'].nunique()}
• Types de cultures: {df['crop_type'].nunique()}
• Années couvertes: {', '.join(map(str, sorted(df['year'].unique())))}
• Applications herbicides: {len(df[df['is_herbicide'] == True]):,}
🌱 Top 5 des cultures:
{df['crop_type'].value_counts().head(5).to_string()}
📍 Top 5 des parcelles:
{df['plot_name'].value_counts().head(5).to_string()}
🏢 Source: Station Expérimentale de Kerguéhennec"""
return summary
except Exception as e:
return f"❌ Erreur lors du chargement des données: {str(e)}"
@mcp.resource("agricultural://dataset/summary")
def dataset_resource() -> str:
"""Agricultural dataset summary resource."""
return get_dataset_summary()
# Crée l'app ASGI FastMCP et protège-la éventuellement
mcp_app = mcp.http_app( # cf. doc: http_app() retourne une app ASGI montable
path="/", # endpoint interne de l'app MCP => "/"
custom_middleware=[
Middleware(BearerAuthMiddleware, token=MCP_BEARER)
] if MCP_BEARER else []
)
# --------- App FastAPI parente (lifespan important) ---------
app = FastAPI(title="Agricultural Analysis - Gradio + FastMCP", lifespan=mcp_app.lifespan)
# Health simple
@app.get("/health")
def health():
return {"ok": True, "service": "agricultural-mcp-server", "version": "1.0.0"}
# Monte MCP sous /mcp/ (final: /mcp/…)
app.mount("/mcp", mcp_app)
# --------- UI Gradio (utilise ton interface existante) ---------
demo = create_gradio_app()
# Monte Gradio à la racine
app = mount_gradio_app(app, demo, path="/")
# --------- Entrée locale facultative ---------
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=PORT)
# ========= Tests FastMCP (exemples curl) =========
# Health
# curl -s https://hackathoncra-mcp.hf.space/health
# MCP tools list
# curl -s -X POST https://hackathoncra-mcp.hf.space/mcp/ \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}'
# MCP tool call
# curl -s -X POST https://hackathoncra-mcp.hf.space/mcp/ \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "get_dataset_summary", "arguments": {}}}'
# MCP avec Bearer (si MCP_BEARER défini)
# curl -s -X POST https://hackathoncra-mcp.hf.space/mcp/ \
# -H "Authorization: Bearer VOTRE_TOKEN" \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}'