diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 77b7440..d099d31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,6 @@ jobs: - name: Install dependencies run: | uv sync --dev - uv sync --dev --extra phoenix - name: Setup environment file run: cp .env.example .env diff --git a/.gitignore b/.gitignore index 1c0dd72..494bece 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ coverage.xml .coverage .scannerwork/ opencode.json -.vscode/ \ No newline at end of file +.vscode/ +agents \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fe52321..ea99ba8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,6 @@ COPY --from=builder /app/.venv /app/.venv # Copy application source and agent configs COPY src/ /app/src/ -COPY agents/ /app/agents/ # Set Python path and venv ENV PYTHONPATH=/app:$PYTHONPATH @@ -33,4 +32,4 @@ USER appuser EXPOSE 8000 -CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["python", "-m", "src.main"] diff --git a/agents/code-reviewer.yaml b/agents/code-reviewer.yaml deleted file mode 100644 index dc53b8a..0000000 --- a/agents/code-reviewer.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: code-reviewer -model: "openai:anthropic/claude-haiku-4.5:nitro" -system_prompt: | - You are an expert code reviewer. Analyze code for correctness, - performance, security, and maintainability. -middleware: - - filesystem - - sub_agent -backend: - type: state -hitl: - rules: - write_file: true - execute: - allowed_decisions: - - approve - - reject -subagents: - - name: security-auditor - model: "openai:anthropic/claude-haiku-4.5:nitro" - description: "Specialized in security vulnerability analysis" - instructions: "Focus on OWASP Top 10 and common security patterns" - - name: performance-analyst - model: "openai:anthropic/claude-haiku-4.5:nitro" - description: "Specialized in performance optimization" - instructions: "Analyze time complexity, memory usage, and bottlenecks" diff --git a/agents/data-extractor.yaml b/agents/data-extractor.yaml deleted file mode 100644 index b3afc5f..0000000 --- a/agents/data-extractor.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: data-extractor -model: "openai:anthropic/claude-haiku-4.5:nitro" -system_prompt: "You are a structured data extractor. Extract weather information from user messages." - -response_format: - type: object - properties: - temperature: - type: number - description: "Temperature in Celsius" - condition: - type: string - description: "Weather condition (sunny, cloudy, rainy, snowy, etc.)" - location: - type: string - description: "Location mentioned in the message" - required: - - temperature - - condition - - location diff --git a/agents/example-agent.yaml b/agents/example-agent.yaml deleted file mode 100644 index 26d0dfe..0000000 --- a/agents/example-agent.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: example-agent -model: "openai:anthropic/claude-haiku-4.5:nitro" -system_prompt: "You are a helpful assistant." diff --git a/agents/mcp-agent.yaml b/agents/mcp-agent.yaml deleted file mode 100644 index 90cf186..0000000 --- a/agents/mcp-agent.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: mcp-agent -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: "You are an agent with MCP tool access." -mcp_servers: - - name: filesystem - transport: stdio - command: npx - args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] diff --git a/agents/minimal.yaml b/agents/minimal.yaml deleted file mode 100644 index a448022..0000000 --- a/agents/minimal.yaml +++ /dev/null @@ -1 +0,0 @@ -name: minimal-agent diff --git a/agents/rag.yaml b/agents/rag.yaml deleted file mode 100644 index 879dbd6..0000000 --- a/agents/rag.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: rag -model: google/gemini-2.5-flash:nitro -system_prompt: | - You are a knowledge base assistant powered by RAG (Retrieval-Augmented Generation). - You answer user questions by querying an indexed knowledge base using the query_knowledge_base tool. - - ## Tool Usage - - Always use the query_knowledge_base tool to retrieve relevant information before answering. - Do not fabricate information -- if the knowledge base does not contain the answer, say so. - - ### Parameters - - - working_dir (required): The knowledge base directory to query. Ask the user which knowledge base - to use if not clear from context. - - query (required): A clear, specific search query derived from the user's question. - Rephrase the user's question into an effective search query when needed. - - mode (optional, default "naive"): The retrieval strategy. - - "naive": Simple keyword/vector search. Good for straightforward factual queries. - - "local": Focuses on specific entities and their relationships. Use for detailed questions - about particular topics. - - "global": Provides a broad overview using community summaries. Use for high-level or - thematic questions. - - "hybrid": Combines local and global strategies. Use when a question spans both specific - details and broader context. - - "mix": Combines naive, local, and global. Use for complex questions requiring comprehensive - coverage. - - top_k (optional, default 10): Number of results to retrieve. Increase for broad questions, - decrease for precise lookups. - - ## Response Guidelines - - - Cite or reference the source material when possible. - - Provide structured, clear answers with appropriate formatting. - - If the initial query returns insufficient results, try rephrasing or using a different mode. - - When the user's question is ambiguous, ask for clarification before querying. -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-extractor-gemini.yaml b/agents/real-estate-extractor-gemini.yaml deleted file mode 100644 index 24d9349..0000000 --- a/agents/real-estate-extractor-gemini.yaml +++ /dev/null @@ -1,408 +0,0 @@ -name: real-estate-extractor-gemini -model: openai:google/gemini-2.5-flash:nitro -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Structure attendue : - {"financial_project": {...}, "property": {...}, "company": {...}, "missing_fields": [...]} - -middleware: - - sub_agent - -response_format: - title: RealEstateExtraction - type: object - properties: - financial_project: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - property: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - company: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" - missing_fields: - type: array - description: "Liste des champs non trouvés (null) au format groupe.champ" - items: - type: string - required: - - financial_project - - property - - company - - missing_fields - -subagents: - - name: financial-extractor - model: openai:google/gemini-2.5-flash:nitro - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:google/gemini-2.5-flash:nitro - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:google/gemini-2.5-flash:nitro - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-extractor-gpt4o-mini.yaml b/agents/real-estate-extractor-gpt4o-mini.yaml deleted file mode 100644 index fc0ee78..0000000 --- a/agents/real-estate-extractor-gpt4o-mini.yaml +++ /dev/null @@ -1,318 +0,0 @@ -name: real-estate-extractor-gpt4o-mini -model: openai:openai/gpt-4o-mini -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -middleware: - - sub_agent - -subagents: - - name: financial-extractor - model: openai:openai/gpt-4o-mini - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:openai/gpt-4o-mini - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:openai/gpt-4o-mini - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-extractor-gpt4o.yaml b/agents/real-estate-extractor-gpt4o.yaml deleted file mode 100644 index fa07227..0000000 --- a/agents/real-estate-extractor-gpt4o.yaml +++ /dev/null @@ -1,318 +0,0 @@ -name: real-estate-extractor-gpt4o -model: openai:openai/gpt-4o -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -middleware: - - sub_agent - -subagents: - - name: financial-extractor - model: openai:openai/gpt-4o - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:openai/gpt-4o - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:openai/gpt-4o - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-extractor-haiku.yaml b/agents/real-estate-extractor-haiku.yaml deleted file mode 100644 index 59e517e..0000000 --- a/agents/real-estate-extractor-haiku.yaml +++ /dev/null @@ -1,318 +0,0 @@ -name: real-estate-extractor-haiku -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -middleware: - - sub_agent - -subagents: - - name: financial-extractor - model: openai:anthropic/claude-haiku-4.5:nitro - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="hybrid", top_k=5. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:anthropic/claude-haiku-4.5:nitro - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="hybrid", top_k=5. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:anthropic/claude-haiku-4.5:nitro - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=5. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-extractor-minimax-27.yaml b/agents/real-estate-extractor-minimax-27.yaml deleted file mode 100644 index f2c2fbd..0000000 --- a/agents/real-estate-extractor-minimax-27.yaml +++ /dev/null @@ -1,318 +0,0 @@ -name: real-estate-extractor-minimax-27 -model: openai:minimax/minimax-m2.7:nitro -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -middleware: - - sub_agent - -subagents: - - name: financial-extractor - model: openai:minimax/minimax-m2.7:nitro - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:minimax/minimax-m2.7:nitro - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:minimax/minimax-m2.7:nitro - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-extractor-opus.yaml b/agents/real-estate-extractor-opus.yaml deleted file mode 100644 index e1afd3e..0000000 --- a/agents/real-estate-extractor-opus.yaml +++ /dev/null @@ -1,318 +0,0 @@ -name: real-estate-extractor-opus -model: openai:anthropic/claude-opus-4.6:nitro -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -middleware: - - sub_agent - -subagents: - - name: financial-extractor - model: openai:anthropic/claude-opus-4.6:nitro - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="hybrid", top_k=5. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:anthropic/claude-opus-4.6:nitro - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="hybrid", top_k=5. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:anthropic/claude-opus-4.6:nitro - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=5. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-extractor.yaml b/agents/real-estate-extractor.yaml deleted file mode 100644 index 33b40d0..0000000 --- a/agents/real-estate-extractor.yaml +++ /dev/null @@ -1,408 +0,0 @@ -name: real-estate-extractor -model: openai:google/gemini-2.5-flash:nitro -system_prompt: | - Tu es un orchestrateur d'extraction de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en déléguant le travail à tes 3 sous-agents spécialisés : - - financial-extractor : données financières (prix, prêt, garantie, durée) - - property-extractor : données du bien immobilier (surface, adresse, type, lots, DPE) - - company-extractor : données de la société porteuse (ancienneté, résultats, endettement) - - ## Règles STRICTES - - - Délègue TOUJOURS aux 3 sous-agents. Ne fais JAMAIS l'extraction toi-même. - - Transmets le working_dir du projet à chaque sous-agent. - - Combine les résultats des 3 sous-agents dans ta réponse finale. - - Si un sous-agent échoue, rapporte l'erreur mais continue avec les autres. - - Liste les champs manquants (null) à la fin sous "missing_fields". - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - Utilise les données exactes retournées par chaque sous-agent. - Les champs non trouvés doivent être null. - Liste tous les champs null dans missing_fields au format "groupe.champ". - - Structure attendue : - {"financial_project": {...}, "property": {...}, "company": {...}, "missing_fields": [...]} - -middleware: - - sub_agent - -response_format: - title: RealEstateExtraction - type: object - properties: - financial_project: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - property: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - company: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" - missing_fields: - type: array - description: "Liste des champs non trouvés (null) au format groupe.champ" - items: - type: string - required: - - financial_project - - property - - company - - missing_fields - -subagents: - - name: financial-extractor - model: openai:google/gemini-2.5-flash:nitro - description: "Extrait les données financières du projet immobilier (prix, prêt, garantie, durée) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières spécialisé dans les documents immobiliers. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - - Paramètres recommandés : mode="naive", top_k=10. - - ## Champs à extraire - - acquisition_price : Prix d'acquisition en EUR - - works_cost : Coût des travaux en EUR - - planned_resale_price : Prix de revente prévu en EUR - - personal_contribution : Apport personnel en EUR - - loan_amount : Montant du prêt en EUR - - guarantee_value : Valeur de la garantie en EUR - - guarantee_type : Type de garantie (hypothèque, caution, nantissement) - - duration_months : Durée du projet en mois - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - - - name: property-extractor - model: openai:google/gemini-2.5-flash:nitro - description: "Extrait les données du bien immobilier (surface, adresse, type, lots, DPE) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données immobilières spécialisé dans les caractéristiques des biens. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les surfaces : "120 m²" → 120. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 2. "loyer mensuel charges locatives lots unités nombre" - 3. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE diagnostic performance énergétique état rénovation" - - Paramètres recommandés : mode="naive", top_k=10. - - ## Champs à extraire - - living_area : Surface habitable en m² - - address : Adresse complète du bien - - property_type : Type (appartement, maison, immeuble, terrain, local commercial, parking) - - monthly_rent_excluding_tax : Loyer mensuel hors taxes en EUR - - annual_charges : Charges annuelles en EUR - - presold_units : Nombre de lots pré-commercialisés - - total_units : Nombre total de lots/unités - - lots : Description textuelle des lots ("3 T2 + 2 T3") - - lots_detail : Détail structuré des lots [{lot_type, living_area}] - - building_year : Année de construction - - number_of_rooms : Nombre de pièces principales - - condition : État (neuf, rénové récemment, bon état, à rénover) - - land_area : Surface du terrain en m² - - floor_number : Étage (0 = rez-de-chaussée) - - has_lift : Présence d'un ascenseur (true/false) - - number_of_bathrooms : Nombre de salles de bain/eau - - garden_area : Surface du jardin en m² - - balcony_area : Surface balcon/terrasse en m² - - energy_label : DPE (A, B, C, D, E, F, G) - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - - - name: company-extractor - model: openai:google/gemini-2.5-flash:nitro - description: "Extrait les données de la société porteuse (ancienneté, résultats nets, endettement, capitaux propres) depuis la base de connaissances RAG." - instructions: | - Tu es un extracteur de données financières d'entreprise spécialisé dans les bilans comptables. - - ## Règles STRICTES - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Ne déduis rien qui ne soit pas explicitement écrit. - - Les résultats nets doivent être ordonnés : y1 = exercice le plus récent (N), y2 = N-1, y3 = N-2. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni pour rechercher les données. - Fais plusieurs requêtes thématiques : - 1. "bilan comptable résultat net exercice chiffre affaires" - 2. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="naive", top_k=10. - - ## Champs à extraire - - company_years_of_existence : Ancienneté de la société en années - - net_result_y1 : Résultat net exercice N (le plus récent) en EUR - - net_result_y2 : Résultat net exercice N-1 en EUR - - net_result_y3 : Résultat net exercice N-2 en EUR - - total_debt : Endettement total / dettes financières en EUR - - equity : Capitaux propres en EUR - mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" - response_format: - type: object - properties: - company_years_of_existence: - type: integer - description: "Ancienneté de la société en années" - net_result_y1: - type: number - description: "Résultat net exercice N (le plus récent) en EUR" - net_result_y2: - type: number - description: "Résultat net exercice N-1 en EUR" - net_result_y3: - type: number - description: "Résultat net exercice N-2 en EUR" - total_debt: - type: number - description: "Endettement total / dettes financières en EUR" - equity: - type: number - description: "Capitaux propres en EUR" diff --git a/agents/real-estate-single-gemini.yaml b/agents/real-estate-single-gemini.yaml deleted file mode 100644 index 65cf6d0..0000000 --- a/agents/real-estate-single-gemini.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: real-estate-single-gemini -model: openai:google/gemini-2.5-flash:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-gpt4o-mini.yaml b/agents/real-estate-single-gpt4o-mini.yaml deleted file mode 100644 index 59d87c9..0000000 --- a/agents/real-estate-single-gpt4o-mini.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: real-estate-single-gpt4o-mini -model: openai:openai/gpt-4o-mini -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-gpt4o.yaml b/agents/real-estate-single-gpt4o.yaml deleted file mode 100644 index 1357b94..0000000 --- a/agents/real-estate-single-gpt4o.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: real-estate-single-gpt4o -model: openai:openai/gpt-4o -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-haiku-v2.yaml b/agents/real-estate-single-haiku-v2.yaml deleted file mode 100644 index 71fd15d..0000000 --- a/agents/real-estate-single-haiku-v2.yaml +++ /dev/null @@ -1,68 +0,0 @@ -name: real-estate-single-haikuv2 -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - -response_format: - title: RealEstateExtraction - type: object - properties: - financial_project: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - required: - - financial_project -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-haiku-v3.yaml b/agents/real-estate-single-haiku-v3.yaml deleted file mode 100644 index 124dcdd..0000000 --- a/agents/real-estate-single-haiku-v3.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: real-estate-single-haikuv3 -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-haiku-v4-finacial.yaml b/agents/real-estate-single-haiku-v4-finacial.yaml deleted file mode 100644 index e242cfb..0000000 --- a/agents/real-estate-single-haiku-v4-finacial.yaml +++ /dev/null @@ -1,68 +0,0 @@ -name: real-estate-single-haikuv4-financial -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - -response_format: - title: RealEstateExtraction - type: object - properties: - financial_project: - type: object - properties: - acquisition_price: - type: number - description: "Prix d'acquisition en EUR" - works_cost: - type: number - description: "Coût des travaux en EUR" - planned_resale_price: - type: number - description: "Prix de revente prévu en EUR" - personal_contribution: - type: number - description: "Apport personnel en EUR" - loan_amount: - type: number - description: "Montant du prêt en EUR" - guarantee_value: - type: number - description: "Valeur de la garantie en EUR" - guarantee_type: - type: string - description: "Type de garantie : hypothèque, caution, nantissement" - duration_months: - type: integer - description: "Durée du projet en mois" - required: - - financial_project -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-haiku-v4-property.yaml b/agents/real-estate-single-haiku-v4-property.yaml deleted file mode 100644 index fe9d4c7..0000000 --- a/agents/real-estate-single-haiku-v4-property.yaml +++ /dev/null @@ -1,110 +0,0 @@ -name: real-estate-single-haikuv4-property -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - -response_format: - title: RealEstateExtraction - type: object - properties: - property: - type: object - properties: - living_area: - type: number - description: "Surface habitable en m²" - address: - type: string - description: "Adresse complète du bien" - property_type: - type: string - description: "Type : appartement, maison, immeuble, terrain, local commercial, parking" - monthly_rent_excluding_tax: - type: number - description: "Loyer mensuel hors taxes en EUR" - annual_charges: - type: number - description: "Charges annuelles en EUR" - presold_units: - type: integer - description: "Nombre de lots pré-commercialisés" - total_units: - type: integer - description: "Nombre total de lots/unités" - lots: - type: string - description: "Description textuelle des lots (ex: 3 T2 + 2 T3)" - lots_detail: - type: array - description: "Détail structuré des lots" - items: - type: object - properties: - lot_type: - type: string - description: "Type de lot : studio, T1, T2, T3, T4, T5, local commercial" - living_area: - type: number - description: "Surface habitable du lot en m²" - building_year: - type: integer - description: "Année de construction" - number_of_rooms: - type: integer - description: "Nombre de pièces principales" - condition: - type: string - description: "État : neuf, rénové récemment, bon état, à rénover" - land_area: - type: number - description: "Surface du terrain en m²" - floor_number: - type: integer - description: "Étage (0 = rez-de-chaussée)" - has_lift: - type: boolean - description: "Présence d'un ascenseur" - number_of_bathrooms: - type: integer - description: "Nombre de salles de bain/eau" - garden_area: - type: number - description: "Surface du jardin en m²" - balcony_area: - type: number - description: "Surface balcon/terrasse en m²" - energy_label: - type: string - description: "DPE : A, B, C, D, E, F, G" - required: - - property -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-haiku.yaml b/agents/real-estate-single-haiku.yaml deleted file mode 100644 index 8822b25..0000000 --- a/agents/real-estate-single-haiku.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: real-estate-single-haiku -model: openai:anthropic/claude-haiku-4.5:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-minimax-27.yaml b/agents/real-estate-single-minimax-27.yaml deleted file mode 100644 index c098891..0000000 --- a/agents/real-estate-single-minimax-27.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: real-estate-single-minimax-27 -model: openai:minimax/minimax-m2.7:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/real-estate-single-opus.yaml b/agents/real-estate-single-opus.yaml deleted file mode 100644 index fd87ccd..0000000 --- a/agents/real-estate-single-opus.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: real-estate-single-opus -model: openai:anthropic/claude-opus-4.6:nitro -system_prompt: | - Tu es un extracteur de données spécialisé dans les documents immobiliers (crowdfunding). - - ## Objectif - - L'utilisateur te donne un identifiant de projet (working_dir). Tu dois extraire toutes les données - factuelles des documents de ce projet en utilisant l'outil query_knowledge_base. - - ## Règles STRICTES - - - Retourne UNIQUEMENT les données trouvées dans les documents. - - Si une donnée n'est pas trouvée, retourne null. - - Ne fais AUCUNE interprétation, calcul, arrondi ou estimation. - - Normalise les montants en nombres : "450K €" → 450000, "1,2 M€" → 1200000. - - Normalise les surfaces : "120 m²" → 120. - - Pour les durées : "18 mois" → 18, "2 ans" → 24. - - Ne déduis rien qui ne soit pas explicitement écrit. - - ## Méthode - - Utilise l'outil query_knowledge_base avec le working_dir fourni. - Fais plusieurs requêtes thématiques : - 1. "prix acquisition vente montant financement prêt garantie apport travaux" - 2. "durée projet calendrier planning mois" - 3. "surface habitable adresse bien immobilier localisation type appartement maison immeuble" - 4. "loyer mensuel charges locatives lots unités nombre" - 5. "année construction étage ascenseur pièces salles bain terrain jardin balcon terrasse DPE" - 6. "bilan comptable résultat net exercice chiffre affaires" - 7. "capitaux propres dette endettement ancienneté société création" - - Paramètres recommandés : mode="hybrid", top_k=10. - - ## Format de réponse - - Tu DOIS répondre UNIQUEMENT avec un objet JSON brut, sans markdown, sans commentaire, sans bloc de code. - Pas de ```, pas de texte avant ou après. Juste le JSON. - - Tu DOIS utiliser EXACTEMENT ces noms de clés (en anglais), pas de traduction : - - { - "financial_project": { - "acquisition_price": number|null, - "works_cost": number|null, - "planned_resale_price": number|null, - "personal_contribution": number|null, - "loan_amount": number|null, - "guarantee_value": number|null, - "guarantee_type": string|null, - "duration_months": integer|null - }, - "property": { - "living_area": number|null, - "address": string|null, - "property_type": string|null, - "monthly_rent_excluding_tax": number|null, - "annual_charges": number|null, - "presold_units": integer|null, - "total_units": integer|null, - "lots": string|null, - "lots_detail": [{"lot_type": string, "living_area": number}]|null, - "building_year": integer|null, - "number_of_rooms": integer|null, - "condition": string|null, - "land_area": number|null, - "floor_number": integer|null, - "has_lift": boolean|null, - "number_of_bathrooms": integer|null, - "garden_area": number|null, - "balcony_area": number|null, - "energy_label": string|null - }, - "company": { - "company_years_of_existence": integer|null, - "net_result_y1": number|null, - "net_result_y2": number|null, - "net_result_y3": number|null, - "total_debt": number|null, - "equity": number|null - }, - "missing_fields": ["groupe.champ", ...] - } - -mcp_servers: - - name: raganything - transport: http - url: http://raganything-api:8000/mcp/ - auth_token: "" diff --git a/agents/research-assistant.yaml b/agents/research-assistant.yaml deleted file mode 100644 index bdc9813..0000000 --- a/agents/research-assistant.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: research-assistant -model: "openai:anthropic/claude-haiku-4.5:nitro" -system_prompt: | - You are a research assistant specialized in technical documentation. - Always cite your sources and provide structured summaries. -tools: - - "src.infrastructure.deepagent.example_tools:get_user_name" - - "src.infrastructure.deepagent.example_tools:get_secret" -middleware: - - filesystem -backend: - type: filesystem - root_dir: "./workspace" -hitl: - rules: - get_secret: true - get_user_name: true -debug: false diff --git a/pyproject.toml b/pyproject.toml index 5affc60..57db886 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,13 +31,11 @@ dependencies = [ "sqlalchemy[asyncio]>=2.0.0", "alembic>=1.13.0", "cachetools>=7.0.5", -] - -[project.optional-dependencies] -langfuse = ["langfuse>=2.0.0"] -phoenix = ["arize-phoenix-otel>=0.1.0", "openinference-instrumentation-langchain>=0.1.0", "arize-phoenix-client>=2.3.0"] -tracing = ["langfuse>=2.0.0", "arize-phoenix-otel>=0.1.0", "openinference-instrumentation-langchain>=0.1.0", "arize-phoenix-client>=2.3.0"] + "arize-phoenix-otel==0.15.0", + "openinference-instrumentation-langchain==0.1.62", + "arize-phoenix-client==2.3.0", +] [dependency-groups] dev = [ "httpx>=0.28.1", diff --git a/src/alembic/versions/003_drop_is_builtin_column.py b/src/alembic/versions/003_drop_is_builtin_column.py new file mode 100644 index 0000000..2b22875 --- /dev/null +++ b/src/alembic/versions/003_drop_is_builtin_column.py @@ -0,0 +1,23 @@ +"""Drop is_builtin column from agent_configs. + +Revision ID: 003 +Revises: 002 +Create Date: 2026-04-12 +""" + +from collections.abc import Sequence + +from alembic import op + +revision: str = "003" +down_revision: str | None = "002" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.drop_column("agent_configs", "is_builtin") + + +def downgrade() -> None: + op.add_column("agent_configs", "is_builtin", nullable=False, server_default="false") diff --git a/src/application/routes/chat.py b/src/application/routes/chat.py index b9885e1..e5b8fd3 100644 --- a/src/application/routes/chat.py +++ b/src/application/routes/chat.py @@ -48,9 +48,10 @@ async def event_generator(): async for chunk in use_case.execute(thread_id, body.message): chunk_count += 1 yield {"data": chunk} + yield {"event": "done", "data": ""} logger.info("[thread=%s] Stream complete, %d chunks", thread_id, chunk_count) except Exception: logger.exception("[thread=%s] Stream error after %d chunks", thread_id, chunk_count) - raise + yield {"event": "error", "data": "stream_error"} - return EventSourceResponse(event_generator()) + return EventSourceResponse(event_generator(), sep="\r\n", ping=15) diff --git a/src/application/use_cases/create_agent_config.py b/src/application/use_cases/create_agent_config.py index 50f5a8a..f4b3399 100644 --- a/src/application/use_cases/create_agent_config.py +++ b/src/application/use_cases/create_agent_config.py @@ -53,7 +53,6 @@ async def execute(self, name: str, yaml_content: str) -> AgentConfig: name=name, model=config.model, minio_path=f"{name}.yaml", - is_builtin=False, created_at=now, updated_at=now, ) diff --git a/src/application/use_cases/delete_agent_config.py b/src/application/use_cases/delete_agent_config.py index b1b1f93..f16ac8a 100644 --- a/src/application/use_cases/delete_agent_config.py +++ b/src/application/use_cases/delete_agent_config.py @@ -1,6 +1,5 @@ import logging -from src.domain.exceptions import ConfigError from src.domain.ports.agent_config_repository import AgentConfigRepository from src.domain.ports.agent_config_store import AgentConfigStore from src.domain.ports.agent_registry import AgentRegistry @@ -29,12 +28,8 @@ async def execute(self, name: str) -> None: Raises: AgentNotFoundError: If no agent with this name exists. - ConfigError: If the agent is built-in. """ - metadata = await self._config_repository.get(name) - - if metadata.is_builtin: - raise ConfigError(f"Cannot delete built-in agent: {name}") + await self._config_repository.get(name) await self._config_store.delete(name) await self._config_repository.delete(name) diff --git a/src/application/use_cases/seed_agents.py b/src/application/use_cases/seed_agents.py deleted file mode 100644 index 007a37a..0000000 --- a/src/application/use_cases/seed_agents.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -from datetime import UTC, datetime -from pathlib import Path - -import yaml - -from src.domain.entities.agent_config_metadata import AgentConfigMetadata -from src.domain.ports.agent_config_loader import AgentConfigLoader -from src.domain.ports.agent_config_repository import AgentConfigRepository -from src.domain.ports.agent_config_store import AgentConfigStore - -logger = logging.getLogger("composable-agents") - - -class SeedAgentsUseCase: - """Seed built-in agent configurations from a local directory into persistent storage.""" - - def __init__( - self, - config_loader: AgentConfigLoader, - config_store: AgentConfigStore, - config_repository: AgentConfigRepository, - ) -> None: - self._config_loader = config_loader - self._config_store = config_store - self._config_repository = config_repository - - async def execute(self, agents_dir: Path) -> None: - """For each YAML file in agents_dir, upload to MinIO and save metadata if not already present. - - If a YAML references system_prompt_file, the prompt is read from disk and inlined - before uploading so that the stored YAML is self-contained. - - Args: - agents_dir: Path to the directory containing seed agent YAML files. - """ - if not agents_dir.exists(): - logger.warning("Agents directory does not exist: %s", agents_dir) - return - - for yaml_file in sorted(agents_dir.glob("*.yaml")): - agent_name = yaml_file.stem - - if await self._config_repository.exists(agent_name): - logger.debug("Agent '%s' already seeded, skipping", agent_name) - continue - - config = self._config_loader.load(yaml_file) - - raw = yaml.safe_load(yaml_file.read_text(encoding="utf-8")) - if raw.get("system_prompt_file"): - raw.pop("system_prompt_file") - raw["system_prompt"] = config.system_prompt - yaml_content = yaml.dump(raw, default_flow_style=False, allow_unicode=True) - - await self._config_store.put(agent_name, yaml_content) - - now = datetime.now(UTC) - metadata = AgentConfigMetadata( - name=agent_name, - model=config.model, - minio_path=f"{agent_name}.yaml", - is_builtin=True, - created_at=now, - updated_at=now, - ) - await self._config_repository.save(metadata) - - logger.info("Seeded built-in agent '%s'", agent_name) diff --git a/src/application/use_cases/stream_message.py b/src/application/use_cases/stream_message.py index 792628e..d87591c 100644 --- a/src/application/use_cases/stream_message.py +++ b/src/application/use_cases/stream_message.py @@ -37,7 +37,12 @@ async def execute(self, thread_id: str, message: str) -> AsyncGenerator[str, Non raise elapsed = time.monotonic() - start ai_msg = Message(role=MessageRole.AI, content="".join(full_response)) - await self._threads.add_message(thread_id, ai_msg) + try: + await self._threads.add_message(thread_id, ai_msg) + except Exception: + logger.exception( + "[thread=%s][agent=%s] Failed to persist AI message after stream", thread_id, thread.agent_name + ) logger.info( "[thread=%s][agent=%s] Stream complete, %d chunks, %d chars, elapsed=%.2fs", thread_id, diff --git a/src/application/use_cases/update_agent_config.py b/src/application/use_cases/update_agent_config.py index 2ce8b93..3dd44fa 100644 --- a/src/application/use_cases/update_agent_config.py +++ b/src/application/use_cases/update_agent_config.py @@ -38,13 +38,10 @@ async def execute(self, name: str, yaml_content: str) -> AgentConfig: Raises: AgentNotFoundError: If no agent with this name exists. - ConfigError: If the agent is built-in or the name in YAML does not match. + ConfigError: If the name in YAML does not match. """ metadata = await self._config_repository.get(name) - if metadata.is_builtin: - raise ConfigError(f"Cannot update built-in agent: {name}") - config = self._config_loader.load_from_string(yaml_content) if config.name != name: diff --git a/src/config.py b/src/config.py index 7275a83..0529c90 100644 --- a/src/config.py +++ b/src/config.py @@ -18,7 +18,6 @@ class TracingSettings(BaseSettings): class Settings(BaseSettings): - agents_dir: str = "./agents" openai_api_key: str | None = None host: str = "0.0.0.0" port: int = 8000 diff --git a/src/dependencies.py b/src/dependencies.py index ed816ca..1796253 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -1,5 +1,4 @@ import logging -from pathlib import Path from miniopy_async import Minio from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine @@ -10,7 +9,6 @@ from src.application.use_cases.get_agent_config import GetAgentConfigUseCase from src.application.use_cases.list_agent_configs import ListAgentConfigsUseCase from src.application.use_cases.load_agent_config import LoadAgentConfigUseCase -from src.application.use_cases.seed_agents import SeedAgentsUseCase from src.application.use_cases.send_message import SendMessageUseCase from src.application.use_cases.stream_message import StreamMessageUseCase from src.application.use_cases.thread_management import ( @@ -22,9 +20,9 @@ from src.application.use_cases.update_agent_config import UpdateAgentConfigUseCase from src.config import Settings from src.domain.exceptions import StorageError +from src.domain.ports.agent_registry import AgentRegistry from src.domain.ports.prompt_manager import PromptManager from src.domain.ports.thread_repository import ThreadRepository -from src.infrastructure.deepagent.registry import DeepAgentRegistry from src.infrastructure.mcp.adapter import LangchainMcpToolLoader from src.infrastructure.minio_store.adapter import MinioAgentConfigStore from src.infrastructure.persistent_registry.adapter import PersistentAgentRegistry @@ -93,23 +91,12 @@ def get_prompt_manager() -> PromptManager: mcp_tool_loader = LangchainMcpToolLoader() tracing_provider = _create_tracing_provider(settings) -# Filesystem-based registry (kept for backward compatibility) -agent_registry = DeepAgentRegistry( - agents_dir=Path(settings.agents_dir), - config_loader=agent_config_loader, - mcp_tool_loader=mcp_tool_loader, - tracing_provider=tracing_provider, - prompt_manager=get_prompt_manager(), -) - -agents_dir = settings.agents_dir - # ============= PERSISTENCE (initialized at startup) ============= _async_engine: AsyncEngine | None = None _minio_store: MinioAgentConfigStore | None = None _pg_repository: PostgresAgentConfigRepository | None = None -_persistent_registry: PersistentAgentRegistry | None = None +agent_registry: AgentRegistry | None = None thread_repository: ThreadRepository | None = None @@ -118,7 +105,7 @@ async def init_persistence() -> None: Must be called during application startup. """ - global _async_engine, _minio_store, _pg_repository, _persistent_registry, agent_registry, thread_repository + global _async_engine, _minio_store, _pg_repository, agent_registry, thread_repository logger.info("Initializing persistence layer") @@ -145,7 +132,7 @@ async def init_persistence() -> None: await _minio_store.ensure_bucket() logger.info("MinIO store initialized (bucket=%s)", settings.minio_bucket) - _persistent_registry = PersistentAgentRegistry( + agent_registry = PersistentAgentRegistry( config_loader=agent_config_loader, config_store=_minio_store, config_repository=_pg_repository, @@ -153,9 +140,8 @@ async def init_persistence() -> None: tracing_provider=tracing_provider, prompt_manager=get_prompt_manager(), ) - agent_registry = _persistent_registry - logger.info("Persistence layer initialized, agent_registry switched to PersistentAgentRegistry") + logger.info("Persistence layer initialized, agent_registry set to PersistentAgentRegistry") async def close_persistence() -> None: @@ -163,8 +149,8 @@ async def close_persistence() -> None: Must be called during application shutdown. """ - if _persistent_registry: - await _persistent_registry.close() + if agent_registry and isinstance(agent_registry, PersistentAgentRegistry): + await agent_registry.close() logger.info("Persistent registry closed") if _async_engine: @@ -172,22 +158,8 @@ async def close_persistence() -> None: logger.info("SQLAlchemy engine disposed") -async def seed_builtin_agents() -> None: - """Seed built-in agents from the configured agents directory.""" - if _minio_store is None or _pg_repository is None: - logger.warning("Persistence not initialized, skipping seed") - return - - seed_use_case = SeedAgentsUseCase( - config_loader=agent_config_loader, - config_store=_minio_store, - config_repository=_pg_repository, - ) - await seed_use_case.execute(agents_dir=Path(settings.agents_dir)) - logger.info("Built-in agents seeded from %s", settings.agents_dir) - +logger.info("Dependencies initialized") -logger.info("Dependencies initialized (agents_dir=%s)", settings.agents_dir) # ============= USE CASE PROVIDERS ============= @@ -199,19 +171,26 @@ def _require_thread_repository() -> ThreadRepository: return thread_repository +def _require_agent_registry() -> AgentRegistry: + """Return agent registry or raise StorageError if not initialized.""" + if agent_registry is None: + raise StorageError("Agent registry not initialized. Check MinIO/PostgreSQL connectivity.") + return agent_registry + + def get_send_message_use_case() -> SendMessageUseCase: """Provide a SendMessageUseCase instance.""" - return SendMessageUseCase(agent_registry, _require_thread_repository()) + return SendMessageUseCase(_require_agent_registry(), _require_thread_repository()) def get_stream_message_use_case() -> StreamMessageUseCase: """Provide a StreamMessageUseCase instance.""" - return StreamMessageUseCase(agent_registry, _require_thread_repository()) + return StreamMessageUseCase(_require_agent_registry(), _require_thread_repository()) def get_create_thread_use_case() -> CreateThreadUseCase: """Provide a CreateThreadUseCase instance.""" - return CreateThreadUseCase(_require_thread_repository(), agent_registry) + return CreateThreadUseCase(_require_thread_repository(), _require_agent_registry()) def get_get_thread_use_case() -> GetThreadUseCase: @@ -234,11 +213,6 @@ def get_load_agent_config_use_case() -> LoadAgentConfigUseCase: return LoadAgentConfigUseCase(agent_config_loader) -def get_agents_dir() -> str: - """Provide the configured agents directory path.""" - return agents_dir - - def _require_persistence() -> tuple[MinioAgentConfigStore, PostgresAgentConfigRepository]: """Return persistence adapters or raise StorageError if not initialized.""" if _minio_store is None or _pg_repository is None: @@ -263,7 +237,7 @@ def get_update_agent_config_use_case() -> UpdateAgentConfigUseCase: config_loader=agent_config_loader, config_store=store, config_repository=repo, - agent_registry=agent_registry, + agent_registry=_require_agent_registry(), ) @@ -273,7 +247,7 @@ def get_delete_agent_config_use_case() -> DeleteAgentConfigUseCase: return DeleteAgentConfigUseCase( config_store=store, config_repository=repo, - agent_registry=agent_registry, + agent_registry=_require_agent_registry(), ) diff --git a/src/domain/entities/agent_config_metadata.py b/src/domain/entities/agent_config_metadata.py index 4efc734..d6d79a0 100644 --- a/src/domain/entities/agent_config_metadata.py +++ b/src/domain/entities/agent_config_metadata.py @@ -9,6 +9,5 @@ class AgentConfigMetadata(BaseModel): name: str model: str minio_path: str - is_builtin: bool = False created_at: datetime updated_at: datetime diff --git a/src/infrastructure/database/models/agent_config.py b/src/infrastructure/database/models/agent_config.py index 16b4339..2f3bb45 100644 --- a/src/infrastructure/database/models/agent_config.py +++ b/src/infrastructure/database/models/agent_config.py @@ -1,6 +1,6 @@ from datetime import datetime -from sqlalchemy import Boolean, DateTime, String +from sqlalchemy import DateTime, String from sqlalchemy.orm import Mapped, mapped_column from src.infrastructure.database.models.base import Base @@ -12,6 +12,5 @@ class AgentConfigModel(Base): name: Mapped[str] = mapped_column(String(100), primary_key=True) model: Mapped[str] = mapped_column(String(200), nullable=False) minio_path: Mapped[str] = mapped_column(String(500), nullable=False) - is_builtin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) diff --git a/src/infrastructure/deepagent/registry.py b/src/infrastructure/deepagent/registry.py deleted file mode 100644 index f3441d6..0000000 --- a/src/infrastructure/deepagent/registry.py +++ /dev/null @@ -1,71 +0,0 @@ -import asyncio -import logging -from pathlib import Path - -from src.domain.exceptions import AgentNotFoundError -from src.domain.ports.agent_config_loader import AgentConfigLoader -from src.domain.ports.agent_registry import AgentRegistry -from src.domain.ports.agent_runner import AgentRunner -from src.domain.ports.mcp_tool_loader import McpToolLoader -from src.domain.ports.prompt_manager import PromptManager -from src.domain.ports.tracing_provider import TracingProvider -from src.infrastructure.deepagent.adapter import DeepAgentRunner -from src.infrastructure.deepagent.factory import create_agent_from_config - -logger = logging.getLogger("composable-agents") - - -class DeepAgentRegistry(AgentRegistry): - """Registre qui cree et cache les agents a la demande depuis un dossier YAML.""" - - def __init__( - self, - agents_dir: Path, - config_loader: AgentConfigLoader, - mcp_tool_loader: McpToolLoader, - tracing_provider: TracingProvider | None = None, - prompt_manager: PromptManager | None = None, - ) -> None: - self._agents_dir = agents_dir - self._config_loader = config_loader - self._mcp_tool_loader = mcp_tool_loader - self._tracing_provider = tracing_provider - self._prompt_manager = prompt_manager - self._runners: dict[str, AgentRunner] = {} - self._lock = asyncio.Lock() - - async def get_runner(self, agent_name: str) -> AgentRunner: - if agent_name in self._runners: - logger.debug("Agent '%s' loaded from cache", agent_name) - return self._runners[agent_name] - - async with self._lock: - if agent_name in self._runners: - return self._runners[agent_name] - - config_path = self._agents_dir / f"{agent_name}.yaml" - if not config_path.exists(): - logger.error("Agent not found: %s", agent_name) - raise AgentNotFoundError(f"Agent not found: {agent_name}") - - logger.info("Building agent '%s' from %s", agent_name, config_path) - config = self._config_loader.load(config_path) - graph = await create_agent_from_config(config, self._mcp_tool_loader, self._prompt_manager) - runner = DeepAgentRunner(graph, tracing_provider=self._tracing_provider) - self._runners[agent_name] = runner - logger.info("Agent '%s' ready and cached", agent_name) - return runner - - async def list_agents(self) -> list[str]: - if not self._agents_dir.exists(): - return [] - return sorted(f.stem for f in self._agents_dir.glob("*.yaml")) - - async def invalidate(self, agent_name: str) -> None: - async with self._lock: - self._runners.pop(agent_name, None) - logger.info("Invalidated cached agent '%s'", agent_name) - - async def close(self) -> None: - logger.info("Closing registry, clearing %d cached agents", len(self._runners)) - self._runners.clear() diff --git a/src/infrastructure/postgres_repository/adapter.py b/src/infrastructure/postgres_repository/adapter.py index 53409c7..98cae95 100644 --- a/src/infrastructure/postgres_repository/adapter.py +++ b/src/infrastructure/postgres_repository/adapter.py @@ -17,7 +17,6 @@ def _model_to_metadata(model: AgentConfigModel) -> AgentConfigMetadata: name=model.name, model=model.model, minio_path=model.minio_path, - is_builtin=model.is_builtin, created_at=model.created_at, updated_at=model.updated_at, ) @@ -50,7 +49,6 @@ async def save(self, metadata: AgentConfigMetadata) -> None: name=metadata.name, model=metadata.model, minio_path=metadata.minio_path, - is_builtin=metadata.is_builtin, created_at=metadata.created_at, updated_at=metadata.updated_at, ) diff --git a/src/main.py b/src/main.py index 3cf400d..01d2d2c 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,39 @@ import asyncio import logging +import sys from contextlib import asynccontextmanager from pathlib import Path +log_level_name = "INFO" +log_level = logging.INFO + +from src.config import Settings + +_settings = Settings() +log_level_name = _settings.log_level.upper() +log_level = getattr(logging, log_level_name, logging.INFO) + +_handler = logging.StreamHandler(sys.stdout) +_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) + +_root = logging.getLogger() +_root.setLevel(log_level) +_root.handlers.clear() +_root.addHandler(_handler) + +for _name in ( + "langchain", + "langchain_core", + "langchain_community", + "langgraph", + "openai", + "httpx", + "httpcore", + "alembic", + "sqlalchemy", +): + logging.getLogger(_name).setLevel(log_level) + from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse @@ -13,12 +44,10 @@ from src.application.routes.prompt import router as prompt_router from src.application.routes.threads import router as threads_router from src.application.routes.websocket import router as websocket_router -from src.config import Settings from src.dependencies import ( close_persistence, init_persistence, mcp_tool_loader, - seed_builtin_agents, tracing_provider, ) from src.domain.exceptions import ( @@ -33,28 +62,9 @@ ThreadNotFoundError, ) -log_level = getattr(logging, Settings().log_level.upper(), logging.INFO) - -logging.basicConfig( - level=log_level, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) - -for _name in ( - "langchain", - "langchain_core", - "langchain_community", - "langgraph", - "openai", - "httpx", - "httpcore", - "alembic", - "sqlalchemy", -): - logging.getLogger(_name).setLevel(log_level) - logger = logging.getLogger("composable-agents") +settings = Settings() def _run_alembic_upgrade() -> None: """Run Alembic migrations to head synchronously. @@ -80,8 +90,7 @@ async def lifespan(_app: FastAPI): await asyncio.to_thread(_run_alembic_upgrade) logger.info("Database migrations completed") await init_persistence() - await seed_builtin_agents() - logger.info("Persistence initialized and agents seeded") + logger.info("Persistence initialized") except Exception: logger.exception("Failed to initialize persistence, falling back to filesystem registry") logger.info("Application startup complete") @@ -185,4 +194,4 @@ async def domain_error_handler(_request: Request, exc: DomainError) -> JSONRespo if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(app, host=settings.host, port=settings.port, log_level=settings.log_level.lower(), log_config=None) \ No newline at end of file diff --git a/tests/unit/test_agent_crud.py b/tests/unit/test_agent_crud.py index 3f0b332..263ff03 100644 --- a/tests/unit/test_agent_crud.py +++ b/tests/unit/test_agent_crud.py @@ -120,19 +120,6 @@ def existing_metadata(self): name="test-agent", model="claude-sonnet-4-5-20250929", minio_path="agent-configs/test-agent.yaml", - is_builtin=False, - created_at=now, - updated_at=now, - ) - - @pytest.fixture - def builtin_metadata(self): - now = datetime.now(UTC) - return AgentConfigMetadata( - name="builtin-agent", - model="claude-sonnet-4-5-20250929", - minio_path="agent-configs/builtin-agent.yaml", - is_builtin=True, created_at=now, updated_at=now, ) @@ -165,15 +152,6 @@ async def test_update_agent_name_mismatch(self, use_case, mock_repository, exist with pytest.raises(ConfigError): await use_case.execute(name="test-agent", yaml_content=mismatched_yaml) - async def test_update_agent_builtin_protected(self, use_case, mock_repository, builtin_metadata): - """Should raise ConfigError when trying to update a built-in agent.""" - mock_repository.get.return_value = builtin_metadata - - builtin_yaml = 'name: builtin-agent\nmodel: claude-sonnet-4-5-20250929\nsystem_prompt: "Modified."\n' - - with pytest.raises(ConfigError, match="built-in"): - await use_case.execute(name="builtin-agent", yaml_content=builtin_yaml) - class TestDeleteAgentConfigUseCase: """Tests for DeleteAgentConfigUseCase.""" @@ -205,19 +183,6 @@ def existing_metadata(self): name="test-agent", model="claude-sonnet-4-5-20250929", minio_path="agent-configs/test-agent.yaml", - is_builtin=False, - created_at=now, - updated_at=now, - ) - - @pytest.fixture - def builtin_metadata(self): - now = datetime.now(UTC) - return AgentConfigMetadata( - name="builtin-agent", - model="claude-sonnet-4-5-20250929", - minio_path="agent-configs/builtin-agent.yaml", - is_builtin=True, created_at=now, updated_at=now, ) @@ -239,13 +204,6 @@ async def test_delete_agent_not_found(self, use_case, mock_repository): with pytest.raises(AgentNotFoundError): await use_case.execute(name="nonexistent") - async def test_delete_agent_builtin_protected(self, use_case, mock_repository, builtin_metadata): - """Should raise ConfigError when trying to delete a built-in agent.""" - mock_repository.get.return_value = builtin_metadata - - with pytest.raises(ConfigError, match="built-in"): - await use_case.execute(name="builtin-agent") - class TestGetAgentConfigUseCase: """Tests for GetAgentConfigUseCase.""" @@ -296,7 +254,6 @@ async def test_list_agents_returns_metadata(self, use_case, mock_repository): name="agent-a", model="gpt-4o", minio_path="agent-configs/agent-a.yaml", - is_builtin=False, created_at=now, updated_at=now, ), @@ -304,7 +261,6 @@ async def test_list_agents_returns_metadata(self, use_case, mock_repository): name="agent-b", model="claude-sonnet-4-5-20250929", minio_path="agent-configs/agent-b.yaml", - is_builtin=True, created_at=now, updated_at=now, ), diff --git a/tests/unit/test_mcp_lifecycle.py b/tests/unit/test_mcp_lifecycle.py index ddc3ce8..54bada0 100644 --- a/tests/unit/test_mcp_lifecycle.py +++ b/tests/unit/test_mcp_lifecycle.py @@ -17,10 +17,6 @@ def test_mcp_tool_loader_is_langchain_instance(self): """The module-level mcp_tool_loader is a LangchainMcpToolLoader.""" assert isinstance(dependencies.mcp_tool_loader, LangchainMcpToolLoader) - def test_agent_registry_received_mcp_tool_loader(self): - """The agent_registry was constructed with the mcp_tool_loader.""" - assert dependencies.agent_registry._mcp_tool_loader is dependencies.mcp_tool_loader - class TestLifespanMcpCleanup: """Tests for lifespan MCP cleanup.""" @@ -35,11 +31,10 @@ async def test_lifespan_calls_mcp_tool_loader_close(self, mock_mcp_tool_loader): patch("src.main.mcp_tool_loader", mock_mcp_tool_loader), patch("src.main.close_persistence", AsyncMock()), patch("src.main.init_persistence", AsyncMock()), - patch("src.main.seed_builtin_agents", AsyncMock()), patch("src.main.tracing_provider", mock_tracing), ): async with lifespan(None): - pass # enter and exit context to trigger cleanup + pass assert mock_mcp_tool_loader._closed is True @@ -54,12 +49,11 @@ async def test_lifespan_handles_cleanup_gracefully(self): with ( patch("src.main.close_persistence", mock_close_persistence), patch("src.main.init_persistence", AsyncMock()), - patch("src.main.seed_builtin_agents", AsyncMock()), patch("src.main.mcp_tool_loader", mock_mcp), patch("src.main.tracing_provider", mock_tracing), ): async with lifespan(None): - pass # enter and exit context to trigger cleanup + pass mock_close_persistence.assert_awaited_once() mock_mcp.close.assert_awaited_once() diff --git a/tests/unit/test_persistent_registry.py b/tests/unit/test_persistent_registry.py index fa29fab..006484f 100644 --- a/tests/unit/test_persistent_registry.py +++ b/tests/unit/test_persistent_registry.py @@ -105,7 +105,6 @@ async def test_list_agents_queries_repository(self, registry, mock_repository): name="agent-a", model="gpt-4o", minio_path="agent-configs/agent-a.yaml", - is_builtin=False, created_at=now, updated_at=now, ), diff --git a/tests/unit/test_postgres_repository.py b/tests/unit/test_postgres_repository.py index 3736f03..13c0f3a 100644 --- a/tests/unit/test_postgres_repository.py +++ b/tests/unit/test_postgres_repository.py @@ -55,7 +55,6 @@ def sample_metadata(self): name="test-agent", model="claude-sonnet-4-5-20250929", minio_path="agent-configs/test-agent.yaml", - is_builtin=False, created_at=now, updated_at=now, ) @@ -80,7 +79,6 @@ async def test_get_returns_metadata(self, repository, mock_session): model.name = "test-agent" model.model = "claude-sonnet-4-5-20250929" model.minio_path = "agent-configs/test-agent.yaml" - model.is_builtin = False model.created_at = now model.updated_at = now @@ -113,7 +111,6 @@ async def test_list_all_returns_list(self, repository, mock_session): model_a.name = "agent-a" model_a.model = "gpt-4o" model_a.minio_path = "agent-configs/agent-a.yaml" - model_a.is_builtin = False model_a.created_at = now model_a.updated_at = now @@ -121,7 +118,6 @@ async def test_list_all_returns_list(self, repository, mock_session): model_b.name = "agent-b" model_b.model = "claude-sonnet-4-5-20250929" model_b.minio_path = "agent-configs/agent-b.yaml" - model_b.is_builtin = True model_b.created_at = now model_b.updated_at = now @@ -135,7 +131,6 @@ async def test_list_all_returns_list(self, repository, mock_session): assert all(isinstance(m, AgentConfigMetadata) for m in result) assert result[0].name == "agent-a" assert result[1].name == "agent-b" - assert result[1].is_builtin is True # -- delete ------------------------------------------------------------ diff --git a/tests/unit/test_registry.py b/tests/unit/test_registry.py deleted file mode 100644 index 0740846..0000000 --- a/tests/unit/test_registry.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Tests for DeepAgentRegistry. - -Uses real YamlAgentConfigLoader with tmp_path (internal). -Uses AsyncMock for mcp_tool_loader (external). -Patches create_agent_from_config and DeepAgentRunner (external LLM boundary). -""" - -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -from src.domain.exceptions import AgentNotFoundError -from src.infrastructure.deepagent.registry import DeepAgentRegistry -from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader - - -class TestDeepAgentRegistry: - @pytest.fixture - def agents_dir(self, tmp_path): - """Create a temporary agents directory with YAML files.""" - d = tmp_path / "agents" - d.mkdir() - (d / "chatbot.yaml").write_text("name: chatbot") - (d / "coder.yaml").write_text("name: coder") - return d - - @pytest.fixture - def config_loader(self): - """Real YamlAgentConfigLoader.""" - return YamlAgentConfigLoader() - - @pytest.fixture - def registry(self, agents_dir, config_loader, mock_mcp_tool_loader): - return DeepAgentRegistry( - agents_dir=agents_dir, - config_loader=config_loader, - mcp_tool_loader=mock_mcp_tool_loader, - ) - - # -- get_runner -------------------------------------------------------- - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_get_runner_creates_and_returns_runner(self, mock_runner_cls, mock_create, registry): - """get_runner should load config, create the graph, wrap it in a runner.""" - mock_graph = MagicMock() - mock_create.return_value = mock_graph - mock_runner_instance = MagicMock() - mock_runner_cls.return_value = mock_runner_instance - - runner = await registry.get_runner("chatbot") - - assert runner is mock_runner_instance - mock_create.assert_awaited_once() - mock_runner_cls.assert_called_once_with(mock_graph, tracing_provider=None) - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_get_runner_caches_on_second_call(self, mock_runner_cls, mock_create, registry): - """get_runner should return the cached runner without recreating.""" - mock_create.return_value = MagicMock() - mock_runner_cls.return_value = MagicMock() - - first = await registry.get_runner("chatbot") - second = await registry.get_runner("chatbot") - - assert first is second - assert mock_create.await_count == 1, "Factory should only be called once" - - async def test_get_runner_raises_on_unknown_agent(self, registry): - """get_runner should raise AgentNotFoundError for missing YAML.""" - with pytest.raises(AgentNotFoundError, match="Agent not found: unknown"): - await registry.get_runner("unknown") - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_get_runner_passes_tracing_provider( - self, mock_runner_cls, mock_create, agents_dir, config_loader, mock_mcp_tool_loader - ): - """get_runner should forward the tracing_provider to DeepAgentRunner.""" - tracing = MagicMock() - registry = DeepAgentRegistry( - agents_dir=agents_dir, - config_loader=config_loader, - mcp_tool_loader=mock_mcp_tool_loader, - tracing_provider=tracing, - ) - mock_create.return_value = MagicMock() - mock_runner_cls.return_value = MagicMock() - - await registry.get_runner("chatbot") - - mock_runner_cls.assert_called_once_with(mock_create.return_value, tracing_provider=tracing) - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_get_runner_passes_mcp_tool_loader_to_factory( - self, mock_runner_cls, mock_create, registry, mock_mcp_tool_loader - ): - """create_agent_from_config should receive the mcp_tool_loader.""" - mock_create.return_value = MagicMock() - mock_runner_cls.return_value = MagicMock() - - await registry.get_runner("chatbot") - - _, kwargs = mock_create.call_args - assert kwargs.get("mcp_tool_loader") is mock_mcp_tool_loader or ( - mock_create.call_args.args[1] is mock_mcp_tool_loader - ) - - # -- list_agents ------------------------------------------------------- - - async def test_list_agents_returns_sorted_yaml_stems(self, registry): - """list_agents should return sorted stem names of .yaml files.""" - result = await registry.list_agents() - assert result == ["chatbot", "coder"] - - async def test_list_agents_excludes_non_yaml_files(self, agents_dir, config_loader, mock_mcp_tool_loader): - """list_agents should ignore files that are not .yaml.""" - (agents_dir / "notes.txt").write_text("not an agent") - (agents_dir / "readme.md").write_text("docs") - registry = DeepAgentRegistry( - agents_dir=agents_dir, - config_loader=config_loader, - mcp_tool_loader=mock_mcp_tool_loader, - ) - result = await registry.list_agents() - assert result == ["chatbot", "coder"] - - async def test_list_agents_returns_empty_when_dir_missing(self, tmp_path, config_loader, mock_mcp_tool_loader): - """list_agents should return [] if agents_dir does not exist.""" - registry = DeepAgentRegistry( - agents_dir=tmp_path / "nonexistent", - config_loader=config_loader, - mcp_tool_loader=mock_mcp_tool_loader, - ) - result = await registry.list_agents() - assert result == [] - - async def test_list_agents_returns_empty_when_no_yaml_files(self, tmp_path, config_loader, mock_mcp_tool_loader): - """list_agents should return [] if the directory has no .yaml files.""" - empty_dir = tmp_path / "empty_agents" - empty_dir.mkdir() - registry = DeepAgentRegistry( - agents_dir=empty_dir, - config_loader=config_loader, - mcp_tool_loader=mock_mcp_tool_loader, - ) - result = await registry.list_agents() - assert result == [] - - # -- invalidate -------------------------------------------------------- - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_invalidate_removes_cached_runner(self, mock_runner_cls, mock_create, registry): - """After invalidate, get_runner should re-build the agent from scratch.""" - mock_create.return_value = MagicMock() - runner_a = MagicMock() - runner_b = MagicMock() - mock_runner_cls.side_effect = [runner_a, runner_b] - - first = await registry.get_runner("chatbot") - assert first is runner_a - - await registry.invalidate("chatbot") - assert "chatbot" not in registry._runners, "Cache entry should be removed" - - second = await registry.get_runner("chatbot") - assert second is runner_b - assert first is not second - assert mock_create.await_count == 2 - - # -- close ------------------------------------------------------------- - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_close_clears_cached_runners(self, mock_runner_cls, mock_create, registry): - """close should clear the internal runner cache.""" - mock_create.return_value = MagicMock() - mock_runner_cls.return_value = MagicMock() - - await registry.get_runner("chatbot") - assert registry._runners, "Runner should be cached before close" - - await registry.close() - assert registry._runners == {}, "Cache should be empty after close" - - @patch("src.infrastructure.deepagent.registry.create_agent_from_config", new_callable=AsyncMock) - @patch("src.infrastructure.deepagent.registry.DeepAgentRunner") - async def test_close_then_get_runner_recreates(self, mock_runner_cls, mock_create, registry): - """After close, get_runner should create the runner again from scratch.""" - mock_create.return_value = MagicMock() - runner_a = MagicMock() - runner_b = MagicMock() - mock_runner_cls.side_effect = [runner_a, runner_b] - - first = await registry.get_runner("chatbot") - await registry.close() - second = await registry.get_runner("chatbot") - - assert first is runner_a - assert second is runner_b - assert first is not second - assert mock_create.await_count == 2 diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index c24fa73..98c4145 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -2,7 +2,7 @@ Uses real InMemoryThreadRepository and YamlAgentConfigLoader (internal). Uses AsyncMock for AgentRunner (external LLM boundary). -Uses a real DeepAgentRegistry with patched factory for agent creation. +Uses real PersistentAgentRegistry with mocked store/repository for agent creation. """ from datetime import UTC, datetime @@ -14,26 +14,26 @@ from src.domain.entities.agent_config_metadata import AgentConfigMetadata from src.domain.entities.message import Message, MessageRole, MessageStatus from src.domain.exceptions import AgentError -from src.infrastructure.deepagent.registry import DeepAgentRegistry +from src.infrastructure.persistent_registry.adapter import PersistentAgentRegistry from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader from src.main import app from tests.fixtures.in_memory_thread_repository import InMemoryThreadRepository +VALID_YAML = ( + "name: {name}\n" + "model: test-model\n" + 'system_prompt: "Test prompt."\n' + "tools: []\n" + "debug: false\n" +) + +AGENTS = ["my-agent", "agent-1", "agent-2", "example-agent", "code-reviewer", "minimal-agent", "research-assistant", "mcp-agent"] + @pytest.fixture -def agents_dir(tmp_path): - """Create a temporary agents directory with real YAML files.""" - d = tmp_path / "agents" - d.mkdir() - (d / "my-agent.yaml").write_text("name: my-agent") - (d / "agent-1.yaml").write_text("name: agent-1") - (d / "agent-2.yaml").write_text("name: agent-2") - (d / "example-agent.yaml").write_text('name: example-agent\nmodel: "test-model"\nsystem_prompt: "Test prompt."') - (d / "code-reviewer.yaml").write_text('name: code-reviewer\nmodel: "test-model"') - (d / "minimal.yaml").write_text("name: minimal-agent") - (d / "research-assistant.yaml").write_text('name: research-assistant\nmodel: "test-model"') - (d / "mcp-agent.yaml").write_text('name: mcp-agent\nmodel: "test-model"\nsystem_prompt: "MCP agent."') - return d +def yaml_store(): + """In-memory YAML store keyed by agent name.""" + return {name: VALID_YAML.format(name=name) for name in AGENTS} @pytest.fixture @@ -72,48 +72,33 @@ def real_loader(): @pytest.fixture -def real_registry(agents_dir, real_loader, mock_mcp_tool_loader): - return DeepAgentRegistry( - agents_dir=agents_dir, - config_loader=real_loader, - mcp_tool_loader=mock_mcp_tool_loader, - ) - - -@pytest.fixture -def mock_config_store(agents_dir): - """AsyncMock for AgentConfigStore that reads from the tmp agents_dir.""" +def mock_config_store(yaml_store): + """AsyncMock for AgentConfigStore that reads from the in-memory yaml_store.""" store = AsyncMock() async def _get(name): - from pathlib import Path - - path = Path(agents_dir) / f"{name}.yaml" - if not path.exists(): + if name not in yaml_store: from src.domain.exceptions import AgentNotFoundError - raise AgentNotFoundError(f"Agent config not found: {name}") - return path.read_text() + return yaml_store[name] store.get.side_effect = _get return store @pytest.fixture -def mock_config_repository(agents_dir): - """AsyncMock for AgentConfigRepository that lists agents from the tmp agents_dir.""" +def mock_config_repository(yaml_store): + """AsyncMock for AgentConfigRepository that lists agents from the in-memory store.""" repo = AsyncMock() - from pathlib import Path now = datetime.now(UTC) metadata_list = [] - for f in sorted(Path(agents_dir).glob("*.yaml")): + for name in sorted(yaml_store.keys()): metadata_list.append( AgentConfigMetadata( - name=f.stem, + name=name, model="test-model", - minio_path=f"{f.stem}.yaml", - is_builtin=False, + minio_path=f"{name}.yaml", created_at=now, updated_at=now, ) @@ -122,25 +107,34 @@ def mock_config_repository(agents_dir): return repo +@pytest.fixture +def real_registry(real_loader, mock_config_store, mock_config_repository, mock_mcp_tool_loader): + return PersistentAgentRegistry( + config_loader=real_loader, + config_store=mock_config_store, + config_repository=mock_config_repository, + mcp_tool_loader=mock_mcp_tool_loader, + ) + + @pytest.fixture(autouse=True) def _wire_dependencies( - real_threads, real_registry, real_loader, agents_dir, mock_runner, mock_config_store, mock_config_repository + real_threads, real_registry, real_loader, mock_runner, mock_config_store, mock_config_repository ): """Wire real internal components + mocked runner into the app dependencies.""" with ( patch( - "src.infrastructure.deepagent.registry.create_agent_from_config", + "src.infrastructure.persistent_registry.adapter.create_agent_from_config", new_callable=AsyncMock, return_value=MagicMock(), ), patch( - "src.infrastructure.deepagent.registry.DeepAgentRunner", + "src.infrastructure.persistent_registry.adapter.DeepAgentRunner", return_value=mock_runner, ), patch("src.dependencies.thread_repository", real_threads), patch("src.dependencies.agent_registry", real_registry), patch("src.dependencies.agent_config_loader", real_loader), - patch("src.dependencies.agents_dir", str(agents_dir)), patch("src.dependencies._minio_store", mock_config_store), patch("src.dependencies._pg_repository", mock_config_repository), ): diff --git a/tests/unit/test_seed_agents.py b/tests/unit/test_seed_agents.py deleted file mode 100644 index f00fd98..0000000 --- a/tests/unit/test_seed_agents.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Tests for SeedAgentsUseCase. - -Mocks AgentConfigStore and AgentConfigRepository (external boundaries). -Uses real YamlAgentConfigLoader (internal). -""" - -from unittest.mock import AsyncMock - -import pytest - -from src.application.use_cases.seed_agents import SeedAgentsUseCase -from src.domain.ports.agent_config_repository import AgentConfigRepository -from src.domain.ports.agent_config_store import AgentConfigStore -from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader - - -class TestSeedAgentsUseCase: - """Tests for SeedAgentsUseCase.""" - - @pytest.fixture - def loader(self): - """Real YamlAgentConfigLoader (internal).""" - return YamlAgentConfigLoader() - - @pytest.fixture - def mock_store(self): - return AsyncMock(spec=AgentConfigStore) - - @pytest.fixture - def mock_repository(self): - return AsyncMock(spec=AgentConfigRepository) - - @pytest.fixture - def use_case(self, loader, mock_store, mock_repository): - return SeedAgentsUseCase( - config_loader=loader, - config_store=mock_store, - config_repository=mock_repository, - ) - - @pytest.fixture - def agents_dir(self, tmp_path): - """Create a temporary agents directory with seed YAML files.""" - d = tmp_path / "agents" - d.mkdir() - (d / "chatbot.yaml").write_text( - 'name: chatbot\nmodel: claude-sonnet-4-5-20250929\nsystem_prompt: "You are a chatbot."\n' - ) - (d / "coder.yaml").write_text( - 'name: coder\nmodel: claude-sonnet-4-5-20250929\nsystem_prompt: "You are a coding assistant."\n' - ) - return d - - async def test_seed_uploads_new_agents(self, use_case, mock_store, mock_repository, agents_dir): - """For each YAML file not in PG, should upload to MinIO and save metadata.""" - mock_repository.exists.return_value = False - - await use_case.execute(agents_dir=agents_dir) - - # Both agents should be uploaded since neither exists - assert mock_store.put.await_count == 2 - assert mock_repository.save.await_count == 2 - - async def test_seed_skips_existing_agents(self, use_case, mock_store, mock_repository, agents_dir): - """If agent already exists in PG, should not upload to MinIO.""" - # chatbot exists, coder does not - mock_repository.exists.side_effect = lambda name: name == "chatbot" - - await use_case.execute(agents_dir=agents_dir) - - # Only coder should be uploaded - assert mock_store.put.await_count == 1 - assert mock_repository.save.await_count == 1 - put_call_name = mock_store.put.call_args[0][0] - assert put_call_name == "coder" - - async def test_seed_inlines_system_prompt_file(self, use_case, mock_store, mock_repository, tmp_path): - """If YAML references system_prompt_file, should read it and inline the prompt before uploading.""" - d = tmp_path / "agents_with_prompt" - d.mkdir() - prompt_file = d / "prompt.md" - prompt_file.write_text("You are a specialized agent with inlined prompt.") - (d / "prompted-agent.yaml").write_text('name: prompted-agent\nsystem_prompt_file: "./prompt.md"\n') - mock_repository.exists.return_value = False - - await use_case.execute(agents_dir=d) - - mock_store.put.assert_awaited_once() - # The uploaded YAML content should have the prompt inlined (no system_prompt_file reference) - uploaded_content = mock_store.put.call_args[0][1] - assert "system_prompt_file" not in uploaded_content - assert "You are a specialized agent with inlined prompt." in uploaded_content diff --git a/tests/unit/test_send_message.py b/tests/unit/test_send_message.py index 0fa16a9..fb5befb 100644 --- a/tests/unit/test_send_message.py +++ b/tests/unit/test_send_message.py @@ -2,7 +2,6 @@ Uses real InMemoryThreadRepository (internal). Uses AsyncMock for AgentRunner (external - calls LLM). -The DeepAgentRegistry is real but with patched factory to avoid LLM calls. """ from unittest.mock import AsyncMock, MagicMock diff --git a/tests/unit/test_thread_management.py b/tests/unit/test_thread_management.py index 3fe8e4a..1c1cd96 100644 --- a/tests/unit/test_thread_management.py +++ b/tests/unit/test_thread_management.py @@ -4,6 +4,9 @@ Uses AsyncMock for AgentRegistry.get_runner (external dependency boundary). """ +from datetime import UTC, datetime +from unittest.mock import AsyncMock + import pytest from src.application.use_cases.thread_management import ( @@ -12,25 +15,50 @@ GetThreadUseCase, ListThreadsUseCase, ) +from src.domain.entities.agent_config_metadata import AgentConfigMetadata from src.domain.exceptions import AgentNotFoundError, ThreadNotFoundError -from src.infrastructure.deepagent.registry import DeepAgentRegistry +from src.domain.ports.agent_config_repository import AgentConfigRepository +from src.domain.ports.agent_config_store import AgentConfigStore +from src.infrastructure.persistent_registry.adapter import PersistentAgentRegistry from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader +VALID_YAML = ( + "name: test-agent\n" + "model: test-model\n" + 'system_prompt: "Test."\n' + "tools: []\n" + "debug: false\n" +) + class TestCreateThreadUseCase: @pytest.fixture - def agents_dir(self, tmp_path): - d = tmp_path / "agents" - d.mkdir() - (d / "test-agent.yaml").write_text("name: test-agent") - return d + def mock_store(self): + store = AsyncMock(spec=AgentConfigStore) + store.get.return_value = VALID_YAML + return store @pytest.fixture - def registry(self, agents_dir, mock_mcp_tool_loader): - """Real DeepAgentRegistry with real YAML files.""" - return DeepAgentRegistry( - agents_dir=agents_dir, + def mock_repository(self): + repo = AsyncMock(spec=AgentConfigRepository) + now = datetime.now(UTC) + repo.list_all.return_value = [ + AgentConfigMetadata( + name="test-agent", + model="test-model", + minio_path="test-agent.yaml", + created_at=now, + updated_at=now, + ) + ] + return repo + + @pytest.fixture + def registry(self, mock_store, mock_repository, mock_mcp_tool_loader): + return PersistentAgentRegistry( config_loader=YamlAgentConfigLoader(), + config_store=mock_store, + config_repository=mock_repository, mcp_tool_loader=mock_mcp_tool_loader, ) @@ -41,7 +69,10 @@ async def test_create_thread(self, thread_repo, registry): assert thread.agent_name == "test-agent" assert thread.id is not None - async def test_create_thread_unknown_agent_raises(self, thread_repo, registry): + async def test_create_thread_unknown_agent_raises(self, thread_repo, registry, mock_store): + from src.domain.exceptions import AgentNotFoundError as ANF + + mock_store.get.side_effect = ANF("not found") use_case = CreateThreadUseCase(thread_repo, registry) with pytest.raises(AgentNotFoundError): diff --git a/tests/unit/test_tracing_di.py b/tests/unit/test_tracing_di.py index 7b2025e..d6182f8 100644 --- a/tests/unit/test_tracing_di.py +++ b/tests/unit/test_tracing_di.py @@ -19,7 +19,7 @@ def test_default_settings_create_noop_provider(self, monkeypatch): monkeypatch.delenv("PROVIDER", raising=False) tracing = TracingSettings(provider="none", enabled=False) - settings = Settings(agents_dir="./agents", tracing=tracing) + settings = Settings(tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) @@ -28,7 +28,7 @@ def test_disabled_langfuse_creates_noop_provider(self, monkeypatch): """When langfuse is disabled, _create_tracing_provider returns NoopTracingProvider.""" tracing = TracingSettings(provider="langfuse", enabled=False) - settings = Settings(agents_dir="./agents", tracing=tracing) + settings = Settings(tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) @@ -37,7 +37,7 @@ def test_disabled_phoenix_creates_noop_provider(self, monkeypatch): """When phoenix is disabled, _create_tracing_provider returns NoopTracingProvider.""" tracing = TracingSettings(provider="phoenix", enabled=False) - settings = Settings(agents_dir="./agents", tracing=tracing) + settings = Settings(tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) @@ -46,7 +46,7 @@ def test_unknown_provider_creates_noop(self, monkeypatch): """When provider is unknown, _create_tracing_provider returns NoopTracingProvider.""" tracing = TracingSettings(provider="unknown", enabled=True) - settings = Settings(agents_dir="./agents", tracing=tracing) + settings = Settings(tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) \ No newline at end of file diff --git a/tests/unit/test_tracing_lifecycle.py b/tests/unit/test_tracing_lifecycle.py index 94db9f9..5ba9960 100644 --- a/tests/unit/test_tracing_lifecycle.py +++ b/tests/unit/test_tracing_lifecycle.py @@ -3,44 +3,46 @@ Uses mock_tracing_provider for verifying flush/shutdown calls (external). """ -from unittest.mock import AsyncMock, patch - class TestTracingLifecycle: async def test_lifespan_calls_tracing_flush(self, mock_tracing_provider): """Lifespan shutdown calls flush() on the tracing_provider.""" + from unittest.mock import AsyncMock, patch + from src.main import lifespan with ( patch("src.main.close_persistence", AsyncMock()), patch("src.main.init_persistence", AsyncMock()), - patch("src.main.seed_builtin_agents", AsyncMock()), patch("src.main.mcp_tool_loader", AsyncMock()), patch("src.main.tracing_provider", mock_tracing_provider), ): async with lifespan(None): - pass # enter and exit context to trigger cleanup + pass mock_tracing_provider.flush.assert_awaited_once() async def test_lifespan_calls_tracing_shutdown(self, mock_tracing_provider): """Lifespan shutdown calls shutdown() on the tracing_provider.""" + from unittest.mock import AsyncMock, patch + from src.main import lifespan with ( patch("src.main.close_persistence", AsyncMock()), patch("src.main.init_persistence", AsyncMock()), - patch("src.main.seed_builtin_agents", AsyncMock()), patch("src.main.mcp_tool_loader", AsyncMock()), patch("src.main.tracing_provider", mock_tracing_provider), ): async with lifespan(None): - pass # enter and exit context to trigger cleanup + pass mock_tracing_provider.shutdown.assert_awaited_once() async def test_lifespan_flush_before_shutdown(self, mock_tracing_provider): """Lifespan calls flush() before shutdown() on the tracing_provider.""" + from unittest.mock import AsyncMock, patch + from src.main import lifespan call_order = [] @@ -62,11 +64,10 @@ async def track_shutdown(): with ( patch("src.main.close_persistence", AsyncMock()), patch("src.main.init_persistence", AsyncMock()), - patch("src.main.seed_builtin_agents", AsyncMock()), patch("src.main.mcp_tool_loader", AsyncMock()), patch("src.main.tracing_provider", mock_tracing_provider), ): async with lifespan(None): - pass # enter and exit context to trigger cleanup + pass assert call_order == ["flush", "shutdown"] diff --git a/tests/unit/test_yaml_loader.py b/tests/unit/test_yaml_loader.py index 981a11a..7a52941 100644 --- a/tests/unit/test_yaml_loader.py +++ b/tests/unit/test_yaml_loader.py @@ -118,18 +118,3 @@ def test_load_from_string_with_system_prompt_file(self, loader): yaml_content = 'name: test-agent\nsystem_prompt_file: "./prompt.md"\n' with pytest.raises(ConfigError, match="system_prompt_file"): loader.load_from_string(yaml_content) - - # -- load (file-based, existing tests) --------------------------------- - - def test_loads_real_estate_extractor(self, loader): - """Verify real-estate-extractor.yaml loads with subagents, response_format, and MCP servers.""" - config = loader.load("agents/real-estate-extractor.yaml") - assert config.name == "real-estate-extractor" - assert len(config.subagents) == 3 - - for sa in config.subagents: - assert sa.response_format is not None - assert sa.response_format["type"] == "object" - assert "properties" in sa.response_format - assert len(sa.mcp_servers) == 1 - assert sa.mcp_servers[0].name == "raganything" diff --git a/uv.lock b/uv.lock index 3fd7aab..aa1ed17 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14'", @@ -270,7 +270,7 @@ wheels = [ [[package]] name = "arize-phoenix-otel" -version = "0.14.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openinference-instrumentation" }, @@ -282,9 +282,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/17/ebd502f1bd8a0a6087ea28be8de8b765bcf34fb1b9bc4d6fd6d91bd822f1/arize_phoenix_otel-0.14.0.tar.gz", hash = "sha256:ad1368f0f52c242591ec554cedeccf718abda81383cf8c8d3ade218a7b20b955", size = 20155, upload-time = "2025-11-19T19:48:29.447Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/f0/b254118db28a2a202573472be67cf61f09cb37912bfde45b27ddc1c5b71f/arize_phoenix_otel-0.15.0.tar.gz", hash = "sha256:56c7dae09aaaa80df9e9595b7384c1bd4054b69b6032ab18e3a110a59b488388", size = 20254, upload-time = "2026-03-02T20:19:04.112Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/be/e7ddb54c4ad6115d2d468b71e90d7a2718735fd217f05c50759799191bfe/arize_phoenix_otel-0.14.0-py3-none-any.whl", hash = "sha256:47bf5563b9342a931385a16609ca83ada44d56a00bf6ed3be199226792b9937f", size = 17708, upload-time = "2025-11-19T19:48:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4d/70d9c9d7137cc2e2aad819932172ef13ce21b4e60bf258910b9f15e426af/arize_phoenix_otel-0.15.0-py3-none-any.whl", hash = "sha256:5ff4d03b52d2dbd9c2a234417848f6b171cd220dc3c4020cf3568be84b89b88b", size = 17697, upload-time = "2026-03-02T20:19:03.242Z" }, ] [[package]] @@ -344,15 +344,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, -] - [[package]] name = "bracex" version = "2.6" @@ -550,6 +541,8 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "alembic" }, + { name = "arize-phoenix-client" }, + { name = "arize-phoenix-otel" }, { name = "asyncpg" }, { name = "cachetools" }, { name = "cryptography" }, @@ -560,6 +553,7 @@ dependencies = [ { name = "langchain-openai" }, { name = "langgraph" }, { name = "miniopy-async" }, + { name = "openinference-instrumentation-langchain" }, { name = "pyasn1" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -571,22 +565,6 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] -[package.optional-dependencies] -langfuse = [ - { name = "langfuse" }, -] -phoenix = [ - { name = "arize-phoenix-client" }, - { name = "arize-phoenix-otel" }, - { name = "openinference-instrumentation-langchain" }, -] -tracing = [ - { name = "arize-phoenix-client" }, - { name = "arize-phoenix-otel" }, - { name = "langfuse" }, - { name = "openinference-instrumentation-langchain" }, -] - [package.dev-dependencies] dev = [ { name = "httpx" }, @@ -600,10 +578,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.13.0" }, - { name = "arize-phoenix-client", marker = "extra == 'phoenix'", specifier = ">=2.3.0" }, - { name = "arize-phoenix-client", marker = "extra == 'tracing'", specifier = ">=2.3.0" }, - { name = "arize-phoenix-otel", marker = "extra == 'phoenix'", specifier = ">=0.1.0" }, - { name = "arize-phoenix-otel", marker = "extra == 'tracing'", specifier = ">=0.1.0" }, + { name = "arize-phoenix-client", specifier = "==2.3.0" }, + { name = "arize-phoenix-otel", specifier = "==0.15.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, { name = "cachetools", specifier = ">=7.0.5" }, { name = "cryptography", specifier = ">=46.0.5" }, @@ -612,12 +588,9 @@ requires-dist = [ { name = "langchain-core", specifier = ">=1.2.22" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.0" }, { name = "langchain-openai", specifier = ">=1.1.7" }, - { name = "langfuse", marker = "extra == 'langfuse'", specifier = ">=2.0.0" }, - { name = "langfuse", marker = "extra == 'tracing'", specifier = ">=2.0.0" }, { name = "langgraph", specifier = ">=1.0.10" }, { name = "miniopy-async", specifier = ">=1.21.0" }, - { name = "openinference-instrumentation-langchain", marker = "extra == 'phoenix'", specifier = ">=0.1.0" }, - { name = "openinference-instrumentation-langchain", marker = "extra == 'tracing'", specifier = ">=0.1.0" }, + { name = "openinference-instrumentation-langchain", specifier = "==0.1.62" }, { name = "pyasn1", specifier = ">=0.6.3" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, @@ -628,7 +601,6 @@ requires-dist = [ { name = "sse-starlette", specifier = ">=3.2.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.40.0" }, ] -provides-extras = ["langfuse", "phoenix", "tracing"] [package.metadata.requires-dev] dev = [ @@ -1457,27 +1429,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/a1/50e7596aca775d8c3883eceeaf47489fac26c57c1abe243c00174f715a8a/langchain_openai-1.1.7-py3-none-any.whl", hash = "sha256:34e9cd686aac1a120d6472804422792bf8080a2103b5d21ee450c9e42d053815", size = 84753, upload-time = "2026-01-07T19:44:58.629Z" }, ] -[[package]] -name = "langfuse" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backoff" }, - { name = "httpx" }, - { name = "openai" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/d0/744e5613c728427330ac2049da0f54fc313e8bf84622f71b025bfba65496/langfuse-3.13.0.tar.gz", hash = "sha256:dacea8111ca4442e97dbfec4f8d676cf9709b35357a26e468f8887b95de0012f", size = 233420, upload-time = "2026-02-06T19:54:14.415Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/63/148382e8e79948f7e5c9c137288e504bb88117574eb7e7c886b4fb470b4b/langfuse-3.13.0-py3-none-any.whl", hash = "sha256:71912ddac1cc831a65df895eae538a556f564c094ae51473e747426e9ded1a9d", size = 417626, upload-time = "2026-02-06T19:54:12.547Z" }, -] - [[package]] name = "langgraph" version = "1.1.3" @@ -1945,7 +1896,7 @@ wheels = [ [[package]] name = "openinference-instrumentation-langchain" -version = "0.1.58" +version = "0.1.62" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openinference-instrumentation" }, @@ -1955,9 +1906,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/f7/ed82c3d146ca6f1b62dabb2e01fbee782a75245d694b23bc90232366dac7/openinference_instrumentation_langchain-0.1.58.tar.gz", hash = "sha256:36a1b1ad162c4e356bd28257173ee3171ad7788a96089553512c6288fa9a0f1c", size = 75239, upload-time = "2026-01-06T23:50:16.243Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/02/4c17a89ac11a539eb6d8a31dce6484e880d104cc772d823bef9b9135267a/openinference_instrumentation_langchain-0.1.62.tar.gz", hash = "sha256:6bbfc59de52082ec0b464ed88361d2c8deb4e3c22bc4ad36d0c9f46154723cef", size = 75564, upload-time = "2026-04-03T21:41:12.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/10/df4805c99e9b17fdd4496b080788340dd09ebc436dd5073e54a1c2633a04/openinference_instrumentation_langchain-0.1.58-py3-none-any.whl", hash = "sha256:9dd2e0b201131e53d9e520624ef4eea6268c08faab1dc10d64b52c60b5169d91", size = 24396, upload-time = "2026-01-06T23:50:14.022Z" }, + { url = "https://files.pythonhosted.org/packages/73/87/e7c41028b3c3ddfd89325d03571e640b54522ab235cdca2a14a14c61ed00/openinference_instrumentation_langchain-0.1.62-py3-none-any.whl", hash = "sha256:6fcf128c4f5d2e5ed353a728ae9af58b0903f9eb40f0c28a58e81319b1b91195", size = 24761, upload-time = "2026-04-03T21:41:11.159Z" }, ] [[package]]