Tracy André commited on
Commit
2ce9eab
·
1 Parent(s): 8247476
agricultural_mcp/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agricultural MCP Module for Weed Pressure Analysis
3
+ """
4
+
5
+ from .tools import WeedPressureAnalyzer
6
+ from .resources import AgriculturalResources
7
+
8
+ __all__ = ['WeedPressureAnalyzer', 'AgriculturalResources']
agricultural_mcp/prompts.py ADDED
File without changes
agricultural_mcp/resources.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from data_loader import AgriculturalDataLoader
3
+
4
+
5
+ class AgriculturalResources:
6
+ def __init__(self):
7
+ self.data_loader = AgriculturalDataLoader()
8
+ self.data_cache = None
9
+
10
+ def load_data(self):
11
+ if self.data_cache is None:
12
+ self.data_cache = self.data_loader.load_all_files()
13
+ return self.data_cache
14
+
15
+ # ===========================
16
+ # Ressource : Exploitation
17
+ # ===========================
18
+ @gr.mcp.resource("exploitation://{siret}")
19
+ def get_exploitation(self, siret: str) -> dict:
20
+ """Retourne les infos d'une exploitation à partir du SIRET"""
21
+ df = self.load_data()
22
+ exp = df[df["siret"] == siret].iloc[0]
23
+ return {
24
+ "siret": exp["siret"],
25
+ "raison_sociale": exp["raisonsoci"],
26
+ "pacage": exp["pacage"],
27
+ "millesime": exp["millesime"],
28
+ }
29
+
30
+ # ===========================
31
+ # Ressource : Parcelle
32
+ # ===========================
33
+ @gr.mcp.resource("parcelle://{numparcell}")
34
+ def get_parcelle(self, numparcell: str) -> dict:
35
+ """Retourne les infos d'une parcelle"""
36
+ df = self.load_data()
37
+ parc = df[df["numparcell"] == numparcell].iloc[0]
38
+ return {
39
+ "numparcell": parc["numparcell"],
40
+ "nomparc": parc["nomparc"],
41
+ "surfparc": parc["surfparc"],
42
+ "numilot": parc["numilot"],
43
+ "refca": parc["refca"],
44
+ }
45
+
46
+ # ===========================
47
+ # Ressource : Intervention
48
+ # ===========================
49
+ @gr.mcp.resource("intervention://{rang}")
50
+ def get_intervention(self, rang: str) -> dict:
51
+ """Retourne les infos d'une intervention"""
52
+ df = self.load_data()
53
+ inter = df[df["rang"] == rang].iloc[0]
54
+ return {
55
+ "rang": inter["rang"],
56
+ "date": inter.get("dateinterv", None),
57
+ "mainoeuvre": inter["mainoeuvre"],
58
+ "materiel": inter["materiel"],
59
+ }
60
+
61
+ # ===========================
62
+ # Ressource : Intrants
63
+ # ===========================
64
+ @gr.mcp.resource("intrant://{codeamm}")
65
+ def get_intrant(self, codeamm: str) -> dict:
66
+ """Retourne les infos d’un produit utilisé"""
67
+ df = self.load_data()
68
+ intr = df[df["codeamm"] == codeamm].iloc[0]
69
+ return {
70
+ "codeamm": intr["codeamm"],
71
+ "codegnis": intr["codegnis"],
72
+ "quantite": intr["kqte"],
73
+ "teneurN": intr["teneurn"],
74
+ "teneurP": intr["teneurp"],
75
+ "teneurK": intr["teneurk"],
76
+ }
77
+
78
+ # ===========================
79
+ # Ressource : Matériel
80
+ # ===========================
81
+ @gr.mcp.resource("materiel://{id}")
82
+ def get_materiel(self, id: str) -> dict:
83
+ """Retourne un matériel (ligne correspondante)"""
84
+ df = self.load_data()
85
+ mat = df[df.index == int(id)].iloc[0] # ici on prend par index de ligne
86
+ return {
87
+ "materiel": mat["materiel"],
88
+ "intervention": mat["rang"],
89
+ "parcelle": mat["numparcell"],
90
+ }
91
+
92
+
93
+ # ===========================
94
+ # Instance pour utilisation
95
+ # ===========================
96
+ resources = AgriculturalResources()
agricultural_mcp/tools.py ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MCP Server for Agricultural Weed Pressure Analysis"""
2
+
3
+ import gradio as gr
4
+ import pandas as pd
5
+ import numpy as np
6
+ import plotly.express as px
7
+ from data_loader import AgriculturalDataLoader
8
+ import warnings
9
+ warnings.filterwarnings('ignore')
10
+
11
+ class WeedPressureAnalyzer:
12
+ """Analyze weed pressure and recommend plots for sensitive crops."""
13
+
14
+ def __init__(self):
15
+ self.data_loader = AgriculturalDataLoader()
16
+ self.data_cache = None
17
+
18
+ def load_data(self):
19
+ if self.data_cache is None:
20
+ self.data_cache = self.data_loader.load_all_files()
21
+ return self.data_cache
22
+
23
+ def calculate_herbicide_ift(self, years=None):
24
+ """Calculate IFT for herbicides by plot and year."""
25
+ df = self.load_data()
26
+
27
+ if years:
28
+ df = df[df['year'].isin(years)]
29
+
30
+ herbicide_df = df[df['is_herbicide'] == True].copy()
31
+
32
+ if len(herbicide_df) == 0:
33
+ return pd.DataFrame()
34
+
35
+ ift_summary = herbicide_df.groupby(['plot_name', 'year', 'crop_type']).agg({
36
+ 'produit': 'count',
37
+ 'plot_surface': 'first',
38
+ 'quantitetot': 'sum'
39
+ }).reset_index()
40
+
41
+ ift_summary['ift_herbicide'] = ift_summary['produit'] / ift_summary['plot_surface']
42
+
43
+ return ift_summary
44
+
45
+ def predict_weed_pressure(self, target_years=[2025, 2026, 2027]):
46
+ """Predict weed pressure for future years."""
47
+ ift_data = self.calculate_herbicide_ift()
48
+
49
+ if len(ift_data) == 0:
50
+ return pd.DataFrame()
51
+
52
+ predictions = []
53
+
54
+ for plot in ift_data['plot_name'].unique():
55
+ plot_data = ift_data[ift_data['plot_name'] == plot].sort_values('year')
56
+
57
+ if len(plot_data) < 2:
58
+ continue
59
+
60
+ years = plot_data['year'].values
61
+ ift_values = plot_data['ift_herbicide'].values
62
+
63
+ if len(years) > 1:
64
+ slope = np.polyfit(years, ift_values, 1)[0]
65
+ intercept = np.polyfit(years, ift_values, 1)[1]
66
+
67
+ for target_year in target_years:
68
+ predicted_ift = slope * target_year + intercept
69
+ predicted_ift = max(0, predicted_ift)
70
+
71
+ if predicted_ift < 1.0:
72
+ risk_level = "Faible"
73
+ elif predicted_ift < 2.0:
74
+ risk_level = "Modéré"
75
+ else:
76
+ risk_level = "Élevé"
77
+
78
+ predictions.append({
79
+ 'plot_name': plot,
80
+ 'year': target_year,
81
+ 'predicted_ift': predicted_ift,
82
+ 'risk_level': risk_level,
83
+ 'recent_crops': ', '.join(plot_data['crop_type'].tail(3).unique()),
84
+ 'historical_avg_ift': plot_data['ift_herbicide'].mean()
85
+ })
86
+
87
+ return pd.DataFrame(predictions)
88
+
89
+ # Initialize analyzer
90
+ analyzer = WeedPressureAnalyzer()
91
+
92
+
93
+
94
+
95
+ def analyze_herbicide_trends(year_start, year_end, plot_filter):
96
+ """
97
+ Analyze herbicide usage trends over time by calculating IFT (Treatment Frequency Index).
98
+
99
+ This tool calculates the IFT (Indice de Fréquence de Traitement) for herbicides, which represents
100
+ the number of herbicide applications per hectare. It provides visualizations and statistics to
101
+ understand weed pressure evolution over time.
102
+
103
+ Args:
104
+ year_start (int): Starting year for analysis (2014-2025)
105
+ year_end (int): Ending year for analysis (2014-2025)
106
+ plot_filter (str): Specific plot name or "Toutes" for all plots
107
+
108
+ Returns:
109
+ tuple: (plotly_figure, markdown_summary)
110
+ - plotly_figure: Interactive line chart showing IFT evolution by plot and year
111
+ - markdown_summary: Detailed statistics including mean/max IFT, risk distribution
112
+ """
113
+ try:
114
+ # Créer la liste des années à partir des deux sliders
115
+ start_year = int(year_start)
116
+ end_year = int(year_end)
117
+
118
+ # S'assurer que start <= end
119
+ if start_year > end_year:
120
+ start_year, end_year = end_year, start_year
121
+
122
+ years = list(range(start_year, end_year + 1))
123
+
124
+ ift_data = analyzer.calculate_herbicide_ift(years=years)
125
+
126
+ if len(ift_data) == 0:
127
+ return None, "Aucune donnée d'herbicides trouvée pour la période sélectionnée."
128
+
129
+ # Filtrage par parcelle si nécessaire
130
+ if plot_filter and plot_filter != "Toutes":
131
+ ift_data = ift_data[ift_data['plot_name'] == plot_filter]
132
+
133
+ if len(ift_data) == 0:
134
+ return None, f"Aucune donnée trouvée pour la parcelle '{plot_filter}' sur la période {years[0]}-{years[-1]}."
135
+
136
+ # Création du graphique
137
+ fig = px.line(ift_data,
138
+ x='year',
139
+ y='ift_herbicide',
140
+ color='plot_name',
141
+ title=f'Évolution de l\'IFT Herbicides ({years[0]}-{years[-1]})',
142
+ labels={'ift_herbicide': 'IFT Herbicides', 'year': 'Année'},
143
+ markers=True)
144
+
145
+ fig.update_layout(
146
+ height=500,
147
+ xaxis_title="Année",
148
+ yaxis_title="IFT Herbicides",
149
+ legend_title="Parcelle"
150
+ )
151
+
152
+ # Ajout d'une ligne de référence IFT = 2.0
153
+ fig.add_hline(y=2.0, line_dash="dash", line_color="red",
154
+ annotation_text="Seuil IFT élevé (2.0)", annotation_position="top right")
155
+ fig.add_hline(y=1.0, line_dash="dash", line_color="orange",
156
+ annotation_text="Seuil IFT modéré (1.0)", annotation_position="bottom right")
157
+
158
+ # Calcul des statistiques
159
+ ift_mean = ift_data['ift_herbicide'].mean()
160
+ ift_max = ift_data['ift_herbicide'].max()
161
+ ift_min = ift_data['ift_herbicide'].min()
162
+ n_plots = ift_data['plot_name'].nunique()
163
+ n_records = len(ift_data)
164
+
165
+ # Classification des niveaux de risque
166
+ low_risk = len(ift_data[ift_data['ift_herbicide'] < 1.0])
167
+ moderate_risk = len(ift_data[(ift_data['ift_herbicide'] >= 1.0) & (ift_data['ift_herbicide'] < 2.0)])
168
+ high_risk = len(ift_data[ift_data['ift_herbicide'] >= 2.0])
169
+
170
+ summary = f"""
171
+ 📊 **Analyse de l'IFT Herbicides ({years[0]}-{years[-1]})**
172
+
173
+ **Période analysée:** {years[0]} à {years[-1]}
174
+ **Parcelle(s):** {plot_filter if plot_filter != "Toutes" else "Toutes les parcelles"}
175
+
176
+ **Statistiques globales:**
177
+ - IFT moyen: {ift_mean:.2f}
178
+ - IFT minimum: {ift_min:.2f}
179
+ - IFT maximum: {ift_max:.2f}
180
+ - Nombre de parcelles: {n_plots}
181
+ - Nombre d'observations: {n_records}
182
+
183
+ **Répartition des niveaux de pression:**
184
+ - 🟢 Faible (IFT < 1.0): {low_risk} observations ({low_risk/n_records*100:.1f}%)
185
+ - 🟡 Modérée (1.0 ≤ IFT < 2.0): {moderate_risk} observations ({moderate_risk/n_records*100:.1f}%)
186
+ - 🔴 Élevée (IFT ≥ 2.0): {high_risk} observations ({high_risk/n_records*100:.1f}%)
187
+
188
+ **Interprétation:**
189
+ - IFT < 1.0: Pression adventices faible ✅
190
+ - 1.0 ≤ IFT < 2.0: Pression adventices modérée ⚠️
191
+ - IFT ≥ 2.0: Pression adventices élevée ❌
192
+ """
193
+
194
+ return fig, summary
195
+
196
+ except Exception as e:
197
+ import traceback
198
+ error_msg = f"Erreur dans l'analyse: {str(e)}\n{traceback.format_exc()}"
199
+ print(error_msg)
200
+ return None, error_msg
201
+
202
+ def predict_future_weed_pressure():
203
+ """
204
+ Predict weed pressure for the next 3 years (2025-2027) using linear regression on historical IFT data.
205
+
206
+ This tool uses historical herbicide IFT data to predict future weed pressure. It applies linear
207
+ regression to each plot's IFT evolution over time and extrapolates to 2025-2027. Risk levels are
208
+ classified as: Faible (IFT < 1.0), Modéré (1.0 ≤ IFT < 2.0), Élevé (IFT ≥ 2.0).
209
+
210
+ Prediction Method:
211
+ 1. Calculate historical IFT for each plot/year combination
212
+ 2. Apply linear regression: IFT = slope × year + intercept
213
+ 3. Extrapolate to target years 2025-2027
214
+ 4. Classify risk levels based on predicted IFT values
215
+ 5. Include recent crop history and average historical IFT for context
216
+
217
+ Returns:
218
+ tuple: (plotly_figure, markdown_summary)
219
+ - plotly_figure: Bar chart showing predicted IFT by plot and year with risk color coding
220
+ - markdown_summary: Risk distribution statistics and interpretation
221
+ """
222
+ try:
223
+ predictions = analyzer.predict_weed_pressure()
224
+
225
+ if len(predictions) == 0:
226
+ return None, "Impossible de générer des prédictions."
227
+
228
+ fig = px.bar(predictions,
229
+ x='plot_name',
230
+ y='predicted_ift',
231
+ color='risk_level',
232
+ facet_col='year',
233
+ title='Prédiction Pression Adventices (2025-2027)',
234
+ color_discrete_map={'Faible': 'green', 'Modéré': 'orange', 'Élevé': 'red'})
235
+
236
+ low_risk = len(predictions[predictions['risk_level'] == 'Faible'])
237
+ moderate_risk = len(predictions[predictions['risk_level'] == 'Modéré'])
238
+ high_risk = len(predictions[predictions['risk_level'] == 'Élevé'])
239
+
240
+ summary = f"""
241
+ 🔮 **Prédictions 2025-2027**
242
+
243
+ **Répartition des risques:**
244
+ - ✅ Risque faible: {low_risk} prédictions
245
+ - ⚠️ Risque modéré: {moderate_risk} prédictions
246
+ - ❌ Risque élevé: {high_risk} prédictions
247
+ """
248
+
249
+ return fig, summary
250
+
251
+ except Exception as e:
252
+ return None, f"Erreur: {str(e)}"
253
+
254
+ def recommend_sensitive_crop_plots():
255
+ """
256
+ Recommend plots suitable for sensitive crops (pois, haricot) based on predicted weed pressure.
257
+
258
+ This tool identifies plots with low predicted weed pressure (IFT < 1.0) and calculates a
259
+ recommendation score to rank them for sensitive crop cultivation.
260
+
261
+ Recommendation Method:
262
+ 1. Get predicted IFT for 2025-2027 from predict_future_weed_pressure()
263
+ 2. Filter plots with risk_level = "Faible" (IFT < 1.0)
264
+ 3. Calculate recommendation_score = 100 - (predicted_ift × 30)
265
+ 4. Sort plots by recommendation score (higher = better)
266
+ 5. Include recent crop history and historical average IFT for context
267
+
268
+ Recommendation Score:
269
+ - 100-70: Excellent for sensitive crops
270
+ - 70-50: Good for sensitive crops with monitoring
271
+ - 50-0: Requires careful management
272
+
273
+ Returns:
274
+ tuple: (plotly_figure, markdown_summary)
275
+ - plotly_figure: Scatter plot showing predicted IFT vs recommendation score
276
+ - markdown_summary: Top recommended plots with scores and criteria
277
+ """
278
+ try:
279
+ predictions = analyzer.predict_weed_pressure()
280
+
281
+ if len(predictions) == 0:
282
+ return None, "Aucune recommandation disponible."
283
+
284
+ suitable_plots = predictions[predictions['risk_level'] == "Faible"].copy()
285
+
286
+ if len(suitable_plots) > 0:
287
+ suitable_plots['recommendation_score'] = 100 - (suitable_plots['predicted_ift'] * 30)
288
+ suitable_plots = suitable_plots.sort_values('recommendation_score', ascending=False)
289
+
290
+ top_recommendations = suitable_plots.head(10)[['plot_name', 'year', 'predicted_ift', 'recommendation_score']]
291
+
292
+ summary = f"""
293
+ 🌱 **Recommandations Cultures Sensibles**
294
+
295
+ **Top parcelles recommandées:**
296
+ {top_recommendations.to_string(index=False)}
297
+
298
+ **Critères:** IFT prédit < 1.0 (faible pression adventices)
299
+ """
300
+
301
+ fig = px.scatter(suitable_plots,
302
+ x='predicted_ift',
303
+ y='recommendation_score',
304
+ color='year',
305
+ hover_data=['plot_name'],
306
+ title='Parcelles Recommandées pour Cultures Sensibles')
307
+
308
+ return fig, summary
309
+ else:
310
+ return None, "Aucune parcelle à faible risque identifiée."
311
+
312
+ except Exception as e:
313
+ return None, f"Erreur: {str(e)}"
314
+
315
+ def explore_raw_data(year_start, year_end, plot_filter, crop_filter, intervention_filter):
316
+ """
317
+ Explore raw agricultural intervention data with filtering capabilities.
318
+
319
+ This tool provides access to the raw dataset from the Station Expérimentale de Kerguéhennec
320
+ (2014-2025) with filtering options to explore specific subsets of data.
321
+
322
+ Args:
323
+ year_start (int): Starting year for filtering (2014-2025)
324
+ year_end (int): Ending year for filtering (2014-2025)
325
+ plot_filter (str): Specific plot name or "Toutes" for all plots
326
+ crop_filter (str): Specific crop type or "Toutes" for all crops
327
+ intervention_filter (str): Specific intervention type or "Toutes" for all interventions
328
+
329
+ Returns:
330
+ tuple: (plotly_figure, markdown_summary)
331
+ - plotly_figure: Interactive data table or visualization
332
+ - markdown_summary: Data summary with statistics and filtering info
333
+ """
334
+ try:
335
+ # Charger les données
336
+ df = analyzer.load_data()
337
+
338
+ # Appliquer les filtres
339
+ if year_start and year_end:
340
+ df = df[(df['year'] >= year_start) & (df['year'] <= year_end)]
341
+
342
+ if plot_filter and plot_filter != "Toutes":
343
+ df = df[df['plot_name'] == plot_filter]
344
+
345
+ if crop_filter and crop_filter != "Toutes":
346
+ df = df[df['crop_type'] == crop_filter]
347
+
348
+ if intervention_filter and intervention_filter != "Toutes":
349
+ df = df[df['intervention_type'] == intervention_filter]
350
+
351
+ if len(df) == 0:
352
+ return None, "Aucune donnée trouvée avec les filtres sélectionnés."
353
+
354
+ # Créer un résumé des données
355
+ summary = f"""
356
+ 📊 **Exploration des Données Brutes**
357
+
358
+ **Filtres appliqués:**
359
+ - Période: {year_start}-{year_end}
360
+ - Parcelle: {plot_filter}
361
+ - Culture: {crop_filter}
362
+ - Type d'intervention: {intervention_filter}
363
+
364
+ **Statistiques:**
365
+ - Nombre total d'enregistrements: {len(df):,}
366
+ - Nombre de parcelles: {df['plot_name'].nunique()}
367
+ - Nombre d'années: {df['year'].nunique()}
368
+ - Types de cultures: {df['crop_type'].nunique()}
369
+ - Types d'interventions: {df['intervention_type'].nunique()}
370
+
371
+ **Répartition par année:**
372
+ {df['year'].value_counts().sort_index().to_string()}
373
+
374
+ **Top 10 parcelles:**
375
+ {df['plot_name'].value_counts().head(10).to_string()}
376
+
377
+ **Top 10 cultures:**
378
+ {df['crop_type'].value_counts().head(10).to_string()}
379
+
380
+ **Top 10 interventions:**
381
+ {df['intervention_type'].value_counts().head(10).to_string()}
382
+ """
383
+
384
+ # Créer une visualisation des données
385
+ if len(df) > 0:
386
+ # Graphique des interventions par année
387
+ yearly_counts = df.groupby('year').size().reset_index(name='count')
388
+ fig = px.bar(yearly_counts, x='year', y='count',
389
+ title=f'Nombre d\'interventions par année ({year_start}-{year_end})',
390
+ labels={'count': 'Nombre d\'interventions', 'year': 'Année'})
391
+
392
+ fig.update_layout(height=400)
393
+ return fig, summary
394
+ else:
395
+ return None, summary
396
+
397
+ except Exception as e:
398
+ return None, f"Erreur lors de l'exploration des données: {str(e)}"
399
+
400
+ def get_available_plots():
401
+ """Get available plots."""
402
+ try:
403
+ df = analyzer.load_data()
404
+ plots = sorted(df['plot_name'].dropna().unique().tolist())
405
+ return ["Toutes"] + plots
406
+ except Exception as e:
407
+ print(f"Erreur lors du chargement des parcelles: {e}")
408
+ return ["Toutes", "Champ ferme Bas", "Etang Milieu", "Lann Chebot"]
409
+
410
+ def get_available_crops():
411
+ """Get available crop types."""
412
+ try:
413
+ df = analyzer.load_data()
414
+ crops = sorted(df['crop_type'].dropna().unique().tolist())
415
+ return ["Toutes"] + crops
416
+ except Exception as e:
417
+ print(f"Erreur lors du chargement des cultures: {e}")
418
+ return ["Toutes", "blé tendre hiver", "pois de conserve", "haricot mange-tout industrie"]
419
+
420
+ def get_available_interventions():
421
+ """Get available intervention types."""
422
+ try:
423
+ df = analyzer.load_data()
424
+ interventions = sorted(df['intervention_type'].dropna().unique().tolist())
425
+ return ["Toutes"] + interventions
426
+ except Exception as e:
427
+ print(f"Erreur lors du chargement des interventions: {e}")
428
+ return ["Toutes", "Traitement et protection des cultures", "Fertilisation", "Travail et Entretien du sol"]
429
+
430
+ # Create Gradio Interface
431
+ def create_mcp_interface():
432
+ with gr.Blocks(title="🚜 Analyse Pression Adventices", theme=gr.themes.Soft()) as demo:
433
+ gr.Markdown("""
434
+ # 🚜 Analyse Pression Adventices - CRA Bretagne
435
+
436
+ Anticiper et réduire la pression des adventices pour optimiser les cultures sensibles (pois, haricot).
437
+ """)
438
+
439
+ with gr.Tabs():
440
+ with gr.Tab("📈 Analyse Tendances"):
441
+ gr.Markdown("### Analyser l'évolution de l'IFT herbicides par parcelle et période")
442
+ gr.Markdown("""
443
+ **Calcul de l'IFT (Indice de Fréquence de Traitement) :**
444
+ - IFT = Nombre d'applications herbicides / Surface de la parcelle
445
+ - Seuils d'interprétation :
446
+ - 🟢 Faible : IFT < 1.0 (pression adventices faible)
447
+ - 🟡 Modéré : 1.0 ≤ IFT < 2.0 (pression modérée)
448
+ - 🔴 Élevé : IFT ≥ 2.0 (pression élevée)
449
+ """)
450
+
451
+ with gr.Row():
452
+ with gr.Column():
453
+ with gr.Row():
454
+ year_start = gr.Slider(
455
+ minimum=2014,
456
+ maximum=2025,
457
+ value=2020,
458
+ step=1,
459
+ label="Année de début"
460
+ )
461
+ year_end = gr.Slider(
462
+ minimum=2014,
463
+ maximum=2025,
464
+ value=2025,
465
+ step=1,
466
+ label="Année de fin"
467
+ )
468
+ plot_dropdown = gr.Dropdown(
469
+ choices=get_available_plots(),
470
+ value="Toutes",
471
+ label="Filtrer par parcelle",
472
+ info="Choisissez une parcelle spécifique ou toutes"
473
+ )
474
+ analyze_btn = gr.Button("🔍 Analyser les Tendances", variant="primary", size="lg")
475
+
476
+ with gr.Row():
477
+ with gr.Column(scale=2):
478
+ trends_plot = gr.Plot(label="Graphique d'évolution")
479
+ with gr.Column(scale=1):
480
+ trends_summary = gr.Markdown(label="Résumé statistique")
481
+
482
+ analyze_btn.click(
483
+ analyze_herbicide_trends,
484
+ inputs=[year_start, year_end, plot_dropdown],
485
+ outputs=[trends_plot, trends_summary]
486
+ )
487
+
488
+ with gr.Tab("🔮 Prédictions"):
489
+ gr.Markdown("### Prédiction de la pression adventices 2025-2027")
490
+ gr.Markdown("""
491
+ **Méthode de prédiction :**
492
+ 1. Calcul de l'IFT historique par parcelle et année
493
+ 2. Régression linéaire : IFT = pente × année + ordonnée_origine
494
+ 3. Extrapolation aux années 2025-2027
495
+ 4. Classification des risques :
496
+ - 🟢 Faible : IFT < 1.0
497
+ - 🟡 Modéré : 1.0 ≤ IFT < 2.0
498
+ - 🔴 Élevé : IFT ≥ 2.0
499
+ """)
500
+
501
+ predict_btn = gr.Button("🎯 Prédire 2025-2027", variant="primary")
502
+
503
+ with gr.Row():
504
+ predictions_plot = gr.Plot()
505
+ predictions_summary = gr.Markdown()
506
+
507
+ predict_btn.click(predict_future_weed_pressure, outputs=[predictions_plot, predictions_summary])
508
+
509
+ with gr.Tab("🌱 Recommandations"):
510
+ gr.Markdown("### Recommandations pour cultures sensibles (pois, haricot)")
511
+ gr.Markdown("""
512
+ **Méthode de recommandation :**
513
+ 1. Prédiction IFT 2025-2027 par régression linéaire
514
+ 2. Filtrage des parcelles à faible risque (IFT < 1.0)
515
+ 3. Calcul du score de recommandation : 100 - (IFT_prédit × 30)
516
+ 4. Classement par score (plus élevé = meilleur)
517
+ """)
518
+
519
+ recommend_btn = gr.Button("🎯 Recommander Parcelles", variant="primary")
520
+
521
+ with gr.Row():
522
+ recommendations_plot = gr.Plot()
523
+ recommendations_summary = gr.Markdown()
524
+
525
+ recommend_btn.click(recommend_sensitive_crop_plots, outputs=[recommendations_plot, recommendations_summary])
526
+
527
+ with gr.Tab("📊 Exploration Données"):
528
+ gr.Markdown("### Explorer les données brutes de la Station Expérimentale de Kerguéhennec")
529
+
530
+ with gr.Row():
531
+ with gr.Column():
532
+ data_year_start = gr.Slider(
533
+ minimum=2014,
534
+ maximum=2025,
535
+ value=2020,
536
+ step=1,
537
+ label="Année de début"
538
+ )
539
+ data_year_end = gr.Slider(
540
+ minimum=2014,
541
+ maximum=2025,
542
+ value=2025,
543
+ step=1,
544
+ label="Année de fin"
545
+ )
546
+ data_plot_filter = gr.Dropdown(
547
+ choices=get_available_plots(),
548
+ value="Toutes",
549
+ label="Filtrer par parcelle"
550
+ )
551
+ data_crop_filter = gr.Dropdown(
552
+ choices=get_available_crops(),
553
+ value="Toutes",
554
+ label="Filtrer par culture"
555
+ )
556
+ data_intervention_filter = gr.Dropdown(
557
+ choices=get_available_interventions(),
558
+ value="Toutes",
559
+ label="Filtrer par type d'intervention"
560
+ )
561
+ explore_btn = gr.Button("🔍 Explorer les Données", variant="primary")
562
+
563
+ with gr.Row():
564
+ data_plot = gr.Plot()
565
+ data_summary = gr.Markdown()
566
+
567
+ explore_btn.click(
568
+ explore_raw_data,
569
+ inputs=[data_year_start, data_year_end, data_plot_filter, data_crop_filter, data_intervention_filter],
570
+ outputs=[data_plot, data_summary]
571
+ )
572
+
573
+
574
+ return demo
575
+
576
+
577
+
app.py CHANGED
@@ -12,4 +12,4 @@ if hf_token:
12
  os.environ["DATASET_ID"] = "HackathonCRA/2024"
13
 
14
  demo = create_mcp_interface()
15
- demo.launch(share=True)
 
12
  os.environ["DATASET_ID"] = "HackathonCRA/2024"
13
 
14
  demo = create_mcp_interface()
15
+ demo.launch(share=True, mcp_server=True)
mcp/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Module for Agricultural Weed Pressure Analysis
3
+ """
4
+
5
+ from .tools import WeedPressureAnalyzer
6
+ from .resources import AgriculturalResources
7
+
8
+ __all__ = ['WeedPressureAnalyzer', 'AgriculturalResources']
mcp/resources.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import typing as t
2
+ import pandas as pd
3
+ import gradio as gr
4
+
5
+ # -------------------------------------------------------------------
6
+ # Hypothèse: AgriculturalDataLoader.load_all_files() concatène 10 CSV
7
+ # et renvoie un DataFrame avec au moins ces colonnes (si présentes):
8
+ # ["millesime","raisonsoci","siret","pacage","refca","numilot","numparcell",
9
+ # "nomparc","surfparc","rang","kqte","teneurn","teneurp","teneurk",
10
+ # "keq","volumebo","codeamm","codegnis","materiel","mainoeuvre", ...]
11
+ # -------------------------------------------------------------------
12
+
13
+ class AgriculturalResources:
14
+ def __init__(self):
15
+ self.data_loader = AgriculturalDataLoader()
16
+ self.data_cache: t.Optional[pd.DataFrame] = None
17
+
18
+ def load_data(self) -> pd.DataFrame:
19
+ if self.data_cache is None:
20
+ df = self.data_loader.load_all_files()
21
+
22
+ # Normalisation minimale & robustesse
23
+ df = df.copy()
24
+ # Harmonise noms connus (au cas où)
25
+ rename_map = {
26
+ "raisonsoci": "raisonsociale",
27
+ "numparcelle": "numparcell",
28
+ "NomParc": "nomparc",
29
+ "SurfParc": "surfparc",
30
+ }
31
+ for k, v in rename_map.items():
32
+ if k in df.columns and v not in df.columns:
33
+ df[v] = df[k]
34
+
35
+ # Types & trim
36
+ for col in ["millesime", "siret", "pacage", "refca", "numilot", "numparcell", "rang",
37
+ "codeamm", "codegnis"]:
38
+ if col in df.columns:
39
+ df[col] = df[col].astype(str).str.strip()
40
+
41
+ for col in ["nomparc", "raisonsociale", "materiel", "mainoeuvre"]:
42
+ if col in df.columns:
43
+ df[col] = df[col].astype(str).str.strip()
44
+
45
+ for col in ["surfparc", "kqte", "teneurn", "teneurp", "teneurk", "keq", "volumebo"]:
46
+ if col in df.columns:
47
+ # coerce = NaN si non convertible
48
+ df[col] = pd.to_numeric(df[col], errors="coerce")
49
+
50
+ # IDs composites utiles
51
+ if {"millesime", "numparcell"}.issubset(df.columns):
52
+ df["parcelle_id"] = df["millesime"] + ":" + df["numparcell"]
53
+ else:
54
+ df["parcelle_id"] = None
55
+
56
+ if {"millesime", "numparcell", "rang"}.issubset(df.columns):
57
+ df["intervention_id"] = df["millesime"] + ":" + df["numparcell"] + ":" + df["rang"]
58
+ else:
59
+ df["intervention_id"] = None
60
+
61
+ self.data_cache = df
62
+
63
+ return self.data_cache
64
+
65
+ # -------------------------
66
+ # Utilitaires internes
67
+ # -------------------------
68
+ def _safe_first(self, df: pd.DataFrame) -> t.Optional[pd.Series]:
69
+ if df is None or df.empty:
70
+ return None
71
+ return df.iloc[0]
72
+
73
+ def _notnull(self, d: dict) -> dict:
74
+ # Retire les champs None/NaN pour des payloads plus propres
75
+ return {k: v for k, v in d.items() if pd.notna(v)}
76
+
77
+ # -------------------------
78
+ # LISTINGS / DISCOVERY
79
+ # -------------------------
80
+
81
+ @gr.mcp.resource("dataset://years")
82
+ def list_years(self) -> t.List[str]:
83
+ """Liste des millésimes disponibles dans l'ensemble des fichiers."""
84
+ df = self.load_data()
85
+ if "millesime" not in df.columns:
86
+ return []
87
+ years = sorted(df["millesime"].dropna().astype(str).unique())
88
+ return years
89
+
90
+ @gr.mcp.resource("exploitation://{siret}/parcelles")
91
+ def list_parcelles_by_exploitation(self, siret: str, millesime: t.Optional[str] = None) -> t.List[dict]:
92
+ """Liste les parcelles d'une exploitation (optionnellement filtrées par millésime)."""
93
+ df = self.load_data()
94
+ q = df[df["siret"] == siret] if "siret" in df.columns else df.iloc[0:0]
95
+ if millesime:
96
+ q = q[q["millesime"] == str(millesime)]
97
+ cols = [c for c in ["parcelle_id","millesime","numparcell","nomparc","surfparc","refca","numilot"] if c in q.columns]
98
+ out = q[cols].drop_duplicates().to_dict(orient="records")
99
+ return out
100
+
101
+ @gr.mcp.resource("parcelles://search")
102
+ def search_parcelles(self, query: str = "", millesime: t.Optional[str] = None, limit: int = 50) -> t.List[dict]:
103
+ """Recherche de parcelles par nom/numéro, filtrable par millésime."""
104
+ df = self.load_data()
105
+ q = df
106
+ if millesime:
107
+ q = q[q["millesime"] == str(millesime)]
108
+ if query:
109
+ mask = False
110
+ if "numparcell" in q.columns:
111
+ mask = q["numparcell"].str.contains(query, case=False, na=False)
112
+ if "nomparc" in q.columns:
113
+ mask = mask | q["nomparc"].str.contains(query, case=False, na=False)
114
+ q = q[mask]
115
+ cols = [c for c in ["parcelle_id","millesime","numparcell","nomparc","surfparc","refca","numilot","siret"] if c in q.columns]
116
+ return q[cols].drop_duplicates().head(limit).to_dict(orient="records")
117
+
118
+ # -------------------------
119
+ # RESSOURCES CANONIQUES
120
+ # -------------------------
121
+
122
+ @gr.mcp.resource("exploitation://{siret}/{millesime}")
123
+ def get_exploitation(self, siret: str, millesime: str) -> dict:
124
+ """Infos d'une exploitation (pour un millésime donné)."""
125
+ df = self.load_data()
126
+ q = df[(df["siret"] == siret) & (df["millesime"] == str(millesime))] if {"siret","millesime"}.issubset(df.columns) else df.iloc[0:0]
127
+ row = self._safe_first(q.sort_values(by=[c for c in ["millesime"] if c in q.columns], ascending=False))
128
+ if row is None:
129
+ return {}
130
+ return self._notnull({
131
+ "millesime": row.get("millesime"),
132
+ "siret": row.get("siret"),
133
+ "raison_sociale": row.get("raisonsociale"),
134
+ "pacage": row.get("pacage"),
135
+ })
136
+
137
+ @gr.mcp.resource("parcelle://{millesime}/{numparcell}")
138
+ def get_parcelle(self, millesime: str, numparcell: str) -> dict:
139
+ """Infos d'une parcelle (identifiée par millésime + numparcell)."""
140
+ df = self.load_data()
141
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell))]
142
+ row = self._safe_first(q)
143
+ if row is None:
144
+ return {}
145
+ return self._notnull({
146
+ "parcelle_id": row.get("parcelle_id"),
147
+ "millesime": row.get("millesime"),
148
+ "numparcell": row.get("numparcell"),
149
+ "nomparc": row.get("nomparc"),
150
+ "surfparc": row.get("surfparc"),
151
+ "siret": row.get("siret"),
152
+ "refca": row.get("refca"),
153
+ "numilot": row.get("numilot"),
154
+ })
155
+
156
+ @gr.mcp.resource("intervention://{millesime}/{numparcell}/{rang}")
157
+ def get_intervention(self, millesime: str, numparcell: str, rang: str) -> dict:
158
+ """Infos d'une intervention (clé composite millésime + numparcell + rang)."""
159
+ df = self.load_data()
160
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell)) & (df["rang"] == str(rang))]
161
+ row = self._safe_first(q)
162
+ if row is None:
163
+ return {}
164
+ return self._notnull({
165
+ "intervention_id": row.get("intervention_id"),
166
+ "millesime": row.get("millesime"),
167
+ "numparcell": row.get("numparcell"),
168
+ "rang": row.get("rang"),
169
+ "mainoeuvre": row.get("mainoeuvre"),
170
+ "materiel": row.get("materiel"),
171
+ "codeamm": row.get("codeamm"),
172
+ "codegnis": row.get("codegnis"),
173
+ "kqte": row.get("kqte"),
174
+ "teneurn": row.get("teneurn"),
175
+ "teneurp": row.get("teneurp"),
176
+ "teneurk": row.get("teneurk"),
177
+ "keq": row.get("keq"),
178
+ "volumebo": row.get("volumebo"),
179
+ })
180
+
181
+ @gr.mcp.resource("intrant://{codeamm}")
182
+ def get_intrant(self, codeamm: str, millesime: t.Optional[str] = None) -> dict:
183
+ """Infos d’un intrant (filtrable par millésime)."""
184
+ df = self.load_data()
185
+ q = df[df["codeamm"] == str(codeamm)] if "codeamm" in df.columns else df.iloc[0:0]
186
+ if millesime:
187
+ q = q[q["millesime"] == str(millesime)]
188
+ row = self._safe_first(q)
189
+ if row is None:
190
+ return {}
191
+ return self._notnull({
192
+ "codeamm": row.get("codeamm"),
193
+ "codegnis": row.get("codegnis"),
194
+ "millesime": row.get("millesime"),
195
+ "kqte": row.get("kqte"),
196
+ "teneurn": row.get("teneurn"),
197
+ "teneurp": row.get("teneurp"),
198
+ "teneurk": row.get("teneurk"),
199
+ "keq": row.get("keq"),
200
+ "volumebo": row.get("volumebo"),
201
+ })
202
+
203
+ @gr.mcp.resource("materiel://{millesime}/{numparcell}/{rang}")
204
+ def get_materiel(self, millesime: str, numparcell: str, rang: str) -> dict:
205
+ """Matériel utilisé pour une intervention donnée."""
206
+ df = self.load_data()
207
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell)) & (df["rang"] == str(rang))]
208
+ row = self._safe_first(q)
209
+ if row is None:
210
+ return {}
211
+ return self._notnull({
212
+ "millesime": row.get("millesime"),
213
+ "numparcell": row.get("numparcell"),
214
+ "rang": row.get("rang"),
215
+ "materiel": row.get("materiel"),
216
+ })
217
+
218
+ @gr.mcp.resource("maindoeuvre://{millesime}/{numparcell}/{rang}")
219
+ def get_main_oeuvre(self, millesime: str, numparcell: str, rang: str) -> dict:
220
+ """Main d’œuvre associée à une intervention donnée."""
221
+ df = self.load_data()
222
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell)) & (df["rang"] == str(rang))]
223
+ row = self._safe_first(q)
224
+ if row is None:
225
+ return {}
226
+ return self._notnull({
227
+ "millesime": row.get("millesime"),
228
+ "numparcell": row.get("numparcell"),
229
+ "rang": row.get("rang"),
230
+ "mainoeuvre": row.get("mainoeuvre"),
231
+ })
232
+
233
+ # -------------------------------------------------------------------
234
+ # Gradio: interfaces simples de test (onglets)
235
+ # -------------------------------------------------------------------
236
+
237
+ res = AgriculturalResources()
238
+
239
+ demo = gr.TabbedInterface(
240
+ [
241
+ # Data discovery
242
+ gr.Interface(res.list_years, inputs=[], outputs=gr.JSON(), title="Years"),
243
+ gr.Interface(res.list_parcelles_by_exploitation,
244
+ inputs=[gr.Textbox(value="18560001000016", label="SIRET"),
245
+ gr.Textbox(value="", label="Millesime (optionnel)")],
246
+ outputs=gr.JSON(),
247
+ title="Parcelles par exploitation"),
248
+ gr.Interface(res.search_parcelles,
249
+ inputs=[gr.Textbox(value="", label="Query (num/nom parcelle)"),
250
+ gr.Textbox(value="", label="Millesime (optionnel)"),
251
+ gr.Number(value=50, label="Limit")],
252
+ outputs=gr.JSON(),
253
+ title="Recherche parcelles"),
254
+
255
+ # Resources canoniques
256
+ gr.Interface(res.get_exploitation,
257
+ inputs=[gr.Textbox(value="18560001000016", label="SIRET"),
258
+ gr.Textbox(value="2025", label="Millesime")],
259
+ outputs=gr.JSON(),
260
+ title="Exploitation"),
261
+ gr.Interface(res.get_parcelle,
262
+ inputs=[gr.Textbox(value="2025", label="Millesime"),
263
+ gr.Textbox(value="12", label="Num parcelle")],
264
+ outputs=gr.JSON(),
265
+ title="Parcelle"),
266
+ gr.Interface(res.get_intervention,
267
+ inputs=[gr.Textbox(value="2025", label="Millesime"),
268
+ gr.Textbox(value="12", label="Num parcelle"),
269
+ gr.Textbox(value="1", label="Rang")],
270
+ outputs=gr.JSON(),
271
+ title="Intervention"),
272
+ gr.Interface(res.get_intrant,
273
+ inputs=[gr.Textbox(value="9100296", label="Code AMM"),
274
+ gr.Textbox(value="", label="Millesime (optionnel)")],
275
+ outputs=gr.JSON(),
276
+ title="Intrant"),
277
+ gr.Interface(res.get_materiel,
278
+ inputs=[gr.Textbox(value="2025", label="Millesime"),
279
+ gr.Textbox(value="12", label="Num parcelle"),
280
+ gr.Textbox(value="1", label="Rang")],
281
+ outputs=gr.JSON(),
282
+ title="Matériel"),
283
+ gr.Interface(res.get_main_oeuvre,
284
+ inputs=[gr.Textbox(value="2025", label="Millesime"),
285
+ gr.Textbox(value="12", label="Num parcelle"),
286
+ gr.Textbox(value="1", label="Rang")],
287
+ outputs=gr.JSON(),
288
+ title="Main d'œuvre"),
289
+ ],
290
+ [
291
+ "Years",
292
+ "Parcelles par Exploitation",
293
+ "Recherche Parcelles",
294
+ "Exploitation",
295
+ "Parcelle",
296
+ "Intervention",
297
+ "Intrant",
298
+ "Matériel",
299
+ "Main d'œuvre",
300
+ ]
301
+ )
mcp_server.py CHANGED
@@ -1,538 +1,151 @@
1
- """MCP Server for Agricultural Weed Pressure Analysis"""
 
 
 
2
 
3
  import gradio as gr
4
  import pandas as pd
5
  import numpy as np
6
  import plotly.express as px
7
  from data_loader import AgriculturalDataLoader
 
 
8
  import warnings
9
  warnings.filterwarnings('ignore')
10
 
11
- class WeedPressureAnalyzer:
12
- """Analyze weed pressure and recommend plots for sensitive crops."""
13
-
14
- def __init__(self):
15
- self.data_loader = AgriculturalDataLoader()
16
- self.data_cache = None
17
-
18
- def load_data(self):
19
- if self.data_cache is None:
20
- self.data_cache = self.data_loader.load_all_files()
21
- return self.data_cache
22
-
23
- def calculate_herbicide_ift(self, years=None):
24
- """Calculate IFT for herbicides by plot and year."""
25
- df = self.load_data()
26
-
27
- if years:
28
- df = df[df['year'].isin(years)]
29
-
30
- herbicide_df = df[df['is_herbicide'] == True].copy()
31
-
32
- if len(herbicide_df) == 0:
33
- return pd.DataFrame()
34
-
35
- ift_summary = herbicide_df.groupby(['plot_name', 'year', 'crop_type']).agg({
36
- 'produit': 'count',
37
- 'plot_surface': 'first',
38
- 'quantitetot': 'sum'
39
- }).reset_index()
40
-
41
- ift_summary['ift_herbicide'] = ift_summary['produit'] / ift_summary['plot_surface']
42
-
43
- return ift_summary
44
-
45
- def predict_weed_pressure(self, target_years=[2025, 2026, 2027]):
46
- """Predict weed pressure for future years."""
47
- ift_data = self.calculate_herbicide_ift()
48
-
49
- if len(ift_data) == 0:
50
- return pd.DataFrame()
51
-
52
- predictions = []
53
-
54
- for plot in ift_data['plot_name'].unique():
55
- plot_data = ift_data[ift_data['plot_name'] == plot].sort_values('year')
56
-
57
- if len(plot_data) < 2:
58
- continue
59
-
60
- years = plot_data['year'].values
61
- ift_values = plot_data['ift_herbicide'].values
62
-
63
- if len(years) > 1:
64
- slope = np.polyfit(years, ift_values, 1)[0]
65
- intercept = np.polyfit(years, ift_values, 1)[1]
66
-
67
- for target_year in target_years:
68
- predicted_ift = slope * target_year + intercept
69
- predicted_ift = max(0, predicted_ift)
70
-
71
- if predicted_ift < 1.0:
72
- risk_level = "Faible"
73
- elif predicted_ift < 2.0:
74
- risk_level = "Modéré"
75
- else:
76
- risk_level = "Élevé"
77
-
78
- predictions.append({
79
- 'plot_name': plot,
80
- 'year': target_year,
81
- 'predicted_ift': predicted_ift,
82
- 'risk_level': risk_level,
83
- 'recent_crops': ', '.join(plot_data['crop_type'].tail(3).unique()),
84
- 'historical_avg_ift': plot_data['ift_herbicide'].mean()
85
- })
86
-
87
- return pd.DataFrame(predictions)
88
-
89
- # Initialize analyzer
90
  analyzer = WeedPressureAnalyzer()
 
91
 
92
-
93
-
94
-
95
- def analyze_herbicide_trends(year_start, year_end, plot_filter):
96
- """
97
- Analyze herbicide usage trends over time by calculating IFT (Treatment Frequency Index).
98
-
99
- This tool calculates the IFT (Indice de Fréquence de Traitement) for herbicides, which represents
100
- the number of herbicide applications per hectare. It provides visualizations and statistics to
101
- understand weed pressure evolution over time.
102
-
103
- Args:
104
- year_start (int): Starting year for analysis (2014-2025)
105
- year_end (int): Ending year for analysis (2014-2025)
106
- plot_filter (str): Specific plot name or "Toutes" for all plots
107
-
108
- Returns:
109
- tuple: (plotly_figure, markdown_summary)
110
- - plotly_figure: Interactive line chart showing IFT evolution by plot and year
111
- - markdown_summary: Detailed statistics including mean/max IFT, risk distribution
112
- """
113
- try:
114
- # Créer la liste des années à partir des deux sliders
115
- start_year = int(year_start)
116
- end_year = int(year_end)
117
-
118
- # S'assurer que start <= end
119
- if start_year > end_year:
120
- start_year, end_year = end_year, start_year
121
-
122
- years = list(range(start_year, end_year + 1))
123
-
124
- ift_data = analyzer.calculate_herbicide_ift(years=years)
125
-
126
- if len(ift_data) == 0:
127
- return None, "Aucune donnée d'herbicides trouvée pour la période sélectionnée."
128
-
129
- # Filtrage par parcelle si nécessaire
130
- if plot_filter and plot_filter != "Toutes":
131
- ift_data = ift_data[ift_data['plot_name'] == plot_filter]
132
-
133
- if len(ift_data) == 0:
134
- return None, f"Aucune donnée trouvée pour la parcelle '{plot_filter}' sur la période {years[0]}-{years[-1]}."
135
-
136
- # Création du graphique
137
- fig = px.line(ift_data,
138
- x='year',
139
- y='ift_herbicide',
140
- color='plot_name',
141
- title=f'Évolution de l\'IFT Herbicides ({years[0]}-{years[-1]})',
142
- labels={'ift_herbicide': 'IFT Herbicides', 'year': 'Année'},
143
- markers=True)
144
-
145
- fig.update_layout(
146
- height=500,
147
- xaxis_title="Année",
148
- yaxis_title="IFT Herbicides",
149
- legend_title="Parcelle"
150
- )
151
-
152
- # Ajout d'une ligne de référence IFT = 2.0
153
- fig.add_hline(y=2.0, line_dash="dash", line_color="red",
154
- annotation_text="Seuil IFT élevé (2.0)", annotation_position="top right")
155
- fig.add_hline(y=1.0, line_dash="dash", line_color="orange",
156
- annotation_text="Seuil IFT modéré (1.0)", annotation_position="bottom right")
157
-
158
- # Calcul des statistiques
159
- ift_mean = ift_data['ift_herbicide'].mean()
160
- ift_max = ift_data['ift_herbicide'].max()
161
- ift_min = ift_data['ift_herbicide'].min()
162
- n_plots = ift_data['plot_name'].nunique()
163
- n_records = len(ift_data)
164
-
165
- # Classification des niveaux de risque
166
- low_risk = len(ift_data[ift_data['ift_herbicide'] < 1.0])
167
- moderate_risk = len(ift_data[(ift_data['ift_herbicide'] >= 1.0) & (ift_data['ift_herbicide'] < 2.0)])
168
- high_risk = len(ift_data[ift_data['ift_herbicide'] >= 2.0])
169
-
170
- summary = f"""
171
- 📊 **Analyse de l'IFT Herbicides ({years[0]}-{years[-1]})**
172
-
173
- **Période analysée:** {years[0]} à {years[-1]}
174
- **Parcelle(s):** {plot_filter if plot_filter != "Toutes" else "Toutes les parcelles"}
175
-
176
- **Statistiques globales:**
177
- - IFT moyen: {ift_mean:.2f}
178
- - IFT minimum: {ift_min:.2f}
179
- - IFT maximum: {ift_max:.2f}
180
- - Nombre de parcelles: {n_plots}
181
- - Nombre d'observations: {n_records}
182
-
183
- **Répartition des niveaux de pression:**
184
- - 🟢 Faible (IFT < 1.0): {low_risk} observations ({low_risk/n_records*100:.1f}%)
185
- - 🟡 Modérée (1.0 ≤ IFT < 2.0): {moderate_risk} observations ({moderate_risk/n_records*100:.1f}%)
186
- - 🔴 Élevée (IFT ≥ 2.0): {high_risk} observations ({high_risk/n_records*100:.1f}%)
187
-
188
- **Interprétation:**
189
- - IFT < 1.0: Pression adventices faible ✅
190
- - 1.0 ≤ IFT < 2.0: Pression adventices modérée ⚠️
191
- - IFT ≥ 2.0: Pression adventices élevée ❌
192
- """
193
-
194
- return fig, summary
195
-
196
- except Exception as e:
197
- import traceback
198
- error_msg = f"Erreur dans l'analyse: {str(e)}\n{traceback.format_exc()}"
199
- print(error_msg)
200
- return None, error_msg
201
-
202
- def predict_future_weed_pressure():
203
- """
204
- Predict weed pressure for the next 3 years (2025-2027) using linear regression on historical IFT data.
205
-
206
- This tool uses historical herbicide IFT data to predict future weed pressure. It applies linear
207
- regression to each plot's IFT evolution over time and extrapolates to 2025-2027. Risk levels are
208
- classified as: Faible (IFT < 1.0), Modéré (1.0 ≤ IFT < 2.0), Élevé (IFT ≥ 2.0).
209
-
210
- Prediction Method:
211
- 1. Calculate historical IFT for each plot/year combination
212
- 2. Apply linear regression: IFT = slope × year + intercept
213
- 3. Extrapolate to target years 2025-2027
214
- 4. Classify risk levels based on predicted IFT values
215
- 5. Include recent crop history and average historical IFT for context
216
-
217
- Returns:
218
- tuple: (plotly_figure, markdown_summary)
219
- - plotly_figure: Bar chart showing predicted IFT by plot and year with risk color coding
220
- - markdown_summary: Risk distribution statistics and interpretation
221
- """
222
- try:
223
- predictions = analyzer.predict_weed_pressure()
224
-
225
- if len(predictions) == 0:
226
- return None, "Impossible de générer des prédictions."
227
-
228
- fig = px.bar(predictions,
229
- x='plot_name',
230
- y='predicted_ift',
231
- color='risk_level',
232
- facet_col='year',
233
- title='Prédiction Pression Adventices (2025-2027)',
234
- color_discrete_map={'Faible': 'green', 'Modéré': 'orange', 'Élevé': 'red'})
235
-
236
- low_risk = len(predictions[predictions['risk_level'] == 'Faible'])
237
- moderate_risk = len(predictions[predictions['risk_level'] == 'Modéré'])
238
- high_risk = len(predictions[predictions['risk_level'] == 'Élevé'])
239
-
240
- summary = f"""
241
- 🔮 **Prédictions 2025-2027**
242
-
243
- **Répartition des risques:**
244
- - ✅ Risque faible: {low_risk} prédictions
245
- - ⚠️ Risque modéré: {moderate_risk} prédictions
246
- - ❌ Risque élevé: {high_risk} prédictions
247
- """
248
-
249
- return fig, summary
250
-
251
- except Exception as e:
252
- return None, f"Erreur: {str(e)}"
253
-
254
- def recommend_sensitive_crop_plots():
255
- """
256
- Recommend plots suitable for sensitive crops (pois, haricot) based on predicted weed pressure.
257
-
258
- This tool identifies plots with low predicted weed pressure (IFT < 1.0) and calculates a
259
- recommendation score to rank them for sensitive crop cultivation.
260
-
261
- Recommendation Method:
262
- 1. Get predicted IFT for 2025-2027 from predict_future_weed_pressure()
263
- 2. Filter plots with risk_level = "Faible" (IFT < 1.0)
264
- 3. Calculate recommendation_score = 100 - (predicted_ift × 30)
265
- 4. Sort plots by recommendation score (higher = better)
266
- 5. Include recent crop history and historical average IFT for context
267
-
268
- Recommendation Score:
269
- - 100-70: Excellent for sensitive crops
270
- - 70-50: Good for sensitive crops with monitoring
271
- - 50-0: Requires careful management
272
-
273
- Returns:
274
- tuple: (plotly_figure, markdown_summary)
275
- - plotly_figure: Scatter plot showing predicted IFT vs recommendation score
276
- - markdown_summary: Top recommended plots with scores and criteria
277
  """
278
- try:
279
- predictions = analyzer.predict_weed_pressure()
280
-
281
- if len(predictions) == 0:
282
- return None, "Aucune recommandation disponible."
283
-
284
- suitable_plots = predictions[predictions['risk_level'] == "Faible"].copy()
285
-
286
- if len(suitable_plots) > 0:
287
- suitable_plots['recommendation_score'] = 100 - (suitable_plots['predicted_ift'] * 30)
288
- suitable_plots = suitable_plots.sort_values('recommendation_score', ascending=False)
289
-
290
- top_recommendations = suitable_plots.head(10)[['plot_name', 'year', 'predicted_ift', 'recommendation_score']]
291
-
292
- summary = f"""
293
- 🌱 **Recommandations Cultures Sensibles**
294
-
295
- **Top parcelles recommandées:**
296
- {top_recommendations.to_string(index=False)}
297
-
298
- **Critères:** IFT prédit < 1.0 (faible pression adventices)
299
- """
300
-
301
- fig = px.scatter(suitable_plots,
302
- x='predicted_ift',
303
- y='recommendation_score',
304
- color='year',
305
- hover_data=['plot_name'],
306
- title='Parcelles Recommandées pour Cultures Sensibles')
307
-
308
- return fig, summary
309
- else:
310
- return None, "Aucune parcelle à faible risque identifiée."
311
-
312
- except Exception as e:
313
- return None, f"Erreur: {str(e)}"
314
-
315
- def explore_raw_data(year_start, year_end, plot_filter, crop_filter, intervention_filter):
316
  """
317
- Explore raw agricultural intervention data with filtering capabilities.
318
-
319
- This tool provides access to the raw dataset from the Station Expérimentale de Kerguéhennec
320
- (2014-2025) with filtering options to explore specific subsets of data.
321
 
322
- Args:
323
- year_start (int): Starting year for filtering (2014-2025)
324
- year_end (int): Ending year for filtering (2014-2025)
325
- plot_filter (str): Specific plot name or "Toutes" for all plots
326
- crop_filter (str): Specific crop type or "Toutes" for all crops
327
- intervention_filter (str): Specific intervention type or "Toutes" for all interventions
328
-
329
- Returns:
330
- tuple: (plotly_figure, markdown_summary)
331
- - plotly_figure: Interactive data table or visualization
332
- - markdown_summary: Data summary with statistics and filtering info
333
- """
334
- try:
335
- # Charger les données
336
- df = analyzer.load_data()
337
-
338
- # Appliquer les filtres
339
- if year_start and year_end:
340
- df = df[(df['year'] >= year_start) & (df['year'] <= year_end)]
341
-
342
- if plot_filter and plot_filter != "Toutes":
343
- df = df[df['plot_name'] == plot_filter]
344
-
345
- if crop_filter and crop_filter != "Toutes":
346
- df = df[df['crop_type'] == crop_filter]
347
-
348
- if intervention_filter and intervention_filter != "Toutes":
349
- df = df[df['intervention_type'] == intervention_filter]
350
-
351
- if len(df) == 0:
352
- return None, "Aucune donnée trouvée avec les filtres sélectionnés."
353
-
354
- # Créer un résumé des données
355
- summary = f"""
356
- 📊 **Exploration des Données Brutes**
357
-
358
- **Filtres appliqués:**
359
- - Période: {year_start}-{year_end}
360
- - Parcelle: {plot_filter}
361
- - Culture: {crop_filter}
362
- - Type d'intervention: {intervention_filter}
363
-
364
- **Statistiques:**
365
- - Nombre total d'enregistrements: {len(df):,}
366
- - Nombre de parcelles: {df['plot_name'].nunique()}
367
- - Nombre d'années: {df['year'].nunique()}
368
- - Types de cultures: {df['crop_type'].nunique()}
369
- - Types d'interventions: {df['intervention_type'].nunique()}
370
-
371
- **Répartition par année:**
372
- {df['year'].value_counts().sort_index().to_string()}
373
-
374
- **Top 10 parcelles:**
375
- {df['plot_name'].value_counts().head(10).to_string()}
376
-
377
- **Top 10 cultures:**
378
- {df['crop_type'].value_counts().head(10).to_string()}
379
-
380
- **Top 10 interventions:**
381
- {df['intervention_type'].value_counts().head(10).to_string()}
382
- """
383
-
384
- # Créer une visualisation des données
385
- if len(df) > 0:
386
- # Graphique des interventions par année
387
- yearly_counts = df.groupby('year').size().reset_index(name='count')
388
- fig = px.bar(yearly_counts, x='year', y='count',
389
- title=f'Nombre d\'interventions par année ({year_start}-{year_end})',
390
- labels={'count': 'Nombre d\'interventions', 'year': 'Année'})
391
-
392
- fig.update_layout(height=400)
393
- return fig, summary
394
- else:
395
- return None, summary
396
-
397
- except Exception as e:
398
- return None, f"Erreur lors de l'exploration des données: {str(e)}"
399
-
400
- def get_available_plots():
401
- """Get available plots."""
402
- try:
403
- df = analyzer.load_data()
404
- plots = sorted(df['plot_name'].dropna().unique().tolist())
405
- return ["Toutes"] + plots
406
- except Exception as e:
407
- print(f"Erreur lors du chargement des parcelles: {e}")
408
- return ["Toutes", "Champ ferme Bas", "Etang Milieu", "Lann Chebot"]
409
-
410
- def get_available_crops():
411
- """Get available crop types."""
412
- try:
413
- df = analyzer.load_data()
414
- crops = sorted(df['crop_type'].dropna().unique().tolist())
415
- return ["Toutes"] + crops
416
- except Exception as e:
417
- print(f"Erreur lors du chargement des cultures: {e}")
418
- return ["Toutes", "blé tendre hiver", "pois de conserve", "haricot mange-tout industrie"]
419
-
420
- def get_available_interventions():
421
- """Get available intervention types."""
422
- try:
423
- df = analyzer.load_data()
424
- interventions = sorted(df['intervention_type'].dropna().unique().tolist())
425
- return ["Toutes"] + interventions
426
- except Exception as e:
427
- print(f"Erreur lors du chargement des interventions: {e}")
428
- return ["Toutes", "Traitement et protection des cultures", "Fertilisation", "Travail et Entretien du sol"]
429
-
430
- # Create Gradio Interface
431
- def create_mcp_interface():
432
- with gr.Blocks(title="🚜 Analyse Pression Adventices", theme=gr.themes.Soft()) as demo:
433
  gr.Markdown("""
434
- # 🚜 Analyse Pression Adventices - CRA Bretagne
435
 
436
- Anticiper et réduire la pression des adventices pour optimiser les cultures sensibles (pois, haricot).
 
 
 
 
437
  """)
438
 
439
  with gr.Tabs():
 
440
  with gr.Tab("📈 Analyse Tendances"):
441
- gr.Markdown("### Analyser l'évolution de l'IFT herbicides par parcelle et période")
442
  gr.Markdown("""
443
- **Calcul de l'IFT (Indice de Fréquence de Traitement) :**
444
  - IFT = Nombre d'applications herbicides / Surface de la parcelle
445
- - Seuils d'interprétation :
446
- - 🟢 Faible : IFT < 1.0 (pression adventices faible)
447
- - 🟡 Modéré : 1.0 ≤ IFT < 2.0 (pression modérée)
448
- - 🔴 Élevé : IFT ≥ 2.0 (pression élevée)
449
  """)
450
 
451
  with gr.Row():
452
  with gr.Column():
453
- with gr.Row():
454
- year_start = gr.Slider(
455
- minimum=2014,
456
- maximum=2025,
457
- value=2020,
458
- step=1,
459
- label="Année de début"
460
- )
461
- year_end = gr.Slider(
462
- minimum=2014,
463
- maximum=2025,
464
- value=2025,
465
- step=1,
466
- label="Année de fin"
467
- )
468
- plot_dropdown = gr.Dropdown(
469
  choices=get_available_plots(),
470
  value="Toutes",
471
- label="Filtrer par parcelle",
472
- info="Choisissez une parcelle spécifique ou toutes"
473
  )
474
- analyze_btn = gr.Button("🔍 Analyser les Tendances", variant="primary", size="lg")
475
 
476
  with gr.Row():
477
- with gr.Column(scale=2):
478
- trends_plot = gr.Plot(label="Graphique d'évolution")
479
- with gr.Column(scale=1):
480
- trends_summary = gr.Markdown(label="Résumé statistique")
481
 
482
  analyze_btn.click(
483
  analyze_herbicide_trends,
484
- inputs=[year_start, year_end, plot_dropdown],
485
- outputs=[trends_plot, trends_summary]
486
  )
487
 
 
488
  with gr.Tab("🔮 Prédictions"):
489
- gr.Markdown("### Prédiction de la pression adventices 2025-2027")
490
  gr.Markdown("""
491
  **Méthode de prédiction :**
492
- 1. Calcul de l'IFT historique par parcelle et année
493
- 2. Régression linéaire : IFT = pente × année + ordonnée_origine
494
- 3. Extrapolation aux années 2025-2027
495
- 4. Classification des risques :
496
- - 🟢 Faible : IFT < 1.0
497
- - 🟡 Modéré : 1.0 ≤ IFT < 2.0
498
- - 🔴 Élevé : IFT ≥ 2.0
499
  """)
500
 
501
- predict_btn = gr.Button("🎯 Prédire 2025-2027", variant="primary")
 
502
 
503
  with gr.Row():
504
- predictions_plot = gr.Plot()
505
- predictions_summary = gr.Markdown()
506
 
507
- predict_btn.click(predict_future_weed_pressure, outputs=[predictions_plot, predictions_summary])
 
 
 
508
 
 
509
  with gr.Tab("🌱 Recommandations"):
510
- gr.Markdown("### Recommandations pour cultures sensibles (pois, haricot)")
511
  gr.Markdown("""
512
- **Méthode de recommandation :**
513
- 1. Prédiction IFT 2025-2027 par régression linéaire
514
- 2. Filtrage des parcelles à faible risque (IFT < 1.0)
515
- 3. Calcul du score de recommandation : 100 - (IFT_prédit × 30)
516
- 4. Classement par score (plus élevé = meilleur)
517
  """)
518
 
519
- recommend_btn = gr.Button("🎯 Recommander Parcelles", variant="primary")
 
520
 
521
  with gr.Row():
522
- recommendations_plot = gr.Plot()
523
- recommendations_summary = gr.Markdown()
524
 
525
- recommend_btn.click(recommend_sensitive_crop_plots, outputs=[recommendations_plot, recommendations_summary])
 
 
 
526
 
527
- with gr.Tab("📊 Exploration Données"):
528
- gr.Markdown("### Explorer les données brutes de la Station Expérimentale de Kerguéhennec")
 
 
 
 
 
 
 
 
529
 
530
  with gr.Row():
531
  with gr.Column():
532
  data_year_start = gr.Slider(
533
  minimum=2014,
534
  maximum=2025,
535
- value=2020,
536
  step=1,
537
  label="Année de début"
538
  )
@@ -570,8 +183,96 @@ def create_mcp_interface():
570
  outputs=[data_plot, data_summary]
571
  )
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
574
- return demo
575
-
576
-
577
-
 
1
+ """
2
+ MCP Server for Agricultural Weed Pressure Analysis
3
+ Integrates tools, resources and prompts from the mcp/ folder
4
+ """
5
 
6
  import gradio as gr
7
  import pandas as pd
8
  import numpy as np
9
  import plotly.express as px
10
  from data_loader import AgriculturalDataLoader
11
+ from agricultural_mcp.tools import WeedPressureAnalyzer, analyze_herbicide_trends, predict_future_weed_pressure, recommend_sensitive_crop_plots, explore_raw_data, get_available_plots, get_available_crops, get_available_interventions
12
+ from agricultural_mcp.resources import AgriculturalResources
13
  import warnings
14
  warnings.filterwarnings('ignore')
15
 
16
+ # Initialize analyzer and resources
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  analyzer = WeedPressureAnalyzer()
18
+ resources = AgriculturalResources()
19
 
20
+ def create_mcp_interface():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  """
22
+ Create the main MCP interface with 5 tabs:
23
+ 1. Analyse Tendances - IFT herbicide analysis
24
+ 2. Prédictions - Weed pressure predictions 2025-2027
25
+ 3. Recommandations - Sensitive crop recommendations
26
+ 4. Exploration Données - Raw data exploration
27
+ 5. MCP Resources - Display resources.py content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  """
 
 
 
 
29
 
30
+ with gr.Blocks(title="Serveur MCP - Analyse Pression Adventices", theme=gr.themes.Soft()) as demo:
31
+ gr.Markdown("# 🌾 Serveur MCP - Analyse Pression Adventices")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  gr.Markdown("""
33
+ **Analyse de la pression adventices et recommandations pour cultures sensibles**
34
 
35
+ Ce serveur MCP (Model Context Protocol) fournit des outils d'analyse pour :
36
+ - Calculer l'IFT (Indice de Fréquence de Traitement) des herbicides
37
+ - Prédire la pression adventices pour 2025-2027
38
+ - Recommander des parcelles pour cultures sensibles (pois, haricot)
39
+ - Explorer les données agricoles avec filtres avancés
40
  """)
41
 
42
  with gr.Tabs():
43
+ # Tab 1: Analyse Tendances
44
  with gr.Tab("📈 Analyse Tendances"):
45
+ gr.Markdown("### Analyse des tendances IFT herbicides")
46
  gr.Markdown("""
47
+ **Méthode de calcul IFT :**
48
  - IFT = Nombre d'applications herbicides / Surface de la parcelle
49
+ - Analyse de l'évolution temporelle par parcelle
50
+ - Classification des niveaux de risque : Faible (IFT < 1.0), Modéré (1.0 IFT < 2.0), Élevé (IFT ≥ 2.0)
 
 
51
  """)
52
 
53
  with gr.Row():
54
  with gr.Column():
55
+ year_start = gr.Slider(
56
+ minimum=2014,
57
+ maximum=2025,
58
+ value=2014,
59
+ step=1,
60
+ label="Année de début"
61
+ )
62
+ year_end = gr.Slider(
63
+ minimum=2014,
64
+ maximum=2025,
65
+ value=2025,
66
+ step=1,
67
+ label="Année de fin"
68
+ )
69
+ plot_filter = gr.Dropdown(
 
70
  choices=get_available_plots(),
71
  value="Toutes",
72
+ label="Filtrer par parcelle"
 
73
  )
74
+ analyze_btn = gr.Button("📊 Analyser les Tendances", variant="primary")
75
 
76
  with gr.Row():
77
+ trend_plot = gr.Plot()
78
+ trend_summary = gr.Markdown()
 
 
79
 
80
  analyze_btn.click(
81
  analyze_herbicide_trends,
82
+ inputs=[year_start, year_end, plot_filter],
83
+ outputs=[trend_plot, trend_summary]
84
  )
85
 
86
+ # Tab 2: Prédictions
87
  with gr.Tab("🔮 Prédictions"):
88
+ gr.Markdown("### Prédictions de pression adventices 2025-2027")
89
  gr.Markdown("""
90
  **Méthode de prédiction :**
91
+ - Régression linéaire sur les données IFT historiques
92
+ - Extrapolation pour les années 2025-2027
93
+ - Classification des niveaux de risque basée sur l'IFT prédit
94
+ - Prise en compte de l'historique des cultures récentes
 
 
 
95
  """)
96
 
97
+ with gr.Row():
98
+ predict_btn = gr.Button("🔮 Générer les Prédictions", variant="primary")
99
 
100
  with gr.Row():
101
+ pred_plot = gr.Plot()
102
+ pred_summary = gr.Markdown()
103
 
104
+ predict_btn.click(
105
+ predict_future_weed_pressure,
106
+ outputs=[pred_plot, pred_summary]
107
+ )
108
 
109
+ # Tab 3: Recommandations
110
  with gr.Tab("🌱 Recommandations"):
111
+ gr.Markdown("### Recommandations pour cultures sensibles")
112
  gr.Markdown("""
113
+ **Critères de recommandation :**
114
+ - Parcelles avec IFT prédit < 1.0 (faible pression adventices)
115
+ - Score de recommandation : 100 - (IFT_prédit × 30)
116
+ - Cultures sensibles : pois, haricot
117
+ - Prise en compte de l'historique cultural récent
118
  """)
119
 
120
+ with gr.Row():
121
+ recommend_btn = gr.Button("🌱 Générer les Recommandations", variant="primary")
122
 
123
  with gr.Row():
124
+ rec_plot = gr.Plot()
125
+ rec_summary = gr.Markdown()
126
 
127
+ recommend_btn.click(
128
+ recommend_sensitive_crop_plots,
129
+ outputs=[rec_plot, rec_summary]
130
+ )
131
 
132
+ # Tab 4: Exploration Données
133
+ with gr.Tab("🔍 Exploration Données"):
134
+ gr.Markdown("### Exploration des données brutes")
135
+ gr.Markdown("""
136
+ **Filtres disponibles :**
137
+ - Années : 2014-2025
138
+ - Parcelles : 106 parcelles disponibles
139
+ - Cultures : 42 types de cultures
140
+ - Types d'intervention : Herbicides, Fertilisation, Semis, etc.
141
+ """)
142
 
143
  with gr.Row():
144
  with gr.Column():
145
  data_year_start = gr.Slider(
146
  minimum=2014,
147
  maximum=2025,
148
+ value=2014,
149
  step=1,
150
  label="Année de début"
151
  )
 
183
  outputs=[data_plot, data_summary]
184
  )
185
 
186
+ # Tab 5: MCP Resources
187
+ with gr.Tab("🔧 MCP Resources"):
188
+ gr.Markdown("### Resources MCP disponibles")
189
+ gr.Markdown("""
190
+ **Resources MCP exposées :**
191
+ - `exploitation://{siret}` - Informations d'exploitation
192
+ - `parcelle://{numparcell}` - Informations de parcelle
193
+ - `intervention://{rang}` - Informations d'intervention
194
+ - `intrant://{codeamm}` - Informations d'intrant
195
+ - `materiel://{id}` - Informations de matériel
196
+ """)
197
+
198
+ with gr.Row():
199
+ with gr.Column():
200
+ gr.Markdown("#### Test des Resources MCP")
201
+
202
+ # Test Exploitation
203
+ with gr.Row():
204
+ siret_input = gr.Textbox(
205
+ label="SIRET Exploitation",
206
+ value="18560001000016",
207
+ placeholder="18560001000016"
208
+ )
209
+ siret_btn = gr.Button("🏢 Test Exploitation", variant="secondary")
210
+
211
+ # Test Parcelle
212
+ with gr.Row():
213
+ parcelle_input = gr.Textbox(
214
+ label="Numéro Parcelle",
215
+ value="12",
216
+ placeholder="12"
217
+ )
218
+ parcelle_btn = gr.Button("🏞️ Test Parcelle", variant="secondary")
219
+
220
+ # Test Intervention
221
+ with gr.Row():
222
+ intervention_input = gr.Textbox(
223
+ label="Rang Intervention",
224
+ value="1",
225
+ placeholder="1"
226
+ )
227
+ intervention_btn = gr.Button("⚙️ Test Intervention", variant="secondary")
228
+
229
+ # Test Intrant
230
+ with gr.Row():
231
+ intrant_input = gr.Textbox(
232
+ label="Code AMM Intrant",
233
+ value="9100296",
234
+ placeholder="9100296"
235
+ )
236
+ intrant_btn = gr.Button("🧪 Test Intrant", variant="secondary")
237
+
238
+ # Test Matériel
239
+ with gr.Row():
240
+ materiel_input = gr.Textbox(
241
+ label="ID Matériel",
242
+ value="1",
243
+ placeholder="1"
244
+ )
245
+ materiel_btn = gr.Button("🔧 Test Matériel", variant="secondary")
246
+
247
+ with gr.Column():
248
+ gr.Markdown("#### Résultat")
249
+ resource_output = gr.JSON(label="Résultat de la resource")
250
+
251
+ # Connexions des boutons
252
+ siret_btn.click(
253
+ lambda siret: resources.get_exploitation(siret),
254
+ inputs=[siret_input],
255
+ outputs=[resource_output]
256
+ )
257
+ parcelle_btn.click(
258
+ lambda parcelle: resources.get_parcelle(parcelle),
259
+ inputs=[parcelle_input],
260
+ outputs=[resource_output]
261
+ )
262
+ intervention_btn.click(
263
+ lambda intervention: resources.get_intervention(intervention),
264
+ inputs=[intervention_input],
265
+ outputs=[resource_output]
266
+ )
267
+ intrant_btn.click(
268
+ lambda intrant: resources.get_intrant(intrant),
269
+ inputs=[intrant_input],
270
+ outputs=[resource_output]
271
+ )
272
+ materiel_btn.click(
273
+ lambda materiel: resources.get_materiel(materiel),
274
+ inputs=[materiel_input],
275
+ outputs=[resource_output]
276
+ )
277
 
278
+ return demo
 
 
 
test_new_structure.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test de la nouvelle structure avec dossier mcp/
3
+ """
4
+
5
+ import os
6
+ from mcp_server import create_mcp_interface
7
+
8
+ # Hugging Face configuration
9
+ hf_token = os.environ.get("HF_TOKEN")
10
+ if hf_token:
11
+ os.environ["HF_TOKEN"] = hf_token
12
+ os.environ["DATASET_ID"] = "HackathonCRA/2024"
13
+
14
+ def test_new_structure():
15
+ """Test de la nouvelle structure avec dossier mcp/"""
16
+ print("🧪 Test de la nouvelle structure avec dossier mcp/...")
17
+
18
+ try:
19
+ demo = create_mcp_interface()
20
+ print("✅ Interface créée avec succès")
21
+
22
+ # Test des imports
23
+ from agricultural_mcp.tools import WeedPressureAnalyzer
24
+ from agricultural_mcp.resources import AgriculturalResources
25
+
26
+ print("✅ Imports des modules mcp/ fonctionnels")
27
+
28
+ # Test des resources
29
+ resources = AgriculturalResources()
30
+ print("✅ Resources MCP initialisées")
31
+
32
+ print("\n🎯 Nouvelle structure fonctionnelle !")
33
+ print("📋 5 onglets disponibles")
34
+ print("🔧 Onglet MCP Resources avec test des resources")
35
+ print("📁 Structure modulaire : tools.py, resources.py, prompts.py")
36
+ print("🚀 Prêt pour déploiement avec mcp_server=True")
37
+
38
+ return True
39
+
40
+ except Exception as e:
41
+ print(f"❌ Erreur: {e}")
42
+ import traceback
43
+ traceback.print_exc()
44
+ return False
45
+
46
+ if __name__ == "__main__":
47
+ test_new_structure()