diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..43cc5fa Binary files /dev/null and b/.DS_Store differ diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..4f5feb4 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,137 @@ +# đ Guide de dĂ©marrage - immo-agent + +## Installation rapide + +```bash +# 1. Cloner et entrer dans le projet +git clone https://github.com/Arno37/immo-agent.git +cd immo-agent + +# 2. Configurer l'environnement +cp .env.example .env +# Ăditer .env avec ta clĂ© MISTRAL_API_KEY + +# 3. Installer les dĂ©pendances +pip install -r requirements.txt +# ou pour dev : pip install -e ".[dev]" +``` + +## đŻ Utiliser le projet + +### Option 1ïžâŁ : API Web (RecommandĂ©) +```bash +python scripts/run_api.py +``` +Puis ouvrez : **http://127.0.0.1:8000** + +### Option 2ïžâŁ : CLI interactive +```bash +python scripts/run_cli.py +``` + +### Option 3ïžâŁ : Importer comme module Python +```python +from immo_agent.runIA import outil_dvf_estimation + +# Estimer un bien +estimation = outil_dvf_estimation( + ville="Paris", + surface="120", + type_bien="Maison", + etat="bon" +) +print(estimation) +``` + +## đ Architecture du projet + +**Voir [STRUCTURE.md](STRUCTURE.md)** pour la description complĂšte + +Dossiers clĂ©s : +- đŠ **immo_agent/** â Code source (dĂ©veloppeurs) +- đŻ **scripts/** â Points d'entrĂ©e (utilisateurs) +- đš **frontend/** â Interface web +- đ **data/** â Bases de donnĂ©es +- đ§Ș **tests/** â Tests unitaires + +## âïž Configuration + +Les paramĂštres se trouvent dans 2 fichiers : + +1. **`.env`** (secrets) + ``` + MISTRAL_API_KEY=sk_... + GROQ_API_KEY=gsk_... + ``` + +2. **`config/settings.py`** (paramĂštres publics) + ```python + DB_PATH = "data/immo_ventes.db" + API_PORT = 8000 + DEFAULT_MODEL = "mistral-large-latest" + ``` + +## â VĂ©rifier l'installation + +```bash +python immo_agent/check_setup.py +``` + +## đ§Ș Lancer les tests + +```bash +pytest tests/ +# ou avec couverture : pytest --cov=immo_agent tests/ +``` + +## đ Documentation + +- [STRUCTURE.md](STRUCTURE.md) - Organisation du projet +- [README.md](README.md) - Description gĂ©nĂ©rale +- [documentation/](documentation/) - SpĂ©cifications dĂ©taillĂ©es + +## đ Troubleshooting + +| ProblĂšme | Solution | +|----------|----------| +| `ModuleNotFoundError: immo_agent` | Installer avec `pip install -e .` | +| Port 8000 occupĂ© | `python scripts/run_api.py --port 8001` | +| ClĂ© API manquante | CrĂ©er `.env` avec `MISTRAL_API_KEY=...` | +| BD non trouvĂ©e | Lancer `python immo_agent/csv_to_sqlite.py` | + +## đĄ DĂ©veloppement + +```bash +# Format le code +black immo_agent/ + +# VĂ©rifier les types +mypy immo_agent/ + +# Lint +flake8 immo_agent/ + +# DĂ©veloppement en mode auto-reload +python scripts/run_api.py # reload=True par dĂ©faut +``` + +## đŠ Structure recommandĂ©e pour ajouter du code + +```python +# immo_agent/features/ma_feature.py +from immo_agent.runIA import model +from config import DB_PATH + +def ma_fonction(): + """Ma nouvelle fonction bien organisĂ©e.""" + pass +``` + +Puis importer : +```python +from immo_agent.features import ma_fonction +``` + +--- + +**Besoin d'aide ?** Consultez [STRUCTURE.md](STRUCTURE.md) ou les docstrings du code. diff --git a/README.md b/README.md index 5846762..c6443b4 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ L'agent support les demandes comme : ``` immo-agent/ -âââ runIA.py # DĂ©finition des outils IA (DVF, estimations) -âââ api.py # API FastAPI web -âââ main.py # CLI chat (WIP - dĂ©pendances manquantes) +âââ immo_agent/ # code Python principal (package) +â âââ runIA.py # DĂ©finition des outils IA (DVF, estimations) +â âââ api.py # API FastAPI web +â âââ main.py # CLI chat (WIP - dĂ©pendances manquantes) âââ frontend/ # Interface web React âââ data/ â âââ immo_ventes.db # Base de donnĂ©es SQLite diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..70dabc7 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,100 @@ +# đ Structure du projet immo-agent + +``` +immo-agent/ +â +âââ immo_agent/ đŠ Package principal (code source) +â âââ __init__.py +â âââ runIA.py đ§ DĂ©finition des outils IA & LLM +â âââ api.py đ API FastAPI +â âââ main.py đŹ CLI interactive +â âââ agent_memory.py đ§ Gestion de la mĂ©moire (langgraph) +â âââ api_auth.py đ Authentification +â âââ db_session.py đŸ Gestion sessions BD +â âââ check_setup.py â VĂ©rification configuration +â âââ csv_to_sqlite.py đ Import donnĂ©es CSV +â âââ init_and_run.py đ Initialisation +â âââ test_mcp.py đ§Ș Tests MCP +â âââ weather_mcp.py đ€ïž MCP serveur mĂ©tĂ©o +â +âââ scripts/ đŻ Points d'entrĂ©e +â âââ run_api.py â python scripts/run_api.py +â âââ run_cli.py â python scripts/run_cli.py +â +âââ frontend/ đš Interface web +â âââ index.html +â âââ script.js +â âââ style.css +â âââ auth.js +â +âââ data/ đ DonnĂ©es +â âââ immo_ventes.db (SQLite - DVF data) +â âââ memory.db (historique conversations) +â âââ auth_system.db +â âââ ValeursFoncieres-2025-S1.csv +â +âââ documentation/ đ Documentation +â âââ benchmark projIA.md +â âââ note-cadrage projIA.md +â +âââ exploration/ đ Scripts d'exploration +â âââ expl_mcp.py +â âââ expl_mcp2.py +â âââ expl_mcp_full.py +â âââ explograph.py +â âââ explograph2.py +â âââ explotools.py +â +âââ tests/ đ§Ș Tests unitaires +â âââ __init__.py +â +âââ config/ âïž Configuration +â âââ settings.py (paramĂštres globaux) +â +âââ .env.example (Ă copier en .env) +âââ .gitignore +âââ README.md (guide utilisateur) +âââ pyproject.toml (configuration build/dependencies) +âââ requirements.txt (dĂ©pendances pip) +``` + +## đ Comment utiliser + +### Via API Web +```bash +python scripts/run_api.py +# â http://127.0.0.1:8000 +``` + +### Via CLI +```bash +python scripts/run_cli.py +``` + +### Importer le package +```python +from immo_agent.runIA import outil_dvf_historique, outil_dvf_estimation +from immo_agent.api import app +from immo_agent.main import chat_loop +``` + +## đ Organisation par rĂŽle + +| Dossier | Usage | Qui ? | +|---------|-------|-------| +| `immo_agent/` | Code mĂ©tier | DĂ©veloppeurs | +| `scripts/` | Lancer l'app | Utilisateurs finaux | +| `frontend/` | Interface | Frontend dev | +| `data/` | BD, donnĂ©es | DonnĂ©es scientifique | +| `tests/` | Validation | QA / CI-CD | +| `config/` | ParamĂštres | DevOps / Config | +| `documentation/` | SpĂ©cs | Analystes | +| `exploration/` | Prototypage | Recherche | + +## âš Avantages de cette structure + +â **Clair** : chaque dossier a un rĂŽle distinct +â **Scalable** : facile d'ajouter des modules +â **Professionnel** : suit les conventions Python +â **Maintenable** : organisation logique +â **Extensible** : prĂȘt pour CI/CD et tests diff --git a/add_gps_columns.py b/add_gps_columns.py new file mode 100644 index 0000000..49af1a9 --- /dev/null +++ b/add_gps_columns.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Script pour ajouter les colonnes latitude et longitude Ă la BD.""" + +import sqlite3 +import os + +db_path = "data/immo_ventes.db" + +if not os.path.exists(db_path): + print(f"â BD non trouvĂ©e: {db_path}") + exit(1) + +try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # RĂ©cupĂ©rer le schĂ©ma actuel + cursor.execute("PRAGMA table_info(transactions)") + colonnes = [col[1] for col in cursor.fetchall()] + + print(f"đ Colonnes existantes: {colonnes}\n") + + # Ajouter latitude si elle n'existe pas + if "latitude" not in colonnes: + print("â Ajout colonne: latitude") + cursor.execute("ALTER TABLE transactions ADD COLUMN latitude FLOAT") + print("â Colonne latitude ajoutĂ©e") + else: + print("â Colonne latitude existe dĂ©jĂ ") + + # Ajouter longitude si elle n'existe pas + if "longitude" not in colonnes: + print("â Ajout colonne: longitude") + cursor.execute("ALTER TABLE transactions ADD COLUMN longitude FLOAT") + print("â Colonne longitude ajoutĂ©e") + else: + print("â Colonne longitude existe dĂ©jĂ ") + + conn.commit() + conn.close() + + print("\nâ BD mise Ă jour avec succĂšs !") + +except sqlite3.OperationalError as e: + print(f"â Erreur: {e}") + print(" â La table 'transactions' n'existe peut-ĂȘtre pas") + print(" â VĂ©rifiez le nom exact de votre table dans la BD") + exit(1) +except Exception as e: + print(f"â Erreur: {e}") + exit(1) diff --git a/app/main.py b/app/main.py index 97a4a81..f538db4 100644 --- a/app/main.py +++ b/app/main.py @@ -9,9 +9,9 @@ app.include_router(IA_chat.router) -app.mount("/static", StaticFiles(directory="static"), name="static") -templates = Jinja2Templates(directory="templates") +app.mount("/static", StaticFiles(directory="frontend"), name="static") +templates = Jinja2Templates(directory="frontend") @app.get("/", response_class=HTMLResponse) def read_root(request: Request): - return templates.TemplateResponse("chat.html", {"request": request}) + return templates.TemplateResponse("index.html", {"request": request}) diff --git a/app/routers/IA_chat.py b/app/routers/IA_chat.py index 8df1eb9..bc59ca0 100644 --- a/app/routers/IA_chat.py +++ b/app/routers/IA_chat.py @@ -1,13 +1,29 @@ from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse, JSONResponse +from pydantic import BaseModel +from app.services.agent import async_run_agent + router = APIRouter() -@router.get("/chat", response_class=HTMLResponse) -def chat_page(request: Request): - return JSONResponse({"message": "Bienvenue sur la page de chat!"}) +class ChatRequest(BaseModel): + message: str -@router.post("/chat") -def chat_post(request: Request): - # Ici, tu peux traiter le message utilisateur - return JSONResponse({"message": "Message reçu!"}) +@router.post("/api/chat") +async def chat_post(req: ChatRequest): + try: + # Appel Ă la fonction run_agent asynchrone + # On utilise une session ("default") pour l'instant + result = await async_run_agent(req.message, "default_session") + + # Le retour est gĂ©nĂ©ralement un dict avec une clĂ© "messages" (langgraph) + reply_content = "" + if result and "messages" in result and len(result["messages"]) > 0: + last_msg = result["messages"][-1] + reply_content = last_msg.content if hasattr(last_msg, 'content') else str(last_msg) + else: + reply_content = str(result) + + return {"reply": reply_content} + except Exception as e: + return {"reply": f"â ïž Erreur cĂŽtĂ© serveur: {str(e)}"} diff --git a/app/services/agent.py b/app/services/agent.py index e4008d3..331e40e 100644 --- a/app/services/agent.py +++ b/app/services/agent.py @@ -17,8 +17,137 @@ class BienImmobilier(BaseModel): surface : float = Field(description="Surface area in square meters") rooms: int = Field(description="Number of rooms") type : str = Field(description="maison ou appartement") - +class HistoriqueVentesInput(BaseModel): + """Input for historical sales data queries.""" + location: str = Field(description="City name or coordinates") + years: int = Field(description="Number of past years to consider") + +class BienSimilairesInput(BaseModel): + """Input for searching similar properties within a range.""" + ville: str = Field(description="City name") + surface_min: float = Field(description="Minimum surface area in square meters") + surface_max: float = Field(description="Maximum surface area in square meters") + type_bien: str = Field(description="Type of property (maison or appartement)") + rayon_km: float = Field(description="Radius in kilometers to consider", default=10) + +@tool +def historique_ventes(input: HistoriqueVentesInput): + """RĂ©cupĂšre l'historique des ventes immobiliĂšres pour une localisation donnĂ©e et une pĂ©riode spĂ©cifiĂ©e. + Args: + input (HistoriqueVentesInput): Les critĂšres de recherche pour l'historique des ventes. + Returns: + list: Une liste d'enregistrements de ventes immobiliĂšres correspondant aux critĂšres. + """ + # Simuler une rĂ©ponse de la base de donnĂ©es DVF + ventes = [ + {"location": input.location, "surface": 70, "rooms": 3, "type": "appartement", "price": 350000, "date": "2023-05-10"}, + {"location": input.location, "surface": 120, "rooms": 5, "type": "maison", "price": 600000, "date": "2022-11-20"}, + # ... plus de donnĂ©es simulĂ©es + ] + return json.dumps(ventes) + +@tool +def biens_similaires(params: BienSimilairesInput): + """Trouve des biens immobiliers similaires dans un pĂ©rimĂštre donnĂ©. + + Cherche les biens qui ont la mĂȘme surface (±10%) et le mĂȘme type + Ă moins de X km de la ville spĂ©cifiĂ©e. + """ + import sqlite3 + import requests + from math import radians, cos, sin, asin, sqrt + + # ĂTAPE 1: RĂ©cupĂ©rer les coordonnĂ©es de la ville + try: + response = requests.get( + "https://geo.api.gouv.fr/communes", + params={"nom": params.ville, "limit": 1} + ) + data = response.json() + if not data: + return f"â Ville '{params.ville}' non trouvĂ©e" + + ref_lat = data[0]["centre"]["coordinates"][1] + ref_lon = data[0]["centre"]["coordinates"][0] + except Exception as e: + return f"â Erreur gĂ©olocalisation: {e}" + + # ĂTAPE 2: Chercher en BD les biens similaires + try: + conn = sqlite3.connect("data/immo_ventes.db") + cursor = conn.cursor() + + # Biens avec surface similaire (±10%) + cursor.execute(""" + SELECT + Commune, + Voie, + "Valeur fonciere", + "Surface reelle bati", + latitude, + longitude + FROM transactions + WHERE "Type local" = ? + AND "Surface reelle bati" BETWEEN ? AND ? + AND latitude IS NOT NULL + AND longitude IS NOT NULL + LIMIT 100 + """, [ + params.type_bien.capitalize(), + params.surface_min * 0.9, + params.surface_max * 1.1 + ]) + + biens = cursor.fetchall() + conn.close() + + if not biens: + return f"â Aucun {params.type_bien} trouvĂ© avec surface {params.surface_min}-{params.surface_max}mÂČ" + + # ĂTAPE 3: Calculer les distances et filtrer + biens_proches = [] + + for commune, voie, prix, surface, lat, lon in biens: + # Formule Haversine + lat1, lon1, lat2, lon2 = map(radians, [ref_lat, ref_lon, lat, lon]) + dlat = lat2 - lat1 + dlon = lon2 - lon1 + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 + c = 2 * asin(sqrt(a)) + distance = c * 6371 # Rayon Terre en km + + # Garder seulement ceux dans le rayon + if distance <= params.rayon_km: + biens_proches.append({ + "commune": commune, + "adresse": voie, + "prix": prix, + "surface": surface, + "distance": round(distance, 1) + }) + + # ĂTAPE 4: Retourner rĂ©sultats formatĂ©s + if not biens_proches: + return f"â Aucun {params.type_bien} Ă {params.surface_min}-{params.surface_max}mÂČ dans {params.rayon_km}km" + + # Trier par distance + biens_proches.sort(key=lambda x: x["distance"]) + + # Formatter le texte pour le LLM + resultat = f"â TrouvĂ© {len(biens_proches)} {params.type_bien}(s) similaire(s) Ă {params.rayon_km}km de {params.ville}:\n\n" + + for i, bien in enumerate(biens_proches[:10], 1): + resultat += f"{i}. {bien['commune']} - {bien['adresse']}\n" + resultat += f" đ° {int(bien['prix'])}⏠| đ {bien['surface']}mÂČ | đ {bien['distance']}km\n\n" + + if len(biens_proches) > 10: + resultat += f"... et {len(biens_proches) - 10} autres" + + return resultat + + except Exception as e: + return f"â Erreur BD: {str(e)}" @tool def estimation_prix(bien: BienImmobilier): @@ -26,29 +155,86 @@ def estimation_prix(bien: BienImmobilier): Args: bien (BienImmobilier): Les caractĂ©ristiques du bien immobilier Ă estimer. Returns: - float: Une estimation du prix du bien immobilier. + str: Une estimation du prix du bien immobilier. """ prix = bien.surface * 5000 if bien.type == "maison": prix *= 1.2 + + return f"Estimation du prix pour un {bien.type} de {bien.surface}mÂČ avec {bien.rooms} piĂšces Ă {bien.location} : {int(prix)}âŹ" - return prix - return f"Estimation du prix pour un {bien.type} de {bien.surface}mÂČ avec {bien.rooms} piĂšces Ă {bien.location} : {prix}⏠" llm = ChatMistralAI(model="mistral-large-latest") -system_prompt = """Tu es un assistant d'estimation de prix pour les biens immobiliers. Utilise les outils a ta disposition et n'invente aucune chiffre""" +def extract_last_city(thread_id): + """Extrait la derniĂšre ville mentionnĂ©e dans l'historique de la conversation.""" + try: + config = {"configurable": {"thread_id": thread_id}} + state = agent.get_state(config=config) + + if not state or not state.values.get("messages"): + return None + + # Parcourir les messages en ordre inverse pour trouver la derniĂšre ville mentionnĂ©e + messages = state.values.get("messages", []) + + villes_communes = [ + "Tours", "Paris", "Bordeaux", "Lyon", "Marseille", "Toulouse", + "Nice", "Nantes", "Strasbourg", "Montpellier", "Lille", "Rennes", + "Reims", "Le Havre", "Saint-Ătienne", "Toulon", "Grenoble", "Angers", + "Saint-Denis", "Villeurbanne", "NĂźmes", "Clermont-Ferrand" + ] + + for msg in reversed(messages): + content = msg.content if hasattr(msg, 'content') else str(msg.get("content", "")) + for ville in villes_communes: + if ville.lower() in content.lower(): + return ville + + return None + except: + return None + + +system_prompt = """Tu es un assistant expert en biens immobiliers. Tu aides l'utilisateur Ă rechercher des propriĂ©tĂ©s, estimer des prix et trouver des biens similaires. -# Ajout du tool estimation_prix -tools = [estimation_prix] +đ RĂGLES IMPORTANTES : +1. Utilise TOUJOURS les outils disponibles pour rĂ©pondre aux requĂȘtes (estimation_prix, historique_ventes, biens_similaires) +2. N'invente JAMAIS de chiffres ou de prix +3. Si l'utilisateur ne mentionne pas une ville dans sa requĂȘte actuelle, utilise AUTOMATIQUEMENT la DERNIĂRE VILLE mentionnĂ©e dans la conversation +4. MĂ©morise chaque nouvelle ville mentionnĂ©e pour les requĂȘtes futures +5. Si aucune ville n'a Ă©tĂ© mentionnĂ©e, demande-la avant de rechercher + +đ EXEMPLE : +- L'utilisateur dit : "Je cherche une maison Ă Tours" +- Ensuite : "Je cherche une maison de 100mÂČ dans un rayon de 120km" +- â Tu dois utiliser Tours automatiquement, car c'est la derniĂšre ville mentionnĂ©e""" + +# Ajout des tools +tools = [estimation_prix, historique_ventes, biens_similaires] agent = create_agent(model=llm, tools=tools, system_prompt=system_prompt, checkpointer=InMemorySaver()) #agent_executor = AgentExecutor(agent=agent, tools=tools) #@traceable -async def async_run_agent(user_message,thread_id): +async def async_run_agent(user_message, thread_id): + """ExĂ©cute l'agent avec mĂ©moire de la derniĂšre ville mentionnĂ©e.""" config = {"configurable": {"thread_id": thread_id}} - messages = {"messages":[{"role":"user", "content": user_message}]} + + # Extraire la derniĂšre ville mentionnĂ©e pour l'ajouter au contexte si nĂ©cessaire + last_city = extract_last_city(thread_id) + + # Augmenter le message utilisateur si pas de ville mentionnĂ©e mais une existante + augmented_message = user_message + if last_city and not any( + city.lower() in user_message.lower() + for city in ["Tours", "Paris", "Bordeaux", "Lyon", "Marseille", "Toulouse", + "Nice", "Nantes", "Strasbourg", "Montpellier", "Lille", "Rennes", + "Reims", "Le Havre", "Saint-Ătienne", "Toulon", "Grenoble", "Angers"] + ): + augmented_message = f"{user_message} (DerniĂšre ville mentionnĂ©e: {last_city})" + + messages = {"messages": [{"role": "user", "content": augmented_message}]} result = await agent.ainvoke(messages, config=config) return result diff --git a/app/templates/chat.html b/app/templates/chat.html new file mode 100644 index 0000000..350889c --- /dev/null +++ b/app/templates/chat.html @@ -0,0 +1,30 @@ + + +
+