diff --git a/backend/app/agents/dev_agent.py b/backend/app/agents/dev_agent.py index 950b65a..86da974 100644 --- a/backend/app/agents/dev_agent.py +++ b/backend/app/agents/dev_agent.py @@ -1,14 +1,69 @@ +import json +import logging +import httpx +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, HumanMessage +from app.core.config import settings +from app.schemas.code_output import ProjectCodeOutput, GeneratedFile +from app.llm.prompts import DEV_AGENT_PROMPT + +logger = logging.getLogger(__name__) + +# Clients HTTPX configurés pour ignorer les blocages de certificats/révocation du lab +sync_client = httpx.Client(verify=False) +async_client = httpx.AsyncClient(verify=False) + async def run_dev_agent(spec: dict, qa_feedback: list = None) -> dict: """ - Agent Dev minimal : - - retourne une pseudo arborescence + un code exemple + Agent Dev : prend un état/spec validé, génère l'arborescence et le code, + et valide techniquement la sortie avant de la transmettre à la QA. """ - return { - "tree": [ - "main.py", - "README.md", - "app/__init__.py", - ], - "code": 'print("Hello from ARC generated project")', - "spec_title": spec.get("title"), - } \ No newline at end of file + logger.info(f"[Dev Agent] Début de la génération pour : {spec.get('title', 'Sans titre')}") + + llm = ChatOpenAI( + base_url=settings.llm_base_url, + api_key=settings.llm_api_key, + model=settings.llm_model_dev, + temperature=0.2, + max_retries=2, + http_client=sync_client, + http_async_client=async_client, + model_kwargs={"response_format": {"type": "json_object"}} + ) + structured_llm = llm.with_structured_output(ProjectCodeOutput, strict=True) + + messages = [ + SystemMessage(content=DEV_AGENT_PROMPT), + ] + + user_content = f"CAHIER DES CHARGES (ProjectSpec) :\n{json.dumps(spec, indent=2, ensure_ascii=False)}\n\n" + if qa_feedback: + user_content += f"⚠️ RETOURS DE VALIDATION QA (Corrections à appliquer impérativement) :\n{json.dumps(qa_feedback, indent=2, ensure_ascii=False)}\n\n" + + user_content += "Génère maintenant le JSON complet contenant l'arborescence ('tree') et tous les fichiers ('files') décrits." + messages.append(HumanMessage(content=user_content)) + + try: + validated_code = await structured_llm.ainvoke(messages) + logger.info(f"[Dev Agent] ✅ Code généré et validé avec succès ({len(validated_code.files)} fichiers)") + return validated_code.model_dump() + + except Exception as e: + logger.error(f"[Dev Agent] ❌ Échec de la génération/validation : {type(e).__name__}: {str(e)}") + return _generate_fallback_code_output(spec) + +def _generate_fallback_code_output(spec: dict) -> dict: + """Génère un livrable minimal de secours en cas de crash du LLM.""" + logger.warning("[Dev Agent] Génération du package de secours (Fallback)") + title = spec.get("title", "automation_script") + + fallback = ProjectCodeOutput( + spec_title=title, + tree=["main.py", "README.md", "requirements.txt"], + files=[ + GeneratedFile(path="main.py", content="import logging\nlogging.basicConfig(level=logging.INFO)\n\ndef main():\n logging.error('Le Dev Agent a rencontré une erreur de génération.')\n\nif __name__ == '__main__':\n main()"), + GeneratedFile(path="README.md", content=f"# {title}\nGénération en mode fallback suite à une erreur technique."), + GeneratedFile(path="requirements.txt", content="# Aucune dépendance externe définie (Fallback)\n") + ] + ) + return fallback.model_dump() \ No newline at end of file diff --git a/backend/app/agents/pm_agent.py b/backend/app/agents/pm_agent.py index 975a5cd..4cbc880 100644 --- a/backend/app/agents/pm_agent.py +++ b/backend/app/agents/pm_agent.py @@ -1,52 +1,49 @@ -#pm_agent.py -import json -import re import logging +import httpx from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, HumanMessage from app.schemas.spec import ProjectSpec from app.llm.prompts import PM_AGENT_PROMPT from app.core.config import settings logger = logging.getLogger(__name__) +sync_client = httpx.Client(verify=False) +async_client = httpx.AsyncClient(verify=False) async def run_pm_agent(user_input: str, history: list | None = None) -> ProjectSpec: """Agent PM qui remplit ProjectSpec en analysant le besoin utilisateur.""" - + llm = ChatOpenAI( base_url=settings.llm_base_url, api_key=settings.llm_api_key, - model="local-model", + model=settings.llm_model, temperature=0.3, + max_retries=2, + http_client=sync_client, + http_async_client=async_client, ) + structured_llm = llm.with_structured_output(ProjectSpec, strict=True) messages = [ - {"role": "system", "content": PM_AGENT_PROMPT}, + SystemMessage(content=PM_AGENT_PROMPT), ] if history: messages.extend(history) - messages.append({ - "role": "user", - "content": f"""En te basant sur TOUT l'historique de notre conversation et sur cette dernière précision, génère le JSON ProjectSpec valide et complètement rempli. + messages.append(HumanMessage( + content=f"""En te basant sur TOUT l'historique de notre conversation et sur cette dernière précision, génère le JSON ProjectSpec valide et complètement rempli. DERNIÈRE PRÉCISION / INPUT UTILISATEUR : {user_input} IMPORTANT : Tu dois impérativement combiner les contraintes énoncées précédemment (comme le langage de programmation choisi, l'OS, etc.) avec cette nouvelle information pour enrichir la spécification existante. Réponds UNIQUEMENT avec du JSON valide. Aucun texte avant le JSON, aucun texte après le JSON.""" - }) + )) try: - response = await llm.ainvoke(messages) - raw_response = response.content if hasattr(response, 'content') else str(response) - logger.debug(f"[PM Agent] Réponse brute: {raw_response}") - - parsed_spec = _extract_json_robust(raw_response) - - project_spec = ProjectSpec(**parsed_spec) - logger.info(f"[PM Agent] ✅ Tentative réussie (parsing JSON manuel)") - + project_spec = await structured_llm.ainvoke(messages) + logger.info(f"[PM Agent] ✅ Tentative réussie (Structured Output)") project_spec = _validate_and_sanitize(project_spec, user_input) return project_spec @@ -56,62 +53,6 @@ Réponds UNIQUEMENT avec du JSON valide. Aucun texte avant le JSON, aucun texte return _generate_fallback_spec(user_input) - -def _extract_json_robust(response_text: str) -> dict: - """ - Extrait et parse le JSON depuis la réponse du LLM. - Robuste pour llama.cpp qui ajoute souvent du texte avant/après le JSON. - """ - - logger.debug(f"[JSON Extract] Réponse brute: {response_text}") - - # Stratégie 1: Chercher un bloc JSON entre ```json et ``` - json_match = re.search(r'```json\s*(.*?)\s*```', response_text, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - logger.debug(f"[JSON Extract] ✓ Trouvé via ```json block") - return json.loads(json_str) - - # Stratégie 2: Chercher un bloc JSON entre ``` et ``` (sans spécifier json) - json_match = re.search(r'```\s*(.*?)\s*```', response_text, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - logger.debug(f"[JSON Extract] ✓ Trouvé via ``` block") - try: - return json.loads(json_str) - except json.JSONDecodeError: - logger.debug(f"[JSON Extract] ✗ Code block trouvé mais pas JSON valide") - - # Stratégie 3: Chercher directement un objet JSON { ... } en étant très prudent - first_brace = response_text.find('{') - last_brace = response_text.rfind('}') - - if first_brace != -1 and last_brace != -1 and last_brace > first_brace: - json_str = response_text[first_brace:last_brace+1].strip() - - logger.debug(f"[JSON Extract] Tentative extraction (longueur: {len(json_str)})") - - try: - parsed = json.loads(json_str) - logger.debug(f"[JSON Extract] ✓ Trouvé via extraction") - return parsed - except json.JSONDecodeError as e: - logger.debug(f"[JSON Extract] ✗ Extraction invalide: {e}") - - # Stratégie 4: Parser tout le texte comme JSON (dernier recours) - try: - parsed = json.loads(response_text.strip()) - logger.debug(f"[JSON Extract] ✓ Trouvé via parsing direct") - return parsed - except json.JSONDecodeError: - logger.debug(f"[JSON Extract] ✗ Parsing direct échoué") - - # Aucune stratégie n'a fonctionné - logger.error(f"[JSON Extract] ❌ Impossible d'extraire JSON valide") - logger.error(f"[JSON Extract] Réponse brute complète:\n{response_text}") - raise ValueError(f"Impossible d'extraire un JSON valide de la réponse du LLM.\nRéponse: {response_text}") - - def _validate_and_sanitize(spec: ProjectSpec, user_input: str) -> ProjectSpec: """ Fait confiance au LLM pour la logique métier. diff --git a/backend/app/core/config.py b/backend/app/core/config.py index b598e0c..3680c8e 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,6 +1,5 @@ from pydantic_settings import BaseSettings, SettingsConfigDict - class Settings(BaseSettings): app_name: str = "ARC Backend" app_env: str = "dev" @@ -12,14 +11,18 @@ class Settings(BaseSettings): redis_url: str = "redis://localhost:6379/0" - llm_base_url: str = "http://gemma-server:8080/v1" - llm_api_key: str = "llama-cpp-local" - # llm_model: str = "gemma-4-E4B-it-UD-Q4_K_XL.gguf" + llm_base_url: str + llm_api_key: str + llm_model: str + llm_model_dev: str embedding_base_url: str = "http://localhost:8002/v1" embedding_model: str = "snowflake-arctic-embed-m-v1.5" - model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") - + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore" + ) settings = Settings() \ No newline at end of file diff --git a/backend/app/llm/prompts.py b/backend/app/llm/prompts.py index 18e87cb..7e1e293 100644 --- a/backend/app/llm/prompts.py +++ b/backend/app/llm/prompts.py @@ -267,39 +267,86 @@ CRITICAL: Si la demande de l'utilisateur est trop courte, trop vague (ex: 'Je ve Ci-dessous la demande utilisateur brute. Tu dois transformer cela en JSON ProjectSpec valide, rempli et cohérent. """ -# DEV_AGENT_PROMPT = """Vous êtes un Product Manager & Architecte Logiciel Senior spécialisé dans la conception d'outils d'automatisation et de scripts robustes en Python. -# Votre objectif est double : -# 1. Agir comme garde-fou technique en imposant des standards de développement d'entreprise drastiques. -# 2. Fournir un code Python prêt à l'emploi, structuré et commenté, accompagné d'un fichier 'requirements.txt' et d'un 'README.md' de qualité professionnelle. +# ============================================================================================================================================== +# ********************************************************************************************************************************************** +# ============================================================================================================================================== -# --- -# ### DIRECTIVES DE CONCEPTION ET STANDARDS IMPOSÉS : -# Vous devez concevoir la solution technique en respectant les règles suivantes : +DEV_AGENT_PROMPT = """Vous êtes un Développeur Senior & Architecte Logiciel Multi-langages, spécialisé dans l'écriture d'outils d'automatisation, de scripts et d'applications modulaires hautement sécurisées. -# 1. ARCHITECTURE ET FORMAT DES LIVRABLES : -# Le projet devra obligatoirement être constitué de 3 briques : -# - Le code source applicatif structuré et modulaire respectant les besoins indiqué par le PM Agent. -# - Un fichier 'requirements.txt' listant l'intégralité des dépendances avec leurs versions fixées. -# - Un fichier 'README.md' d'une qualité professionnelle exemplaire. +Votre objectif est de générer l'intégralité du code source d'un projet basé sur les spécifications fournies (ProjectSpec) et d'éventuels retours de l'équipe QA. -# 2. STRUCTURE ET NORMES DU CODE PYTHON : -# - Convention de nommage : Respect strict de la PEP 8. Fonctions, variables et scripts en snake_case (ex: 'file_processor.py'). Classes en PascalCase. -# - Modularité : Pas de script monolithique géant. Séparation claire entre la configuration (chargée via variables d'environnement), la logique métier (fonctions principales) et les connecteurs d'I/O ou d'API. -# - Sécurité : Interdiction formelle d'écrire des clés d'API, des tokens ou des identifiants en dur. Utilisation obligatoire du module 'os' ou de 'pydantic-settings' pour lire l'environnement. -# - Robustesse : Utilisation systématique de blocs 'try/except' ciblés avec un module de 'logging' Python natif (pas de simples 'print'). +--- -# 3. STRUCTURE ATTENDUE DU README.md : -# Le fichier d'accompagnement devra obligatoirement comporter les sections suivantes : -# - # [Titre du Projet] (Rappel de l'objectif macro). -# - ## Prérequis (Version de Python, outils tiers requis). -# - ## Installation (Création de l'environnement virtuel, installation des requirements). -# - ## Configuration (Exemple de fichier .env avec les variables nécessaires à créer). -# - ## Utilisation (Explication textuelle et exemples de commandes CLI pour lancer le script). +### DIRECTIVES D'ARCHITECTURE (ADAPTATIVE) : +Tu dois appliquer STRICTEMENT l'une des deux structures suivantes selon des critères précis : -# --- +1. ARBORESCENCE "SIMPLE" (Pour les scripts uniques, outils CLI mono-fichier ou automatisations courtes) : + - RÈGLE : Tout le code métier tient dans un seul et unique fichier à la racine. Pas de sous-dossiers inutiles. + - À la racine : Le fichier de documentation (ex: README.md), le fichier de gestion des dépendances (ex: requirements.txt, package.json, Cargo.toml), le script unique (point d'entrée), et son fichier de test associé. -# ### PROCESSUS DE TRAVAIL ET SÉCURITÉ DES DONNÉES : -# - Développer à partir des informations fournies par l'Agent PM. -# """ \ No newline at end of file +2. ARBORESCENCE "COMPLEXE" (Obligatoire pour les API REST, applications Web, architectures modulaires ou multi-fichiers) : + - RÈGLE : Dès que le projet nécessite une séparation des responsabilités (ex: modèles, contrôleurs/routes, services) ou est une API, cette structure est MANDATAIRE. + - À la racine : Uniquement la documentation, la configuration globale (.env, .gitignore, etc.), le fichier de gestion des dépendances, et le POINT D'ENTRÉE PRINCIPAL de l'application (ex: main.py, index.js, server.ts, Program.cs). + - En sous-dossiers : + - L'intégralité des modules internes, composants logiques, routes ou couches métiers doit être isolée dans un ou plusieurs sous-dossiers dédiés (ex: `/app`, `/src`, `/src/models`). Aucun autre fichier de code métier que le point d'entrée ne doit se trouver à la racine. + - Les tests unitaires et fonctionnels doivent être isolés dans un répertoire dédié à la racine (ex: `/tests`, `/specs`). + +--- + +### PARADIGMES ET QUALITÉ DE CODE (CLEAN CODE) : + +1. PROGRAMMATION ORIENTÉE OBJET (POO) & MODULARITÉ : + - Tu dois privilégier une approche orientée objet. Utilise des **classes** pour modéliser les entités, les services et la logique métier. + - Favorise l'**encapsulation** (méthodes privées/protégées) pour protéger l'état interne des objets. + - Utilise l'**abstraction** pour définir des interfaces ou des classes de base lorsque la logique le permet, afin de faciliter l'extension. + +2. PRINCIPES DE DESIGN (SOLID & DRY) : + - **Single Responsibility :** Chaque classe ou fonction ne doit avoir qu'une seule responsabilité. + - **DRY (Don't Repeat Yourself) :** Extrais la logique répétitive dans des fonctions ou des classes utilitaires. + - **Découplage :** Évite les dépendances trop fortes entre les modules pour permettre une évolution facile du code. + - Utilise des **Design Patterns** reconnus (Factory, Singleton, Strategy, etc.) si la complexité du projet le justifie. + +--- + +### STANDARDS DE CONCEPTION IMPOSÉS : + +1. GESTION DES CONFIGURATIONS ET DES SÉCURITÉS (CRUCIAL) : + - **Secrets & Données Sensibles :** Interdiction absolue de coder en dur ou de demander à l'utilisateur d'écrire un mot de passe, un token ou une clé API directement dans le code source. Passage OBLIGATOIRE par les variables d'environnement (ex: process.env, os.getenv, ENV[]). + - **Variables Utilisateur :** Toutes les variables modifiables par l'utilisateur (paramètres métiers, compteurs, seuils) doivent être centralisées et regroupées de manière visible au début du point d'entrée principal (`main`) ou dans un fichier de configuration dédié, avec des commentaires explicites. + +2. COUVERTURE DE TESTS REQUISE : + - Tu dois obligatoirement générer un ou plusieurs fichiers de tests unitaires fonctionnels et robustes utilisant le framework natif ou standard du langage choisi. + +3. ROBUSTESSE : + - Respect strict des conventions de nommage et guides de style du langage ciblé (ex: PEP 8 pour Python, standard Ruby, etc.). + - Gestion d'erreurs exhaustive via des blocs try/catch/except ciblés et utilisation d'un module de Logging (pas de sorties consoles brutes ou de 'print' sans contexte). + +--- + +### SÉCURITÉ ET VÉRIFICATION DU FORMAT DE SORTIE : +Tu dois réaliser une auto-vérification stricte de ta structure : **chaque fichier déclaré dans le tableau 'tree' doit obligatoirement posséder son équivalent exact et son contenu complet dans le tableau 'files'**, et inversement. + +> **IMPORTANT :** Il est strictement interdit de générer un code de fallback, incomplet ou un message d'erreur si la ProjectSpec est valide. Tu dois implémenter l'intégralité de la logique métier demandée. + +Réponds UNIQUEMENT avec un objet JSON respectant la structure dynamique suivante. Aucun texte avant, aucun texte après, AUCUN commentaire de type '//' ou '#' à l'intérieur de la structure JSON. + +{ + "spec_title": "Titre du projet basé sur la ProjectSpec", + "tree": [ + "chemin/vers/le/premier_fichier.ext", + "chemin/vers/le/second_fichier.ext" + ], + "files": [ + { + "path": "chemin/vers/le/premier_fichier.ext", + "content": "Contenu complet, réel et fonctionnel du fichier..." + }, + { + "path": "chemin/vers/le/second_fichier.ext", + "content": "Contenu complet, réel et fonctionnel du fichier..." + } + ] +} +""" \ No newline at end of file diff --git a/backend/app/schemas/code_output.py b/backend/app/schemas/code_output.py new file mode 100644 index 0000000..5853e6d --- /dev/null +++ b/backend/app/schemas/code_output.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, Field +from typing import List + +class GeneratedFile(BaseModel): + path: str = Field(description="Chemin relatif du fichier par rapport à la racine, ex: 'app/utils.py'") + content: str = Field(description="Contenu source complet du fichier") + +class ProjectCodeOutput(BaseModel): + tree: List[str] = Field(description="Liste complète des chemins de fichiers générés") + files: List[GeneratedFile] = Field(description="Liste des objets fichiers") + spec_title: str = Field(description="Titre du projet d'origine") \ No newline at end of file diff --git a/backend/chainlit_app.py b/backend/chainlit_app.py index 7eeb008..fb46ad1 100644 --- a/backend/chainlit_app.py +++ b/backend/chainlit_app.py @@ -65,13 +65,18 @@ async def on_message(message: cl.Message): summary+= f"- **Nom du projet** : {spec.get('title')}\n" summary+= f"- **Description** : {spec.get('description')}\n" - summary+= f"- **Actions** : {', '.join(spec.get('requirements', []))}\n" - summary+= f"- **Contraintes** : {', '.join(spec.get('constraints', []))}\n" + summary += "- **Actions** :\n" + summary += "\n".join( + f" - {req}" for req in spec.get("requirements", []) + ) + "\n" + summary += "- **Contraintes** :\n" + summary += "\n".join( + f" - {constraint}" for constraint in spec.get("constraints", []) + ) + "\n" summary+= f"- **Langage** : {spec.get('language')}\n" summary += "\n**Est-ce que cela vous convient ?**" - # Ajout des boutons de décision res = await cl.AskActionMessage( content=summary, actions=[ @@ -90,11 +95,9 @@ async def on_message(message: cl.Message): if res and res.get("name") == "oui": await cl.Message(content="🚀 **Spécifications validées !** Lancement de la génération du code...").send() - # On passe le statut attendu par ton graphe new_state["status"] = "spec_approved" cl.user_session.set("graph_state", new_state) - # On relance immédiatement le workflow pour exécuter la suite (dev, qa...) async with httpx.AsyncClient() as client: response = await client.post( "http://127.0.0.1:8000/api/workflow/run", @@ -109,7 +112,6 @@ async def on_message(message: cl.Message): ).send() else: - # Si "non" (ou si le choix a expiré) new_state["status"] = "spec_incomplete" cl.user_session.set("graph_state", new_state) diff --git a/backend/tests/test_mistral.py b/backend/tests/test_mistral.py new file mode 100644 index 0000000..05688ae --- /dev/null +++ b/backend/tests/test_mistral.py @@ -0,0 +1,22 @@ +import httpx +from langchain_openai import ChatOpenAI +from app.core.config import settings + +sync_client = httpx.Client(verify=False) +async_client = httpx.AsyncClient(verify=False) + +llm = ChatOpenAI( + base_url=settings.llm_base_url, + api_key=settings.llm_api_key, + model=settings.llm_model, + temperature=0.3, + http_client=sync_client, + http_async_client=async_client +) + +try: + print("\n=== Appell LLM... ===") + res = llm.invoke("Dis bonjour en un mot.") + print(f"Réponse du modèle : {res.content}") +except Exception as e: + print(f"\n❌ Erreur détectée : {e}") \ No newline at end of file