Spaces:
Sleeping
Sleeping
Eddyhzd commited on
Commit ·
37ac408
1
Parent(s): 0b6912d
first commit
Browse files- .gitignore +67 -0
- PROMPT.md +97 -0
- README.md +122 -4
- agricultural_mcp/__init__.py +8 -0
- agricultural_mcp/prompts.py +0 -0
- agricultural_mcp/resources.py +237 -0
- agricultural_mcp/tools.py +577 -0
- app.py +15 -0
- data_loader.py +269 -0
- debug_app.py +77 -0
- mcp_server.py +322 -0
- requirements.txt +8 -0
- test_fixed_resources.py +47 -0
- test_new_structure.py +47 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.46.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|