diff --git a/README.md b/README.md index f015b62..2668783 100644 --- a/README.md +++ b/README.md @@ -2,78 +2,45 @@ ## Contexte du Projet ARC est une plateforme d'automatisation du développement logiciel basée sur un workflow multi-agents (IA). Le système orchestre plusieurs modèles d'IA spécialisés pour transformer un besoin utilisateur en un code source validé, testé et stocké. -___ -### Étape 0 : Préparation de l'environnement du projet -Tâches : -- Créer backend minimal -- Créer modèle de données simple -- langGraph -___ -### Étape 1 : Analyse du Besoin & Qualification (Agent PM / Business Analyst) -- L'utilisateur entre une demande en langage naturel. -- **Agent 1 (PM)** analyse la demande. Si des informations manquent pour coder, il pose des questions clarificatrices à l'utilisateur jusqu'à obtenir un cahier des charges complet. -- **Vérification BDD :** Avant de coder, le système cherche dans une base de données vectorielle si un projet similaire existe déjà. - - *Si oui :* On propose le lien à l'utilisateur. Si l'utilisateur valide, le workflow s'arrête ici. - - *Si non (ou si l'utilisateur rejette l'existant) :* On passe à l'étape 2. - -Outils : -- LangGraph -> LangGraph est adapté aux workflows multi‑agents avec états, transitions conditionnelles, persistance et human‑in‑the‑loop -- Python -> backend, les agents, les appels LLM, les tests, les embeddings et les intégrations + compatible avec autres tools -- Pydantic AI / Pydantic -> forcer l’Agent PM à produire un cahier des charges structuré -- Chainlit -> adapté aux interfaces conversationnelles - BDD : -- Qdrant -> adapté à la recherche sémantique + stocker les embeddings de projets/scripts -- Snowflake Arctic Embed 2.0 -> modèle d’embedding -- Redis (optionnel) -> cache de recherche ;sessions utilisateur ;état temporaire ;verrouillage d’un workflow ;file d’attente simple -___ -### Étape 2 : Génération de Code (Agent Développeur) -- **Agent 2 (Dev)** reçoit le cahier des charges validé et génère l'arborescence et le code source du projet. - -Outils : -- LangGraph -> LangGraph est adapté aux workflows multi‑agents avec états, transitions conditionnelles, persistance et human‑in‑the‑loop -- Mistral ou Gemma (modèle trop généraliste/léger->tache simple) -> DeepSeek Coder/Qwen2.5 -- vLLM -> meilleur choix qu’Ollama pour une plateforme plus industrialisée. Llama.cpp modèles quantifiés sur CPU ou machines modestes + moins adapté à une plateforme multi‑utilisateur -- Pydantic -___ -### Étape 3 : Tests et Assurance Qualité (Agent QA / Testeur) -- **Agent 3 (QA)** récupère le code de l'Agent 2. Il doit exécuter le code (via une sandbox sécurisée) ou générer/exécuter des tests unitaires pour vérifier la qualité, la sécurité et le fonctionnement. -- **Boucle de correction automatique (Loop 1) :** Si les tests échouent, l'Agent 3 renvoie les erreurs à l'Agent 2 avec les logs. L'Agent 2 corrige et renvoie à l'Agent 3. Cette boucle tourne au maximum 3 fois jusqu'à ce que le code soit "vert". -- **EXTENSION FUTURE** si les tests échouent 3 fois, envoyé à une IA plus puissante. - -Outils : -- Docker -- Ruff -> qualité de code -- Bandit -> sécurité -- Semgrep (optionnel) -> règles de sécurité et qualité plus larges - Boucle correction : -- LangGraph -- Pydantic -___ -### Étape 4 : Livraison & Feedback Utilisateur (Boucle Humaine) -- Une fois le code validé par l'Agent 3, il est présenté à l'utilisateur. -- L'utilisateur teste et valide. - - *Si Validé :* Le projet est sauvegardé dans la base de données (pour la recherche de l'Étape 1) et livré (ex: zip ou dépôt GitHub). - - *Si Refusé :* L'utilisateur indique ce qui ne va pas. Tout le contexte (code actuel + retours) est renvoyé à l'**Étape 1** pour réanalyse, et le cycle recommence. - -Outils : -- Chainlit -- Git -___ -### Étape finale : - -Tâches : -- Tests fonctionnels de bout en bout -- Sécurité minimale -- Documentation finale -- Présentation ## Lancer le projet +Avant de lancer le projet, il est important de créer le fichier .env dans le dossier */backend* et de remplir les éléments suivant : ```bash -chmod +x run_project.sh -./run_project.sh +LLM_BASE_URL= +LLM_API_KEY= +LLM_MODEL= +LLM_MODEL_DEV= +LLM_MODEL_QA= +EMBEDDING_BASE_URL= +EMBEDDING_MODEL= +GITEA_ADMIN_USER= +GITEA_ADMIN_PASSWORD= ``` +Une fois les informations fournies, éxectuer la commande suivante pour lancer le projet +```bash +python .\orchestrator.py +``` + +* L'interface utilisateur conversationnelle se trouve sur l'adresse suivant : http://localhost:8001/ +* Le Gitea se trouve à l'adresse suivante : http://localhost:3000/ +___ + +## Arrêter le projet + +```bash +python .\stop_project.py +``` +___ + +## Visualiser les logs + +```bash +docker compose -p arc-app logs -f +``` +___ + ## Structure du projet ```bash ARC/ @@ -85,15 +52,13 @@ ARC/ │ │ │ └── qa_agent.py │ │ │ │ │ ├── api/ -│ │ │ ├── routes/ -│ │ │ │ ├── health.py -│ │ │ │ └── workflow.py -│ │ │ └── deps.py +│ │ │ └── routes/ +│ │ │ ├── health.py +│ │ │ └── workflow.py │ │ │ │ │ ├── core/ │ │ │ ├── config.py -│ │ │ ├── logging.py -│ │ │ └── security.py +│ │ │ └── logging.py │ │ │ │ │ ├── graph/ # LangGraph │ │ │ ├── state.py @@ -103,10 +68,6 @@ ARC/ │ │ ├── llm/ # appels modèles │ │ │ ├── client.py # wrapper d’appel │ │ │ ├── prompts.py # prompts centralisés -│ │ │ └── providers.py # Gemma/llama.cpp.... -│ │ │ -│ │ ├── models/ # modèles métier / persistance (métadonnées d’un projet/version/statut/lien Git/hash/tags) -│ │ │ └── project.py │ │ │ │ │ ├── repositories/ # accès externes, Qdrant / Redis / stockage │ │ │ ├── qdrant_repository.py @@ -122,8 +83,7 @@ ARC/ │ │ │ ├── api.py │ │ │ ├── spec.py │ │ │ ├── code_output.py -│ │ │ ├── qa_report.py -│ │ │ └── project.py +│ │ │ └── qa_report.py │ │ │ │ │ ├── services/ │ │ │ ├── workflow_service.py @@ -146,7 +106,7 @@ ARC/ │ │ ├── test_qdrant.py │ │ └── test_snowflake.py │ │ -│ ├── .env +│ ├── .env # IMPORTANT élément à remplir │ ├── chainlit_app.py │ ├── chainlit_fr_FR.md │ ├── docker-compose-ai.yml # Conteneurs pour les agents et l'embedding diff --git a/backend/README.md b/backend/README.md index e98e8fb..0673822 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,52 +1,67 @@ # ARC Backend -Backend minimal pour le projet ARC : -- API FastAPI -- orchestration LangGraph -- agents PM / Dev / QA -- interface Chainlit -- intégration future Qdrant / Redis / vLLM +## Organisation du projet -## Installation +### Étape 0 : Préparation de l'environnement du projet +Tâches : +- Créer backend minimal +- Créer modèle de données simple +- langGraph +___ +### Étape 1 : Analyse du Besoin & Qualification (Agent PM / Business Analyst) +- L'utilisateur entre une demande en langage naturel. +- **Agent 1 (PM)** analyse la demande. Si des informations manquent pour coder, il pose des questions clarificatrices à l'utilisateur jusqu'à obtenir un cahier des charges complet. +- **Vérification BDD :** Avant de coder, le système cherche dans une base de données vectorielle si un projet similaire existe déjà. + - *Si oui :* On propose le lien à l'utilisateur. Si l'utilisateur valide, le workflow s'arrête ici. + - *Si non (ou si l'utilisateur rejette l'existant) :* On passe à l'étape 2. -```bash -python -m venv .venv -.venv\Scripts\activate -pip install -r requirements.txt -uvicorn app.main:app --reload --port 8000 -``` +Outils : +- LangGraph -> LangGraph est adapté aux workflows multi‑agents avec états, transitions conditionnelles, persistance et human‑in‑the‑loop +- Python -> backend, les agents, les appels LLM, les tests, les embeddings et les intégrations + compatible avec autres tools +- Pydantic AI / Pydantic -> forcer l’Agent PM à produire un cahier des charges structuré +- Chainlit -> adapté aux interfaces conversationnelles + BDD : +- Qdrant (optionnel) -> adapté à la recherche sémantique + stocker les embeddings de projets/scripts +- Snowflake Arctic Embed 2.0 (optionnel) -> modèle d’embedding +- Redis (optionnel) -> cache de recherche ;sessions utilisateur ;état temporaire ;verrouillage d’un workflow ;file d’attente simple +___ +### Étape 2 : Génération de Code (Agent Développeur) +- **Agent 2 (Dev)** reçoit le cahier des charges validé et génère l'arborescence et le code source du projet. -## Lancer Chainlit +Outils : +- LangGraph -> LangGraph est adapté aux workflows multi‑agents avec états, transitions conditionnelles, persistance et human‑in‑the‑loop +- Mistral ou Gemma (modèle trop généraliste/léger->tache simple) -> DeepSeek Coder/Qwen2.5 +- vLLM -> meilleur choix qu’Ollama pour une plateforme plus industrialisée. Llama.cpp modèles quantifiés sur CPU ou machines modestes + moins adapté à une plateforme multi‑utilisateur +- Pydantic +- Gitea +___ +### Étape 3 : Tests et Assurance Qualité (Agent QA / Testeur) +- **Agent 3 (QA)** récupère le code de l'Agent 2. Il doit exécuter le code (via une sandbox sécurisée) ou générer/exécuter des tests unitaires pour vérifier la qualité, la sécurité et le fonctionnement. +- **Boucle de correction automatique (Loop 1) :** Si les tests échouent, l'Agent 3 renvoie les erreurs à l'Agent 2 avec les logs. L'Agent 2 corrige et renvoie à l'Agent 3. Cette boucle tourne au maximum 3 fois jusqu'à ce que le code soit "vert". +- **EXTENSION FUTURE** si les tests échouent 3 fois, envoyé à une IA plus puissante. -```bash -chainlit run chainlit_app.py --port 8001 -``` +Outils : +- Docker +- Semgrep -> règles de sécurité et qualité plus larges + Boucle correction : +- world model (optionnel) +- LangGraph +- Pydantic +___ +### Étape 4 : Livraison & Feedback Utilisateur (Boucle Humaine) +- Une fois le code validé par l'Agent 3, il est présenté à l'utilisateur. +- L'utilisateur teste et valide. + - *Si Validé :* Le projet est sauvegardé dans la base de données (pour la recherche de l'Étape 1) et livré (ex: zip ou dépôt GitHub). + - *Si Refusé :* L'utilisateur indique ce qui ne va pas. Tout le contexte (code actuel + retours) est renvoyé à l'**Étape 1** pour réanalyse, et le cycle recommence. -## Lancement auto +Outils : +- Chainlit +- Gitea +___ +### Étape finale : -```bash -python orchestrator.py -``` - -## Arrêt propre - -```bash -python stop_project.py -``` - -# Logs - -```bash -docker compose -p arc-app logs -f -docker compose -p arc-ai logs -f -``` - -## Tests - -```bash -python .\tests\test_snowflake.py -docker compose exec app python tests/test_qdrant.py -``` - -API dispo sur : -- http://127.0.0.1:8001 \ No newline at end of file +Tâches : +- Tests fonctionnels de bout en bout +- Sécurité minimale +- Documentation finale +- Présentation \ No newline at end of file diff --git a/backend/app/agents/dev_agent.py b/backend/app/agents/dev_agent.py index 04cc341..9ae104e 100644 --- a/backend/app/agents/dev_agent.py +++ b/backend/app/agents/dev_agent.py @@ -56,7 +56,7 @@ async def run_dev_agent(spec: dict, qa_feedback: list = None, repo_url: str = No 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) + return _generate_fallback_code_output(spec, repo_url=repo_url, files=files) async def _ensure_gitea_repo(base_name: str, repo_url: str = None) -> str: """ @@ -189,18 +189,46 @@ async def _deploy_to_gitea(code_data: ProjectCodeOutput, project_title: str, rep if os.path.exists(work_dir): shutil.rmtree(work_dir) -def _generate_fallback_code_output(spec: dict) -> dict: - """Génère un livrable minimal de secours en cas de crash du LLM.""" +def _generate_fallback_code_output(spec: dict, repo_url: str = None, files: list = None) -> dict: + """ + Génère un livrable de secours. Récupère et reconstruit l'état du code + à partir des fichiers existants s'ils sont fournis, sinon génère un README d'erreur. + """ 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") + fallback_files = [] + fallback_tree = [] + + if files: + for f in files: + path = "" + content = "" + + if isinstance(f, dict): + path = f.get("path") or f.get("filename") or f.get("name") or "" + content = f.get("content") or f.get("code") or "" + elif hasattr(f, "path") and hasattr(f, "content"): + path = getattr(f, "path", "") + content = getattr(f, "content", "") + + if path: + fallback_files.append(GeneratedFile(path=path, content=content)) + fallback_tree.append(path) + + if not fallback_files: + fallback_files = [ + GeneratedFile( + path="README.md", + content=f"# {title}\nErreur interne : le code n'a pas pu être généré par l'agent Dev lors du premier essai." + ) ] + fallback_tree = ["README.md"] + + fallback = ProjectCodeOutput( + repo_url=repo_url, + spec_title=title, + tree=fallback_tree, + files=fallback_files ) 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 4cbc880..0fa6e03 100644 --- a/backend/app/agents/pm_agent.py +++ b/backend/app/agents/pm_agent.py @@ -38,7 +38,7 @@ 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: diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/core/config.py b/backend/app/core/config.py index fee69b4..95d16c2 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -19,11 +19,11 @@ class Settings(BaseSettings): llm_model_dev: str llm_model_qa: str - embedding_base_url: str = "http://localhost:8002/v1" - embedding_model: str = "snowflake-arctic-embed-m-v1.5" + embedding_base_url: str + embedding_model: str - gitea_base_url: str - gitea_api_url: str + gitea_base_url: str = "http://git-arc:3000" + gitea_api_url: str = "http://git-arc:3000/api/v1" gitea_admin_user: str gitea_admin_password: str gitea_token: Optional[str] = None diff --git a/backend/app/core/security.py b/backend/app/core/security.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/llm/prompts.py b/backend/app/llm/prompts.py index 0e9320c..9e45924 100644 --- a/backend/app/llm/prompts.py +++ b/backend/app/llm/prompts.py @@ -1,45 +1,21 @@ -PM_AGENT_PROMPT = """Tu es un Product Manager senior spécialisé en structuration de cahiers des charges. +PM_AGENT_PROMPT = """Tu es un Product Manager senior et un Lead Developer spécialisé en structuration de cahiers des charges techniques. === TÂCHE UNIQUE ET EXCLUSIVE === -Tu reçois une demande utilisateur brute. -Tu DOIS la transformer en JSON valide suivant EXACTEMENT cette structure (ProjectSpec). +Tu reçois une demande utilisateur brute. Tu dois l'analyser et la transformer en un JSON valide suivant EXACTEMENT la structure "ProjectSpec". -⚠️ RÈGLE ABSOLUE : Tu génères UNIQUEMENT du JSON valide. Pas de texte avant, pas de texte après. -Si tu ne comprends pas un champ, tu le remplis avec une valeur par défaut INTELLIGENTE. +⚠️ RÈGLE DE SORTIE : Tu génères UNIQUEMENT du JSON valide. Pas de texte avant, pas de texte après, pas de balises markdown de code block (pas de ```json). +Si tu ne comprends pas un champ ou si l'info est absente mais non critique, remplis-le avec une valeur par défaut INTELLIGENTE. --- -=== STRUCTURE JSON À RETOURNER === - -{ - "title": "...", - "description": "...", - "requirements": ["...", "..."], - "constraints": ["..."], - "language": "...", - "target_stack": "...", - "io_config": { - "has_inputs": true/false, - "input_type": "file|directory|api|database|none", - "input_paths_or_sources": ["..."], - "has_outputs": true/false, - "output_type": "file|directory|database|api_response|log_only", - "output_formats": ["..."] - }, - "auth_config": { - "requires_auth": true/false, - "auth_method": "env_variables|service_account_json|oauth2|api_key|none", - "target_tools_and_apis": ["..."] - }, - "env_config": { - "target_os": "linux|windows|macos|cross_platform", - "python_version": "^3.11", - "critical_dependencies": ["..."] - }, - "error_handling_strategy": "abort_on_error|log_and_continue|retry_policy", - "is_complete": true, - "clarifying_question": null -} +=== GESTION PARTICULIÈRE : CAS DU HORS-SUJET === +Si la demande de l'utilisateur n'est PAS un projet de développement, de script ou d'automatisation informatique (ex: "Comment on fait des bébés ?", "Rédige un poème", "Donne-moi une recette de cuisine") : +1. "title" : "Hors Sujet" +2. "description" : "La demande ne concerne pas un projet de développement ou d'automatisation." +3. "requirements" : ["La demande utilisateur doit être reformulée pour cibler un script ou une tâche d'automatisation."] +4. "is_complete" : false +5. "clarifying_question" : "Désolé, l'outil ARC est conçu exclusivement pour concevoir, automatiser et développer des scripts ou des projets informatiques. Votre demande ne semble pas correspondre à un projet de développement. Comment puis-je vous aider sur un sujet technique ou d'automatisation ?" +6. Remplis tous les autres champs avec des valeurs vides ou par défaut cohérentes. --- @@ -47,253 +23,81 @@ Si tu ne comprends pas un champ, tu le remplis avec une valeur par défaut INTEL **1. title** : - JAMAIS vide. Jamais null. - - Si vague: génère un titre court et clair dérivé du besoin - Format: "Verb + Object" (ex: "CSV Data Cleaner", "API Report Generator") - - Si l'utilisateur dit "teste mon code" → "Code Test Runner" - - Si l'utilisateur dit juste "script" → "Automation Script" + - Si l'utilisateur dit juste "script" -> "Automation Script" **2. description** : - JAMAIS vide. Jamais null. - - 1-2 phrases qui expliquent QUOI et POURQUOI. - - Réutilise les mots clés de la demande pour que ce soit cohérent. - - Ex: "Traite les fichiers CSV mensuels. Filtre les anomalies et génère un rapport Excel." + - 1-2 phrases qui expliquent QUOI et POURQUOI de manière cohérente avec la demande. **3. requirements** : - - JAMAIS vide. JAMAIS une liste vide []. - - MINIMUM 3 items (même générés intelligemment). - - Format: "Action: Détail" ou "Étape N: Décrire l'action" - - Ex: - [ - "Lire les données CSV depuis le dossier /input", - "Valider l'intégrité des colonnes requises", - "Générer un rapport Excel avec statistiques" - ] + - JAMAIS vide. JAMAIS une liste vide []. MINIMUM 3 items détaillés (même déduits intelligemment). + - Format: "Action: Détail" ou "Étape N: Description" **4. constraints** : - - Peux être vide si pas mentionné. - - Sinon: normes de code, limitations, formats (ex: ["PEP 8 strict", "Logs rotatifs"]) + - Peut être vide []. Sinon, normes, formats ou limites (ex: ["PEP 8", "Logs rotatifs"]). **5. language** : - - JAMAIS vide. - - Par défaut: "Python" - - Sauf si l'utilisateur dit "Node.js", "Go", etc. + - Par défaut: "Python" (Sauf si spécifié autrement comme Go, Bash, Node.js). **6. target_stack** : - - Peux être vide - - Sauf si l'utilisateur dit "FastAPI", "Django", etc. + - Framework ou outil spécifique (ex: "FastAPI", "Pandas"). Peut être vide. **7. io_config.has_inputs** : - - TRUE si l'utilisateur mentionne : "fichier", "dossier", "données", "lire", "importer", "télécharger", "API", "base de données", "table" - - FALSE sinon. - - ⚠️ Si TRUE → input_type DOIT être rempli (pas null) + - TRUE si la demande mentionne : lire, importer, fichier, dossier, API, base de données, table, etc. + - FALSE sinon. Si TRUE -> input_type DOIT être rempli (pas "none"). **8. io_config.input_type** : - - DÉDUIS du contexte : - - "fichier CSV/Excel/JSON/PDF/TXT" → "file" - - "dossier" → "directory" - - "API REST/GraphQL/Service Web" → "api" - - "base de données/table SQL/MongoDB" → "database" - - Aucun input → "none" - - ❌ JAMAIS null si has_inputs=true + - Déduis du contexte ("file", "directory", "api", "database"). "none" si pas d'inputs. **9. io_config.has_outputs** : - - TRUE si l'utilisateur mentionne : "exporter", "générer", "créer", "sauvegarder", "envoyer", "résultat", "fichier", "rapport" - - FALSE sinon. + - TRUE si la demande mentionne : générer, exporter, créer, sauvegarder, envoyer, rapport, fichier, etc. **10. io_config.output_type** : - - DÉDUIS du contexte : - - "fichier Excel/CSV/PDF/JSON" → "file" - - "dossier de résultats" → "directory" - - "base de données" → "database" - - "appel API/réponse HTTP" → "api_response" - - "juste des logs/console" → "log_only" - - ❌ JAMAIS null si has_outputs=true + - Déduis du contexte ("file", "directory", "database", "api_response", "log_only"). **11. io_config.output_formats** : - - Formats de sortie mentionnés (ex: ["CSV", "JSON", "XLSX", "PDF"]) - - Peux être vide si has_outputs=false + - Liste de formats (ex: ["CSV", "XLSX"]). Peut être vide []. **12. auth_config.requires_auth** : - - TRUE si l'utilisateur mentionne : "API", "token", "key", "clé", "authentification", "compte", "service account", "OAuth", "login", "credential", "Jira", "SharePoint", "GCP", "AWS", "Azure", "base de données" - - FALSE sinon. + - TRUE si la demande interagit avec un service tiers, une API ou une base de données nécessitant des accès (ex: Jira, Zabbix, SharePoint, DB SQL, API externe, GCP, AWS). **13. auth_config.auth_method** : - - DÉDUIS du contexte : - - "variables d'environnement/.env" → "env_variables" - - "fichier .json avec credentials" → "service_account_json" - - "OAuth/OpenID" → "oauth2" - - "API key/token" → "api_key" - - Pas d'auth → "none" - - ⚠️ Si requires_auth=true → DÉDUIS intelligemment, jamais null + - Déduis intelligemment ("env_variables", "service_account_json", "oauth2", "api_key"). "none" si pas d'authentification. **14. auth_config.target_tools_and_apis** : - - Liste les services externes (ex: ["Jira API", "Google Drive", "AWS S3"]) - - Peux être vide si requires_auth=false + - Services cibles à lister (ex: ["Jira API"]). Peut être vide []. **15. env_config.target_os** : - - "cross_platform" par défaut - - Sauf si mentionné : "linux", "windows", "macos" + - "cross_platform" par défaut (Sauf si linux, windows, macos est spécifié). **16. env_config.language_version** : - - "^3.11" par défaut si Python - - Sauf si la version exacte est mentionnée par l'utilisateur (ex: "Python 3.12", "Node.js 18", "Go 1.20") - - Sinon vide + - "^3.11" par défaut pour Python. Sinon la version mentionnée. **17. env_config.critical_dependencies** : - - DÉDUIS du contexte : - - Données tabulaires → "pandas" - - API REST → "requests" - - Configuration → "pydantic" - - Base de données → "sqlalchemy", "pymongo" - - Fichiers → "openpyxl", "python-docx", "PyPDF2" - - Parallelisation → "asyncio", "concurrent.futures" - - Toujours inclure: ["logging"] (natif mais important) + - Déduis du contexte (ex: "pandas", "requests", "sqlalchemy", "openpyxl"). Inclure toujours: ["logging"]. **18. error_handling_strategy** : - - JAMAIS vide. JAMAIS null. - - "log_and_continue" par défaut (robuste) - - "abort_on_error" si critique - - "retry_policy" si mentionné "retry", "robustesse", "résilience" + - "log_and_continue" par défaut. "abort_on_error" ou "retry_policy" si la tolérance aux pannes est mentionnée. **19. is_complete** : - - TRUE si tu as pu remplir tous les champs principaux (title, description, requirements) sans ambiguïté - - FALSE UNIQUEMENT si quelque chose est vraiment manquant et ambigu + - Définis impérativement à FALSE si : + - La demande est Hors Sujet (voir section GESTION DU HORS-SUJET). + - Le script doit interagir avec un serveur ou un équipement mais il manque l'adresse IP, le nom d'hôte ou le sous-réseau. + - Le script doit requêter un service ou une API (ex: Jira, SharePoint, Zabbix) mais il manque le lien (URL de l'instance/du serveur). + - Le script nécessite une authentification mais le type d'accès (compte de service, clé API, token) ou le compte n'est pas spécifié. + - Les traitements, calculs ou filtres à appliquer sur les données entrantes restent flous ou ambigus. + - Définis à TRUE uniquement si tous ces éléments fonctionnels et d'accès sont clairs et exploitables immédiatement par un développeur. **20. clarifying_question** : - - null si is_complete=true - - Sinon: une question naturelle et polie pour l'utilisateur - - Ex: "Quel format de fichier en entrée (CSV, Excel, JSON) et où souhaitez-vous la sortie ?" + - Doit être "null" si is_complete est TRUE. + - Si is_complete est FALSE : Rédige **UNE SEULE** question polie, claire et ciblée pour débloquer le développement. + - *Exemple de question technique* : "Pourriez-vous me fournir l'adresse IP du serveur cible ainsi que le type de compte de service requis pour l'authentification ?" + - *Exemple de question hors-sujet* : Voir la section "GESTION PARTICULIÈRE : CAS DU HORS-SUJET" ci-dessus. --- -=== EXEMPLES CONCRETS === - -### Input 1: "Je veux un script en Ruby qui lit un CSV et l'exporte en Excel" -```json -{ - "title": "CSV to Excel Converter", - "description": "Lit un fichier CSV, valide les données et exporte le résultat en format Excel (.xlsx).", - "requirements": [ - "Lire le fichier CSV depuis le dossier source", - "Valider les colonnes requises", - "Exporter en fichier Excel avec formatage" - ], - "constraints": ["PEP 8 strict"], - "language": "Ruby", - "target_stack": "", - "io_config": { - "has_inputs": true, - "input_type": "file", - "input_paths_or_sources": ["input.csv"], - "has_outputs": true, - "output_type": "file", - "output_formats": ["XLSX"] - }, - "auth_config": { - "requires_auth": false, - "auth_method": "none", - "target_tools_and_apis": [] - }, - "env_config": { - "target_os": "cross_platform", - "language_version": "3.2.1O", - "critical_dependencies": ["pandas", "openpyxl"] - }, - "error_handling_strategy": "log_and_continue", - "is_complete": true, - "clarifying_question": null -} -``` - -### Input 2: "Je dois récupérer des données sur une API Zabbix et les stocker en Python 3.12" -```json -{ - "title": "API Data Fetcher", - "description": "Récupère les données depuis une API, les valide et les stocke en base de données.", - "requirements": [ - "Appeler l'API avec authentification", - "Parser les données JSON", - "Insérer ou mettre à jour les enregistrements en base de données" - ], - "constraints": [], - "language": "Python", - "target_stack": "Zabbix API", - "io_config": { - "has_inputs": true, - "input_type": "api", - "input_paths_or_sources": ["https://api.example.com/data"], - "has_outputs": true, - "output_type": "database", - "output_formats": ["SQL", "JSON"] - }, - "auth_config": { - "requires_auth": true, - "auth_method": "api_key", - "target_tools_and_apis": ["REST API"] - }, - "env_config": { - "target_os": "cross_platform", - "language_version": "3.12", - "critical_dependencies": ["requests", "sqlalchemy", "pydantic"] - }, - "error_handling_strategy": "retry_policy", - "is_complete": true, - "clarifying_question": null -} -``` ---- - -=== VALIDATION DE COMPLÉTUDE DE LA SPÉCIFICATION (OBLIGATOIRE) === - -Avant de considérer la ProjectSpec comme complète, tu dois te demander : - -"Un développeur senior peut-il implémenter l'intégralité du projet sans devoir faire la moindre hypothèse importante ?" - -Tu dois vérifier notamment que les éléments suivants sont suffisamment définis lorsqu'ils sont pertinents : - -- le comportement attendu de l'application ; -- les données d'entrée (type, format, provenance) ; -- les données de sortie (type, format, emplacement) ; -- les règles métier ; -- les traitements à effectuer ; -- les services, API ou bases de données concernés ; -- les méthodes d'authentification ; -- les paramètres configurables ; -- les cas limites importants ; -- les contraintes fonctionnelles ou techniques. - -Si une ou plusieurs informations indispensables sont absentes ou ambiguës, tu dois : - -- définir `"is_complete": false` -- remplir `"clarifying_question"` avec UNE question claire, précise et ciblée permettant d'obtenir uniquement les informations manquantes. -- ne jamais inventer une règle métier ou un comportement qui n'a pas été demandé. - -Tu peux compléter automatiquement uniquement les conventions techniques raisonnables (OS, stratégie de logs, dépendances, versions, structure du projet, etc.), mais jamais les besoins fonctionnels. - ---- - -=== DERNIER CHECKPOINT === - -Avant de retourner le JSON : -1. ✅ title rempli ? (pas vide, pas null) -2. ✅ description rempli ? (pas vide, pas null) -3. ✅ requirements rempli ? (pas vide, 3+ items) -4. ✅ target_stack rempli ? (pas vide, pas null) -5. ✅ error_handling_strategy rempli ? (pas vide, pas null) -6. ✅ Si has_inputs=true → input_type rempli ? (pas null) -7. ✅ Si has_outputs=true → output_type rempli ? (pas null) -8. ✅ Si requires_auth=true → auth_method rempli ? (pas null, pas "none") - -Si tout est ✅, tu retournes le JSON. -Si un champ fail, tu génères une VALEUR PAR DÉFAUT INTELLIGENTE et marque is_complete=false. - -CRITICAL: Si la demande de l'utilisateur est trop courte, trop vague (ex: 'Je veux un script Ruby'), ou manque d'objectifs clairs, tu DOIS impérativement définir is_complete à false et formuler une question précise dans clarifying_question pour lui demander ce que le script doit faire concrètement. - ---- - -=== MAINTENANT, ANALYSE ET GÉNÈRE LE JSON === - -Ci-dessous la demande utilisateur brute. Tu dois transformer cela en JSON ProjectSpec valide, rempli et cohérent. +=== MAINTENANT, ANALYSE LA DEMANDE CI-DESSOUS ET GÉNÈRE LE JSON === """ diff --git a/backend/app/llm/providers.py b/backend/app/llm/providers.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/models/project.py b/backend/app/models/project.py deleted file mode 100644 index da8a973..0000000 --- a/backend/app/models/project.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import BaseModel -from typing import List, Optional - - -class ProjectRecord(BaseModel): - id: Optional[str] = None - title: str - summary: str - tags: List[str] = [] - repository_url: Optional[str] = None \ No newline at end of file diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py deleted file mode 100644 index 137a39e..0000000 --- a/backend/app/schemas/project.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import BaseModel -from typing import List, Optional - - -class ProjectSummary(BaseModel): - id: Optional[str] = None - title: str - summary: str - tags: List[str] = [] - repository_url: Optional[str] = None \ No newline at end of file diff --git a/backend/chainlit_app.py b/backend/chainlit_app.py index 572ff3c..67926a3 100644 --- a/backend/chainlit_app.py +++ b/backend/chainlit_app.py @@ -1,10 +1,11 @@ import chainlit as cl -import asyncio +import unicodedata import zipfile import httpx import json import os import io +import re # --- CONFIGURATION GITEA --- GITEA_TOKEN = os.getenv("GITEA_TOKEN", "ton_token_gitea_ici") @@ -143,7 +144,7 @@ def inject_conforme_badge(state: dict) -> dict: return state -async def reset_to_start(message_text: str = "Bonjour 👋 Je suis ARC. Décris-moi ton besoin logiciel."): +async def reset_to_start(message_text: str = "Bonjour 👋 Je suis ARC. Décrivez-moi votre besoin logiciel."): """ Réinitialise complètement l'état de la session utilisateur. """ @@ -166,6 +167,25 @@ async def reset_to_start(message_text: str = "Bonjour 👋 Je suis ARC. Décris- await cl.Message(content=message_text).send() +def sanitize_repo_name(name: str) -> str: + """ + Nettoie une chaîne pour la rendre 100% compatible avec un nom de dépôt Git. + Gère les accents, les espaces et supprime les caractères interdits. + """ + if not name: + return "mon-projet" + + normalized = unicodedata.normalize('NFKD', name) + no_accent = normalized.encode('ascii', 'ignore').decode('utf-8') + lowered = no_accent.lower() + spaced_to_hyphen = lowered.replace(" ", "_") + + cleaned = re.sub(r'[^a-z0-9-_]', '', spaced_to_hyphen) + cleaned = re.sub(r'-+', '-', cleaned) + cleaned = re.sub(r'_+', '_', cleaned) + + return cleaned.strip('-_') + async def render_workflow_state(new_state: dict): """ Fonction centrale pour aiguiller l'affichage Chainlit @@ -181,7 +201,7 @@ async def render_workflow_state(new_state: dict): elif current_status == "spec_ready": spec = new_state.get("spec", {}) - summary = "### Éléments importants à retenir de ton projet :\n\n" + summary = "### Éléments importants à retenir de votre projet :\n\n" summary += f"- **Nom du projet** : {spec.get('title')}\n" summary += f"- **Description** : {spec.get('description')}\n" summary += "- **Actions** :\n" @@ -189,10 +209,12 @@ async def render_workflow_state(new_state: dict): summary += "- **Contraintes** :\n" summary += "\n".join(f" - {constraint}" for constraint in spec.get("constraints", [])) + "\n" summary += f"- **Langage** : {spec.get('language')}\n\n" - summary += "**Est-ce que cela vous convient ?**" + summary += "💡 *(Le nom du projet indiqué pourra être modifié par la suite)*" + + await cl.Message(content=summary).send() res = await cl.AskActionMessage( - content=summary, + content="**Est-ce que cela vous convient ?**", actions=[ cl.Action(name="oui", payload={"value": "oui"}, label="Oui, c'est parfait 👍"), cl.Action(name="non", payload={"value": "non"}, label="Non, modifier ❌") @@ -207,6 +229,34 @@ async def render_workflow_state(new_state: dict): return if res and res.get("name") == "oui": + proposed_title = spec.get('title', 'mon_projet') + + name_choice = await cl.AskActionMessage( + content=f"Le nom proposé pour le projet est : **{proposed_title}**.\nSouhaitez-vous le conserver ou le modifier ?", + actions=[ + cl.Action(name="garder_nom", payload={"value": "keep"}, label="Conserver ce nom 🏷️"), + cl.Action(name="modifier_nom", payload={"value": "change"}, label="Choisir un autre nom ✏️") + ], + timeout=3600 + ).send() + + if name_choice is None: + await cl.Message(content="⏰ **Session expirée.** Envoie un message pour reprendre.").send() + return + + if name_choice.get("name") == "modifier_nom": + new_name_res = await cl.AskUserMessage( + content="Saisissez le nouveau nom de votre projet : 👇", + timeout=600 + ).send() + + if new_name_res and new_name_res.get("output"): + raw_title = new_name_res["output"].strip() + custom_title = sanitize_repo_name(raw_title) + spec['title'] = custom_title + new_state['spec'] = spec + await cl.Message(content=f"🏷️ Nom du projet configuré sur : **{custom_title}**").send() + await cl.Message(content="🚀 **Spécifications validées !** Lancement de la génération du code...").send() new_state["status"] = "spec_approved" @@ -216,7 +266,7 @@ async def render_workflow_state(new_state: dict): response = await client.post( "http://127.0.0.1:8000/api/workflow/run", json=new_state, - timeout=600.0 + timeout=1200.0 ) final_state = response.json() cl.user_session.set("graph_state", final_state) @@ -228,7 +278,7 @@ async def render_workflow_state(new_state: dict): cl.user_session.set("graph_state", new_state) await cl.Message( - content="🔄 **Compris.** Qu'est-ce qui ne convient pas ? S'il te plaît, précise les éléments manquants ou à corriger :" + content="🔄 **Compris.** Qu'est-ce qui ne convient pas ? S'il vous plaît, précisez les éléments manquants ou à corriger :" ).send() elif current_status in ["wait_human_review", "approved_by_human"]: @@ -334,7 +384,7 @@ async def render_workflow_state(new_state: dict): cl.user_session.set("graph_state", new_state) async with httpx.AsyncClient() as client: - resp = await client.post("http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=600.0) + resp = await client.post("http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=1200.0) final_state = resp.json() cl.user_session.set("graph_state", final_state) @@ -342,7 +392,7 @@ async def render_workflow_state(new_state: dict): elif res and res.get("name") == "refuser_projet": feedback_user = await cl.AskUserMessage( content="Veuillez décrire les corrections ou les modifications à apporter au projet.", - timeout=600 + timeout=1200 ).send() if feedback_user: @@ -353,7 +403,7 @@ async def render_workflow_state(new_state: dict): await cl.Message(content="🔄 **Feedback transmis.** Prise en compte des modifications...").send() async with httpx.AsyncClient() as client: - resp = await client.post("http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=600.0) + resp = await client.post("http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=1200.0) loop_state = resp.json() cl.user_session.set("graph_state", loop_state) @@ -392,13 +442,13 @@ async def render_workflow_state(new_state: dict): if res.get("name") == "recommencer_projet": reset_msg = ( "🔄 **Ancien projet supprimé avec succès.**\n\n" - "Faisons table rase ! Décris-moi ton besoin logiciel pour repartir sur de nouvelles bases : 👇" + "Faisons table rase ! Décrivez-moi votre besoin logiciel pour repartir sur de nouvelles bases : 👇" ) await reset_to_start(reset_msg) else: reset_msg = ( "❌ **Session fermée et dépôt nettoyé.**\n\n" - "Si tu as un nouveau besoin à soumettre plus tard, envoie simplement un message pour démarrer." + "Si vous avez un nouveau besoin à soumettre plus tard, envoie simplement un message pour démarrer." ) await reset_to_start(reset_msg) @@ -466,7 +516,7 @@ async def on_message(message: cl.Message): response = await client.post( "http://127.0.0.1:8000/api/workflow/run", json=state, - timeout=600.0 + timeout=1200.0 ) new_state = response.json()