Eddyhzd commited on
Commit
37ac408
·
1 Parent(s): 0b6912d

first commit

Browse files
.gitignore ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ env/
26
+ ENV/
27
+
28
+ # IDEs
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Data files (large CSV/Excel files)
35
+ *.csv
36
+ *.xlsx
37
+ data/
38
+ *.parquet
39
+
40
+ # Model files
41
+ models/
42
+ *.pkl
43
+ *.joblib
44
+
45
+ # Logs
46
+ *.log
47
+ logs/
48
+
49
+ # Environment variables
50
+ .env
51
+ .env.local
52
+
53
+ # Cache
54
+ .cache/
55
+ *.cache
56
+
57
+ # OS
58
+ .DS_Store
59
+ Thumbs.db
60
+
61
+ # Gradio
62
+ gradio_cached_examples/
63
+ flagged/
64
+
65
+ # Jupyter
66
+ .ipynb_checkpoints/
67
+ *.ipynb
PROMPT.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 🚜 Hackathon CRA – Prompt d’implémentation
2
+ 🎯 Problématique
3
+
4
+ Comment anticiper et réduire la pression des adventices dans les parcelles agricoles bretonnes, dans un contexte de réduction progressive des herbicides, en s’appuyant sur l’analyse des données historiques, climatiques et agronomiques, afin d’identifier les parcelles les plus adaptées à la culture de plantes sensibles (pois, haricot) sur les 3 prochaines années ?
5
+
6
+ 🔍 Objectifs du modèle de simulation
7
+
8
+ Prédire la pression adventices sur chaque parcelle pour les 3 prochaines campagnes.
9
+
10
+ Identifier les parcelles à faible risque adaptées aux cultures sensibles (pois, haricot).
11
+
12
+ Intégrer les données suivantes :
13
+
14
+ Climatiques
15
+
16
+ Historiques d’intervention
17
+
18
+ Rotations
19
+
20
+ Rendements
21
+
22
+ IFT (Indice de Fréquence de Traitement)
23
+
24
+ Proposer des alternatives techniques en cas de retrait de certaines molécules herbicides.
25
+
26
+ ⚙️ Objectifs techniques
27
+
28
+ Créer un serveur MCP (Model Context Protocol).
29
+
30
+ Utiliser Gradio pour exposer ce serveur MCP.
31
+
32
+ Assurer la compatibilité avec Hugging Face (hébergement HF).
33
+
34
+ Configuration Hugging Face :
35
+
36
+ hf_token = os.environ.get("HF_TOKEN")
37
+ dataset_id = "HackathonCRA/2024"
38
+
39
+
40
+ (dataset accessible via HF avec cet id et ce token, ces données sont dispo pour toi dans OneDrive_1_9-17-2025. Ce dossier sert uniquement a t'aider mais tu dois utiliser data_loader.py pour charger ces données qui sont sur HF en format csv).
41
+
42
+ Fournir au LLM des tools et resources pour :
43
+
44
+ Analyses graphiques et statistiques précises et sourcées.
45
+
46
+ Filtrer (ou non) par années et par parcelles (certaines parcelles ne sont pas disponibles tous les ans).
47
+
48
+ L’outil doit être simple, rapide à mettre en place et fonctionnel.
49
+
50
+ 🧑‍💻 Prompt pour l’IA
51
+
52
+ Tu es un expert en intelligence artificielle chargé de mettre en place un outil pour le CRA dans le cadre d’un hackathon agricole.
53
+
54
+ Ta mission :
55
+
56
+ Analyser les données mises à disposition.
57
+
58
+ Concevoir et implémenter un serveur MCP conforme aux objectifs ci-dessus.
59
+
60
+ Exposer ce serveur via une interface Gradio, compatible avec Hugging Face.
61
+
62
+ Fournir des tools et resources exploitables par un LLM, permettant d’effectuer des analyses fiables, visuelles et interactives.
63
+
64
+
65
+
66
+ Voici de la documentation pour faire des mcp avec gradio :
67
+ - https://www.gradio.app/guides/building-mcp-server-with-gradio
68
+ - https://huggingface.co/blog/gradio-mcp
69
+
70
+ Voici un exemple de MCP qui fonctionne actuellement :
71
+ import gradio as gr
72
+
73
+ ```
74
+ def letter_counter(word, letter):
75
+ """Count the occurrences of a specific letter in a word.
76
+
77
+ Args:
78
+ word: The word or phrase to analyze
79
+ letter: The letter to count occurrences of
80
+
81
+ Returns:
82
+ The number of times the letter appears in the word
83
+ """
84
+ return word.lower().count(letter.lower())
85
+
86
+ demo = gr.Interface(
87
+ fn=letter_counter,
88
+ inputs=["text", "text"],
89
+ outputs="number",
90
+ title="Letter Counter",
91
+ description="Count how many times a letter appears in a word"
92
+ )
93
+
94
+ demo.launch(mcp_server=True)
95
+ ```
96
+
97
+ Appuies toi sur cette documentation pour produire ce MCP, au plus simple et efficace pour avoir un produit fonctionnel.
README.md CHANGED
@@ -1,12 +1,130 @@
1
  ---
2
- title: Hackathon CRA
3
- emoji: 💻
4
- colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.46.0
8
  app_file: app.py
9
  pinned: false
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Analyse Pression Adventices - CRA Bretagne
3
+ emoji: 🚜
4
+ colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.46.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Serveur MCP pour analyser la pression adventices agricoles
12
+ tags:
13
+ - agriculture
14
+ - mcp
15
+ - gradio
16
+ - prediction
17
+ - herbicides
18
+ - bretagne
19
+ - hackathon
20
  ---
21
 
22
+ # 🚜 Hackathon CRA - Analyse Pression Adventices
23
+
24
+ ## 🎯 Objectif
25
+
26
+ Serveur MCP (Model Context Protocol) pour anticiper et réduire la pression des adventices dans les parcelles agricoles bretonnes, en s'appuyant sur l'analyse des données historiques de la Station Expérimentale de Kerguéhennec (2014-2024).
27
+
28
+ ## 🔍 Fonctionnalités
29
+
30
+ ### 📈 Analyse des Tendances IFT
31
+ - Calcul de l'Indice de Fréquence de Traitement (IFT) herbicides
32
+ - Évolution temporelle par parcelle et par culture
33
+ - Filtrage par période et parcelle
34
+
35
+ ### 🔮 Prédictions 2025-2027
36
+ - Modèle prédictif basé sur les tendances historiques
37
+ - Classification des risques (Faible/Modéré/Élevé)
38
+ - Visualisations interactives
39
+
40
+ ### 🌱 Recommandations Cultures Sensibles
41
+ - Identification des parcelles adaptées aux pois et haricot
42
+ - Score de recommandation basé sur l'IFT prédit
43
+ - Critères de sélection optimisés
44
+
45
+ ### 🔄 Alternatives Techniques
46
+ - Propositions d'alternatives mécaniques, culturales et biologiques
47
+ - Plans d'action pour réduction des herbicides
48
+ - Documentation des meilleures pratiques
49
+
50
+ ## ⚙️ Installation
51
+
52
+ ```bash
53
+ # Cloner le projet
54
+ git clone <repo-url>
55
+ cd mcp
56
+
57
+ # Installer les dépendances
58
+ pip install -r requirements.txt
59
+
60
+ # Configuration Hugging Face (optionnel)
61
+ export HF_TOKEN="your_hf_token"
62
+ export DATASET_ID="HackathonCRA/2024"
63
+ ```
64
+
65
+ ## 🚀 Lancement
66
+
67
+ ### Local
68
+ ```bash
69
+ python mcp_server.py
70
+ ```
71
+
72
+ ### Hugging Face Spaces
73
+ ```bash
74
+ python app.py
75
+ ```
76
+
77
+ Le serveur MCP sera accessible sur `http://localhost:7860`
78
+
79
+ ## 📊 Structure des Données
80
+
81
+ Les données proviennent de la Station Expérimentale de Kerguéhennec et incluent :
82
+
83
+ - **Variables temporelles** : millésime, dates d'intervention
84
+ - **Variables spatiales** : parcelles, surfaces
85
+ - **Variables culturales** : types de cultures, rotations
86
+ - **Variables techniques** : produits utilisés, quantités, IFT
87
+
88
+ ## 🤖 Architecture MCP
89
+
90
+ Le serveur expose des outils d'analyse via le protocole MCP :
91
+
92
+ 1. **analyze_herbicide_trends** : Analyse des tendances IFT
93
+ 2. **predict_future_weed_pressure** : Prédictions 2025-2027
94
+ 3. **recommend_sensitive_crop_plots** : Recommandations parcelles
95
+ 4. **generate_technical_alternatives** : Alternatives techniques
96
+
97
+ ## 📈 Méthodes d'Analyse
98
+
99
+ ### Calcul IFT Herbicides
100
+ ```
101
+ IFT = Nombre d'applications / Surface parcelle
102
+ ```
103
+
104
+ ### Prédiction Pression Adventices
105
+ - Régression linéaire sur données historiques
106
+ - Classification en niveaux de risque
107
+ - Extrapolation 2025-2027
108
+
109
+ ### Score de Recommandation
110
+ ```
111
+ Score = 100 - (IFT_prédit × 30)
112
+ ```
113
+
114
+ ## 🛠️ Technologies
115
+
116
+ - **Gradio** : Interface utilisateur et serveur MCP
117
+ - **Pandas/Numpy** : Traitement des données
118
+ - **Plotly** : Visualisations interactives
119
+ - **Hugging Face** : Hébergement et datasets
120
+ - **Python 3.8+** : Langage principal
121
+
122
+ ## 📝 Licence
123
+
124
+ Projet développé dans le cadre du Hackathon CRA Bretagne 2024.
125
+
126
+ ## 🤝 Contact
127
+
128
+ - **Équipe** : Hackathon CRA Bretagne
129
+ - **Données** : Station Expérimentale de Kerguéhennec
130
+ - **Support** : GitHub Issues
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,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import typing as t
2
+ import pandas as pd
3
+ import gradio as gr
4
+ from data_loader import AgriculturalDataLoader
5
+
6
+ # -------------------------------------------------------------------
7
+ # Hypothèse: AgriculturalDataLoader.load_all_files() concatène 10 CSV
8
+ # et renvoie un DataFrame avec au moins ces colonnes (si présentes):
9
+ # ["millesime","raisonsoci","siret","pacage","refca","numilot","numparcell",
10
+ # "nomparc","surfparc","rang","kqte","teneurn","teneurp","teneurk",
11
+ # "keq","volumebo","codeamm","codegnis","materiel","mainoeuvre", ...]
12
+ # -------------------------------------------------------------------
13
+
14
+ class AgriculturalResources:
15
+ def __init__(self):
16
+ self.data_loader = AgriculturalDataLoader()
17
+ self.data_cache: t.Optional[pd.DataFrame] = None
18
+
19
+ def load_data(self) -> pd.DataFrame:
20
+ if self.data_cache is None:
21
+ df = self.data_loader.load_all_files()
22
+
23
+ # Normalisation minimale & robustesse
24
+ df = df.copy()
25
+ # Harmonise noms connus (au cas où)
26
+ rename_map = {
27
+ "raisonsoci": "raisonsociale",
28
+ "numparcelle": "numparcell",
29
+ "NomParc": "nomparc",
30
+ "SurfParc": "surfparc",
31
+ }
32
+ for k, v in rename_map.items():
33
+ if k in df.columns and v not in df.columns:
34
+ df[v] = df[k]
35
+
36
+ # Types & trim
37
+ for col in ["millesime", "siret", "pacage", "refca", "numilot", "numparcell", "rang",
38
+ "codeamm", "codegnis"]:
39
+ if col in df.columns:
40
+ df[col] = df[col].astype(str).str.strip()
41
+
42
+ for col in ["nomparc", "raisonsociale", "materiel", "mainoeuvre"]:
43
+ if col in df.columns:
44
+ df[col] = df[col].astype(str).str.strip()
45
+
46
+ for col in ["surfparc", "kqte", "teneurn", "teneurp", "teneurk", "keq", "volumebo"]:
47
+ if col in df.columns:
48
+ # coerce = NaN si non convertible
49
+ df[col] = pd.to_numeric(df[col], errors="coerce")
50
+
51
+ # IDs composites utiles
52
+ if {"millesime", "numparcell"}.issubset(df.columns):
53
+ df["parcelle_id"] = df["millesime"] + ":" + df["numparcell"]
54
+ else:
55
+ df["parcelle_id"] = None
56
+
57
+ if {"millesime", "numparcell", "rang"}.issubset(df.columns):
58
+ df["intervention_id"] = df["millesime"] + ":" + df["numparcell"] + ":" + df["rang"]
59
+ else:
60
+ df["intervention_id"] = None
61
+
62
+ self.data_cache = df
63
+
64
+ return self.data_cache
65
+
66
+ # -------------------------
67
+ # Utilitaires internes
68
+ # -------------------------
69
+ def _safe_first(self, df: pd.DataFrame) -> t.Optional[pd.Series]:
70
+ if df is None or df.empty:
71
+ return None
72
+ return df.iloc[0]
73
+
74
+ def _notnull(self, d: dict) -> dict:
75
+ # Retire les champs None/NaN pour des payloads plus propres
76
+ return {k: v for k, v in d.items() if pd.notna(v)}
77
+
78
+ # -------------------------
79
+ # LISTINGS / DISCOVERY
80
+ # -------------------------
81
+
82
+ @gr.mcp.resource("dataset://years")
83
+ def list_years(self) -> t.List[str]:
84
+ """Liste des millésimes disponibles dans l'ensemble des fichiers."""
85
+ df = self.load_data()
86
+ if "millesime" not in df.columns:
87
+ return []
88
+ years = sorted(df["millesime"].dropna().astype(str).unique())
89
+ return years
90
+
91
+ @gr.mcp.resource("exploitation://{siret}/parcelles")
92
+ def list_parcelles_by_exploitation(self, siret: str, millesime: t.Optional[str] = None) -> t.List[dict]:
93
+ """Liste les parcelles d'une exploitation (optionnellement filtrées par millésime)."""
94
+ df = self.load_data()
95
+ q = df[df["siret"] == siret] if "siret" in df.columns else df.iloc[0:0]
96
+ if millesime:
97
+ q = q[q["millesime"] == str(millesime)]
98
+ cols = [c for c in ["parcelle_id","millesime","numparcell","nomparc","surfparc","refca","numilot"] if c in q.columns]
99
+ out = q[cols].drop_duplicates().to_dict(orient="records")
100
+ return out
101
+
102
+ @gr.mcp.resource("parcelles://search")
103
+ def search_parcelles(self, query: str = "", millesime: t.Optional[str] = None, limit: int = 50) -> t.List[dict]:
104
+ """Recherche de parcelles par nom/numéro, filtrable par millésime."""
105
+ df = self.load_data()
106
+ q = df
107
+ if millesime:
108
+ q = q[q["millesime"] == str(millesime)]
109
+ if query:
110
+ mask = False
111
+ if "numparcell" in q.columns:
112
+ mask = q["numparcell"].str.contains(query, case=False, na=False)
113
+ if "nomparc" in q.columns:
114
+ mask = mask | q["nomparc"].str.contains(query, case=False, na=False)
115
+ q = q[mask]
116
+ cols = [c for c in ["parcelle_id","millesime","numparcell","nomparc","surfparc","refca","numilot","siret"] if c in q.columns]
117
+ return q[cols].drop_duplicates().head(limit).to_dict(orient="records")
118
+
119
+ # -------------------------
120
+ # RESSOURCES CANONIQUES
121
+ # -------------------------
122
+
123
+ @gr.mcp.resource("exploitation://{siret}/{millesime}")
124
+ def get_exploitation(self, siret: str, millesime: str) -> dict:
125
+ """Infos d'une exploitation (pour un millésime donné)."""
126
+ df = self.load_data()
127
+ q = df[(df["siret"] == siret) & (df["millesime"] == str(millesime))] if {"siret","millesime"}.issubset(df.columns) else df.iloc[0:0]
128
+ row = self._safe_first(q.sort_values(by=[c for c in ["millesime"] if c in q.columns], ascending=False))
129
+ if row is None:
130
+ return {}
131
+ return self._notnull({
132
+ "millesime": row.get("millesime"),
133
+ "siret": row.get("siret"),
134
+ "raison_sociale": row.get("raisonsociale"),
135
+ "pacage": row.get("pacage"),
136
+ })
137
+
138
+ @gr.mcp.resource("parcelle://{millesime}/{numparcell}")
139
+ def get_parcelle(self, millesime: str, numparcell: str) -> dict:
140
+ """Infos d'une parcelle (identifiée par millésime + numparcell)."""
141
+ df = self.load_data()
142
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell))]
143
+ row = self._safe_first(q)
144
+ if row is None:
145
+ return {}
146
+ return self._notnull({
147
+ "parcelle_id": row.get("parcelle_id"),
148
+ "millesime": row.get("millesime"),
149
+ "numparcell": row.get("numparcell"),
150
+ "nomparc": row.get("nomparc"),
151
+ "surfparc": row.get("surfparc"),
152
+ "siret": row.get("siret"),
153
+ "refca": row.get("refca"),
154
+ "numilot": row.get("numilot"),
155
+ })
156
+
157
+ @gr.mcp.resource("intervention://{millesime}/{numparcell}/{rang}")
158
+ def get_intervention(self, millesime: str, numparcell: str, rang: str) -> dict:
159
+ """Infos d'une intervention (clé composite millésime + numparcell + rang)."""
160
+ df = self.load_data()
161
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell)) & (df["rang"] == str(rang))]
162
+ row = self._safe_first(q)
163
+ if row is None:
164
+ return {}
165
+ return self._notnull({
166
+ "intervention_id": row.get("intervention_id"),
167
+ "millesime": row.get("millesime"),
168
+ "numparcell": row.get("numparcell"),
169
+ "rang": row.get("rang"),
170
+ "mainoeuvre": row.get("mainoeuvre"),
171
+ "materiel": row.get("materiel"),
172
+ "codeamm": row.get("codeamm"),
173
+ "codegnis": row.get("codegnis"),
174
+ "kqte": row.get("kqte"),
175
+ "teneurn": row.get("teneurn"),
176
+ "teneurp": row.get("teneurp"),
177
+ "teneurk": row.get("teneurk"),
178
+ "keq": row.get("keq"),
179
+ "volumebo": row.get("volumebo"),
180
+ })
181
+
182
+ @gr.mcp.resource("intrant://{codeamm}")
183
+ def get_intrant(self, codeamm: str, millesime: t.Optional[str] = None) -> dict:
184
+ """Infos d’un intrant (filtrable par millésime)."""
185
+ df = self.load_data()
186
+ q = df[df["codeamm"] == str(codeamm)] if "codeamm" in df.columns else df.iloc[0:0]
187
+ if millesime:
188
+ q = q[q["millesime"] == str(millesime)]
189
+ row = self._safe_first(q)
190
+ if row is None:
191
+ return {}
192
+ return self._notnull({
193
+ "codeamm": row.get("codeamm"),
194
+ "codegnis": row.get("codegnis"),
195
+ "millesime": row.get("millesime"),
196
+ "kqte": row.get("kqte"),
197
+ "teneurn": row.get("teneurn"),
198
+ "teneurp": row.get("teneurp"),
199
+ "teneurk": row.get("teneurk"),
200
+ "keq": row.get("keq"),
201
+ "volumebo": row.get("volumebo"),
202
+ })
203
+
204
+ @gr.mcp.resource("materiel://{millesime}/{numparcell}/{rang}")
205
+ def get_materiel(self, millesime: str, numparcell: str, rang: str) -> dict:
206
+ """Matériel utilisé pour une intervention donnée."""
207
+ df = self.load_data()
208
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell)) & (df["rang"] == str(rang))]
209
+ row = self._safe_first(q)
210
+ if row is None:
211
+ return {}
212
+ return self._notnull({
213
+ "millesime": row.get("millesime"),
214
+ "numparcell": row.get("numparcell"),
215
+ "rang": row.get("rang"),
216
+ "materiel": row.get("materiel"),
217
+ })
218
+
219
+ @gr.mcp.resource("maindoeuvre://{millesime}/{numparcell}/{rang}")
220
+ def get_main_oeuvre(self, millesime: str, numparcell: str, rang: str) -> dict:
221
+ """Main d’œuvre associée à une intervention donnée."""
222
+ df = self.load_data()
223
+ q = df[(df["millesime"] == str(millesime)) & (df["numparcell"] == str(numparcell)) & (df["rang"] == str(rang))]
224
+ row = self._safe_first(q)
225
+ if row is None:
226
+ return {}
227
+ return self._notnull({
228
+ "millesime": row.get("millesime"),
229
+ "numparcell": row.get("numparcell"),
230
+ "rang": row.get("rang"),
231
+ "mainoeuvre": row.get("mainoeuvre"),
232
+ })
233
+
234
+ # -------------------------------------------------------------------
235
+ # Instance pour utilisation
236
+ # -------------------------------------------------------------------
237
+ 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 ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main application launcher for Hugging Face deployment
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
+ demo = create_mcp_interface()
15
+ demo.launch(mcp_server=True)
data_loader.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data loader for agricultural intervention data.
3
+ Loads data exclusively from Hugging Face datasets.
4
+ """
5
+
6
+ import pandas as pd
7
+ import numpy as np
8
+ from typing import List, Optional
9
+ import os
10
+ from datasets import Dataset, load_dataset
11
+ from huggingface_hub import HfApi, hf_hub_download
12
+
13
+
14
+ class AgriculturalDataLoader:
15
+ """Loads and preprocesses agricultural intervention data from Hugging Face datasets."""
16
+
17
+ def __init__(self, hf_token: str = None, dataset_id: str = None):
18
+ self.hf_token = hf_token or os.environ.get("HF_TOKEN")
19
+ self.dataset_id = dataset_id or "HackathonCRA/2024"
20
+ self.data_cache = {}
21
+
22
+ def load_all_files(self) -> pd.DataFrame:
23
+ """Load data from Hugging Face dataset."""
24
+ if 'combined_data' in self.data_cache:
25
+ return self.data_cache['combined_data']
26
+
27
+ # Load from Hugging Face only
28
+ df = self.load_from_huggingface()
29
+ self.data_cache['combined_data'] = df
30
+ return df
31
+
32
+ def load_from_huggingface(self) -> pd.DataFrame:
33
+ """Load data from Hugging Face dataset."""
34
+ print(f"🤗 Loading dataset from Hugging Face: {self.dataset_id}")
35
+
36
+ try:
37
+ # Try multiple loading strategies
38
+ df = None
39
+
40
+ # Strategy 1: Try direct dataset loading
41
+ try:
42
+ dataset = load_dataset(
43
+ self.dataset_id,
44
+ token=self.hf_token,
45
+ streaming=False
46
+ )
47
+ df = dataset["train"].to_pandas()
48
+ print(f"✅ Loaded via load_dataset: {len(df)} records")
49
+
50
+ except Exception as e1:
51
+ print(f"⚠️ load_dataset failed: {e1}")
52
+
53
+ # Strategy 2: Load individual CSV files from HF Hub
54
+ try:
55
+ df = self._load_csv_files_from_hub()
56
+ print(f"✅ Loaded via individual CSV files: {len(df)} records")
57
+
58
+ except Exception as e2:
59
+ print(f"⚠️ CSV loading failed: {e2}")
60
+ raise ValueError(f"All loading strategies failed. Dataset: {e1}, CSV: {e2}")
61
+
62
+ if df is None or len(df) == 0:
63
+ raise ValueError("No data loaded from any strategy")
64
+
65
+ # Apply preprocessing
66
+ df = self._preprocess_data(df)
67
+ print(f"✅ Successfully processed {len(df)} records from Hugging Face")
68
+
69
+ return df
70
+
71
+ except Exception as e:
72
+ raise ValueError(f"Failed to load dataset from Hugging Face: {e}")
73
+
74
+ def _load_csv_files_from_hub(self) -> pd.DataFrame:
75
+ """Load individual CSV files from Hugging Face Hub."""
76
+ from huggingface_hub import hf_hub_download
77
+ import tempfile
78
+
79
+ print("📂 Loading individual CSV files from HF Hub...")
80
+
81
+ # Get list of CSV files
82
+ api = HfApi()
83
+ try:
84
+ repo_info = api.repo_info(repo_id=self.dataset_id, repo_type="dataset", token=self.hf_token)
85
+ csv_files = [f.rfilename for f in repo_info.siblings if f.rfilename.endswith('.csv')]
86
+ except Exception as e:
87
+ raise ValueError(f"Failed to get repo info: {e}")
88
+
89
+ if not csv_files:
90
+ raise ValueError("No CSV files found in the dataset repository")
91
+
92
+ print(f"📋 Found {len(csv_files)} CSV files")
93
+
94
+ all_dataframes = []
95
+
96
+ for csv_file in csv_files:
97
+ try:
98
+ # Download CSV file to temporary location
99
+ local_path = hf_hub_download(
100
+ repo_id=self.dataset_id,
101
+ filename=csv_file,
102
+ repo_type="dataset",
103
+ token=self.hf_token
104
+ )
105
+
106
+ # Read CSV with appropriate settings
107
+ # First, let's check if we need to skip the first row
108
+ df = pd.read_csv(local_path)
109
+
110
+ # If the first row contains "Interventions (sortie sous excel)", skip it
111
+ if df.columns[0].startswith('Interventions'):
112
+ df = pd.read_csv(local_path)
113
+ all_dataframes.append(df)
114
+ print(f" ✅ {csv_file}: {len(df)} rows")
115
+
116
+ except Exception as e:
117
+ print(f" ⚠️ Failed to load {csv_file}: {e}")
118
+ continue
119
+
120
+ if not all_dataframes:
121
+ raise ValueError("No CSV files could be loaded successfully")
122
+
123
+ # Combine all dataframes
124
+ combined_df = pd.concat(all_dataframes, ignore_index=True)
125
+ return combined_df
126
+
127
+ def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
128
+ """Preprocess the agricultural data."""
129
+ print(f"🔧 Preprocessing {len(df)} records...")
130
+ print(f"📋 Available columns: {list(df.columns)}")
131
+
132
+ # Convert date columns
133
+ date_columns = ['datedebut', 'datefin']
134
+ for col in date_columns:
135
+ if col in df.columns:
136
+ df[col] = pd.to_datetime(df[col], format='%d/%m/%y', errors='coerce')
137
+
138
+ # Convert numeric columns
139
+ numeric_columns = ['surfparc', 'quantitetot', 'neffqte', 'peffqte', 'kqte',
140
+ 'teneurn', 'teneurp', 'teneurk', 'keq', 'volumebo']
141
+ for col in numeric_columns:
142
+ if col in df.columns:
143
+ df[col] = pd.to_numeric(df[col], errors='coerce')
144
+
145
+ # Add derived columns (with error checking)
146
+ if 'millesime' in df.columns:
147
+ df['year'] = df['millesime']
148
+ else:
149
+ print("⚠️ Column 'millesime' not found, trying to infer year from filename or date")
150
+ # Try to extract year from date if available
151
+ if 'datedebut' in df.columns:
152
+ df['year'] = pd.to_datetime(df['datedebut'], errors='coerce').dt.year
153
+ else:
154
+ # Set a default year or raise error
155
+ print("❌ Cannot determine year - setting to 2024 as default")
156
+ df['year'] = 2024
157
+
158
+ if 'libelleusag' in df.columns:
159
+ df['crop_type'] = df['libelleusag']
160
+ else:
161
+ df['crop_type'] = 'unknown'
162
+
163
+ if 'libevenem' in df.columns:
164
+ df['intervention_type'] = df['libevenem']
165
+ else:
166
+ df['intervention_type'] = 'unknown'
167
+
168
+ if 'familleprod' in df.columns:
169
+ df['product_family'] = df['familleprod']
170
+ # Calculate IFT (Treatment Frequency Index) for herbicides
171
+ df['is_herbicide'] = df['familleprod'].str.contains('Herbicides', na=False)
172
+ df['is_fungicide'] = df['familleprod'].str.contains('Fongicides', na=False)
173
+ df['is_insecticide'] = df['familleprod'].str.contains('Insecticides', na=False)
174
+ else:
175
+ df['product_family'] = 'unknown'
176
+ df['is_herbicide'] = False
177
+ df['is_fungicide'] = False
178
+ df['is_insecticide'] = False
179
+
180
+ if 'nomparc' in df.columns:
181
+ df['plot_name'] = df['nomparc']
182
+ else:
183
+ df['plot_name'] = 'unknown'
184
+
185
+ if 'numparcell' in df.columns:
186
+ df['plot_number'] = df['numparcell']
187
+ else:
188
+ df['plot_number'] = 0
189
+
190
+ if 'surfparc' in df.columns:
191
+ df['plot_surface'] = df['surfparc']
192
+ else:
193
+ df['plot_surface'] = 1.0
194
+
195
+ print(f"✅ Preprocessing completed: {len(df)} records with {len(df.columns)} columns")
196
+ return df
197
+
198
+ def get_years_available(self) -> List[int]:
199
+ """Get list of available years in the data."""
200
+ df = self.load_all_files()
201
+ return sorted(df['year'].dropna().unique().astype(int).tolist())
202
+
203
+ def get_plots_available(self) -> List[str]:
204
+ """Get list of available plots."""
205
+ df = self.load_all_files()
206
+ return sorted(df['plot_name'].dropna().unique().tolist())
207
+
208
+ def get_crops_available(self) -> List[str]:
209
+ """Get list of available crop types."""
210
+ df = self.load_all_files()
211
+ return sorted(df['crop_type'].dropna().unique().tolist())
212
+
213
+ def filter_data(self,
214
+ years: Optional[List[int]] = None,
215
+ plots: Optional[List[str]] = None,
216
+ crops: Optional[List[str]] = None,
217
+ intervention_types: Optional[List[str]] = None) -> pd.DataFrame:
218
+ """Filter the data based on criteria."""
219
+ df = self.load_all_files()
220
+
221
+ if years:
222
+ df = df[df['year'].isin(years)]
223
+ if plots:
224
+ df = df[df['plot_name'].isin(plots)]
225
+ if crops:
226
+ df = df[df['crop_type'].isin(crops)]
227
+ if intervention_types:
228
+ df = df[df['intervention_type'].isin(intervention_types)]
229
+
230
+ return df
231
+
232
+ def get_herbicide_usage(self, years: Optional[List[int]] = None) -> pd.DataFrame:
233
+ """Get herbicide usage data for weed pressure analysis."""
234
+ df = self.filter_data(years=years)
235
+ herbicide_data = df[df['is_herbicide'] == True].copy()
236
+
237
+ # Group by plot, year, and crop
238
+ usage_summary = herbicide_data.groupby(['plot_name', 'year', 'crop_type']).agg({
239
+ 'quantitetot': 'sum',
240
+ 'produit': 'count', # Number of herbicide applications
241
+ 'surfparc': 'first'
242
+ }).reset_index()
243
+
244
+ usage_summary.columns = ['plot_name', 'year', 'crop_type', 'total_quantity', 'num_applications', 'plot_surface']
245
+ usage_summary['ift_herbicide'] = usage_summary['num_applications'] / usage_summary['plot_surface']
246
+
247
+ return usage_summary
248
+
249
+ def upload_to_huggingface(self) -> str:
250
+ """Upload data to Hugging Face dataset."""
251
+ if not self.hf_token:
252
+ raise ValueError("HF_TOKEN not provided")
253
+
254
+ df = self.load_all_files()
255
+ dataset = Dataset.from_pandas(df)
256
+
257
+ # Upload to Hugging Face
258
+ dataset.push_to_hub(
259
+ repo_id=self.dataset_id,
260
+ token=self.hf_token,
261
+ private=False
262
+ )
263
+
264
+ return f"Data uploaded to {self.dataset_id}"
265
+
266
+ def clear_cache(self):
267
+ """Clear cached data to force reload from Hugging Face."""
268
+ self.data_cache.clear()
269
+ print("📋 Cache cleared - will reload from Hugging Face on next access")
debug_app.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Debug application to identify ASGI error
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 create_simple_interface():
15
+ """Create a simplified interface to test"""
16
+ import gradio as gr
17
+
18
+ with gr.Blocks(title="Debug MCP") as demo:
19
+ gr.Markdown("# Debug MCP Interface")
20
+
21
+ with gr.Tab("Test"):
22
+ test_btn = gr.Button("Test Button")
23
+ test_output = gr.Markdown()
24
+
25
+ def test_function():
26
+ return "Test successful"
27
+
28
+ test_btn.click(test_function, outputs=[test_output])
29
+
30
+ return demo
31
+
32
+ def create_minimal_mcp_interface():
33
+ """Create minimal MCP interface"""
34
+ import gradio as gr
35
+ from mcp_server import mcp_registry
36
+
37
+ with gr.Blocks(title="MCP Debug") as demo:
38
+ gr.Markdown("# MCP Resources Debug")
39
+
40
+ with gr.Tab("Resources"):
41
+ list_btn = gr.Button("List Resources")
42
+ output = gr.Markdown()
43
+
44
+ def list_resources():
45
+ resources = mcp_registry.list_resources()
46
+ result = "## MCP Resources\n\n"
47
+ for uri, info in resources.items():
48
+ result += f"- `{uri}`: {info['description']}\n"
49
+ return result
50
+
51
+ list_btn.click(list_resources, outputs=[output])
52
+
53
+ return demo
54
+
55
+ if __name__ == "__main__":
56
+ print("Testing simple interface...")
57
+ try:
58
+ demo = create_simple_interface()
59
+ print("✅ Simple interface created")
60
+ except Exception as e:
61
+ print(f"❌ Simple interface error: {e}")
62
+
63
+ print("Testing minimal MCP interface...")
64
+ try:
65
+ demo = create_minimal_mcp_interface()
66
+ print("✅ Minimal MCP interface created")
67
+ except Exception as e:
68
+ print(f"❌ Minimal MCP interface error: {e}")
69
+
70
+ print("Testing full MCP interface...")
71
+ try:
72
+ demo = create_mcp_interface()
73
+ print("✅ Full MCP interface created")
74
+ except Exception as e:
75
+ print(f"❌ Full MCP interface error: {e}")
76
+ import traceback
77
+ traceback.print_exc()
mcp_server.py ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )
152
+ data_year_end = gr.Slider(
153
+ minimum=2014,
154
+ maximum=2025,
155
+ value=2025,
156
+ step=1,
157
+ label="Année de fin"
158
+ )
159
+ data_plot_filter = gr.Dropdown(
160
+ choices=get_available_plots(),
161
+ value="Toutes",
162
+ label="Filtrer par parcelle"
163
+ )
164
+ data_crop_filter = gr.Dropdown(
165
+ choices=get_available_crops(),
166
+ value="Toutes",
167
+ label="Filtrer par culture"
168
+ )
169
+ data_intervention_filter = gr.Dropdown(
170
+ choices=get_available_interventions(),
171
+ value="Toutes",
172
+ label="Filtrer par type d'intervention"
173
+ )
174
+ explore_btn = gr.Button("🔍 Explorer les Données", variant="primary")
175
+
176
+ with gr.Row():
177
+ data_plot = gr.Plot()
178
+ data_summary = gr.Markdown()
179
+
180
+ explore_btn.click(
181
+ explore_raw_data,
182
+ inputs=[data_year_start, data_year_end, data_plot_filter, data_crop_filter, data_intervention_filter],
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
+ - `dataset://years` - Liste des millésimes disponibles
192
+ - `exploitation://{siret}/{millesime}` - Informations d'exploitation
193
+ - `parcelle://{millesime}/{numparcell}` - Informations de parcelle
194
+ - `intervention://{millesime}/{numparcell}/{rang}` - Informations d'intervention
195
+ - `intrant://{codeamm}` - Informations d'intrant
196
+ - `materiel://{millesime}/{numparcell}/{rang}` - Informations de matériel
197
+ - `maindoeuvre://{millesime}/{numparcell}/{rang}` - Informations de main d'œuvre
198
+ """)
199
+
200
+ with gr.Row():
201
+ with gr.Column():
202
+ gr.Markdown("#### Test des Resources MCP")
203
+
204
+ # Test Years
205
+ years_btn = gr.Button("📅 Liste des Années", variant="primary")
206
+
207
+ # Test Exploitation
208
+ with gr.Row():
209
+ siret_input = gr.Textbox(
210
+ label="SIRET Exploitation",
211
+ value="18560001000016",
212
+ placeholder="18560001000016"
213
+ )
214
+ siret_millesime_input = gr.Textbox(
215
+ label="Millesime",
216
+ value="2025",
217
+ placeholder="2025"
218
+ )
219
+ siret_btn = gr.Button("🏢 Test Exploitation", variant="secondary")
220
+
221
+ # Test Parcelle
222
+ with gr.Row():
223
+ parcelle_millesime_input = gr.Textbox(
224
+ label="Millesime",
225
+ value="2025",
226
+ placeholder="2025"
227
+ )
228
+ parcelle_input = gr.Textbox(
229
+ label="Numéro Parcelle",
230
+ value="12",
231
+ placeholder="12"
232
+ )
233
+ parcelle_btn = gr.Button("🏞️ Test Parcelle", variant="secondary")
234
+
235
+ # Test Intervention
236
+ with gr.Row():
237
+ intervention_millesime_input = gr.Textbox(
238
+ label="Millesime",
239
+ value="2025",
240
+ placeholder="2025"
241
+ )
242
+ intervention_parcelle_input = gr.Textbox(
243
+ label="Num Parcelle",
244
+ value="12",
245
+ placeholder="12"
246
+ )
247
+ intervention_rang_input = gr.Textbox(
248
+ label="Rang",
249
+ value="1",
250
+ placeholder="1"
251
+ )
252
+ intervention_btn = gr.Button("⚙️ Test Intervention", variant="secondary")
253
+
254
+ # Test Intrant
255
+ with gr.Row():
256
+ intrant_input = gr.Textbox(
257
+ label="Code AMM Intrant",
258
+ value="9100296",
259
+ placeholder="9100296"
260
+ )
261
+ intrant_millesime_input = gr.Textbox(
262
+ label="Millesime (optionnel)",
263
+ value="",
264
+ placeholder="2025"
265
+ )
266
+ intrant_btn = gr.Button("🧪 Test Intrant", variant="secondary")
267
+
268
+ # Test Matériel
269
+ with gr.Row():
270
+ materiel_millesime_input = gr.Textbox(
271
+ label="Millesime",
272
+ value="2025",
273
+ placeholder="2025"
274
+ )
275
+ materiel_parcelle_input = gr.Textbox(
276
+ label="Num Parcelle",
277
+ value="12",
278
+ placeholder="12"
279
+ )
280
+ materiel_rang_input = gr.Textbox(
281
+ label="Rang",
282
+ value="1",
283
+ placeholder="1"
284
+ )
285
+ materiel_btn = gr.Button("🔧 Test Matériel", variant="secondary")
286
+
287
+ with gr.Column():
288
+ gr.Markdown("#### Résultat")
289
+ resource_output = gr.JSON(label="Résultat de la resource")
290
+
291
+ # Connexions des boutons
292
+ years_btn.click(
293
+ lambda: resources.list_years(),
294
+ outputs=[resource_output]
295
+ )
296
+ siret_btn.click(
297
+ lambda siret, millesime: resources.get_exploitation(siret, millesime),
298
+ inputs=[siret_input, siret_millesime_input],
299
+ outputs=[resource_output]
300
+ )
301
+ parcelle_btn.click(
302
+ lambda millesime, parcelle: resources.get_parcelle(millesime, parcelle),
303
+ inputs=[parcelle_millesime_input, parcelle_input],
304
+ outputs=[resource_output]
305
+ )
306
+ intervention_btn.click(
307
+ lambda millesime, parcelle, rang: resources.get_intervention(millesime, parcelle, rang),
308
+ inputs=[intervention_millesime_input, intervention_parcelle_input, intervention_rang_input],
309
+ outputs=[resource_output]
310
+ )
311
+ intrant_btn.click(
312
+ lambda codeamm, millesime: resources.get_intrant(codeamm, millesime if millesime else None),
313
+ inputs=[intrant_input, intrant_millesime_input],
314
+ outputs=[resource_output]
315
+ )
316
+ materiel_btn.click(
317
+ lambda millesime, parcelle, rang: resources.get_materiel(millesime, parcelle, rang),
318
+ inputs=[materiel_millesime_input, materiel_parcelle_input, materiel_rang_input],
319
+ outputs=[resource_output]
320
+ )
321
+
322
+ return demo
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio[mcp]>=5.46.0
2
+ pandas>=2.0.0
3
+ numpy>=1.24.0
4
+ plotly>=5.0.0
5
+ datasets>=2.14.0
6
+ huggingface_hub>=0.16.0
7
+ matplotlib>=3.7.0
8
+ seaborn>=0.12.0
test_fixed_resources.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test de l'application avec resources corrigées
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_fixed_resources():
15
+ """Test de l'application avec resources corrigées"""
16
+ print("🧪 Test de l'application avec resources corrigées...")
17
+
18
+ try:
19
+ demo = create_mcp_interface()
20
+ print("✅ Interface créée avec succès")
21
+
22
+ # Test des resources
23
+ from agricultural_mcp.resources import AgriculturalResources
24
+ resources = AgriculturalResources()
25
+
26
+ print("✅ Resources MCP initialisées")
27
+
28
+ # Test d'une resource simple
29
+ years = resources.list_years()
30
+ print(f"✅ Test list_years: {len(years)} années trouvées")
31
+
32
+ print("\n🎯 Application avec resources corrigées fonctionnelle !")
33
+ print("📋 5 onglets disponibles")
34
+ print("🔧 Onglet MCP Resources avec test des resources corrigées")
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_fixed_resources()
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()