File size: 8,334 Bytes
7ca901a
a764c8f
a507ec3
53bf046
7ca901a
a764c8f
 
 
 
 
 
 
 
a507ec3
7ca901a
a507ec3
 
7ca901a
a764c8f
53bf046
a764c8f
53bf046
a507ec3
 
 
53bf046
a764c8f
 
 
 
 
 
 
 
 
 
 
 
 
 
a507ec3
53bf046
a764c8f
a507ec3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53bf046
a507ec3
 
eb1a639
a507ec3
 
 
 
 
 
eb1a639
a507ec3
 
 
eb1a639
a507ec3
 
 
 
 
 
eb1a639
a507ec3
 
 
eb1a639
a507ec3
 
 
53bf046
a507ec3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a764c8f
 
 
 
 
 
a507ec3
 
a764c8f
 
 
 
a507ec3
 
 
53bf046
a764c8f
 
 
 
a507ec3
53bf046
a764c8f
 
 
 
7ca901a
53bf046
a764c8f
53bf046
a507ec3
a764c8f
eb1a639
 
a764c8f
 
53bf046
a764c8f
53bf046
a764c8f
 
53bf046
a764c8f
53bf046
a764c8f
 
 
53bf046
a764c8f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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": {}}'