fin étape 2 (dev_agent) + automatisation lancement projet/arrêt projet + séparation des docker-compose
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -45,4 +45,10 @@ dist/
|
||||
*.egg-info/
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# Token files
|
||||
.gitea_token
|
||||
|
||||
# Repo temp
|
||||
temp_repos/
|
||||
140
README.md
140
README.md
@@ -67,70 +67,92 @@ Tâches :
|
||||
- Documentation finale
|
||||
- Présentation
|
||||
|
||||
## Lancer le projet
|
||||
|
||||
```bash
|
||||
chmod +x run_project.sh
|
||||
./run_project.sh
|
||||
```
|
||||
|
||||
## Structure du projet
|
||||
```bash
|
||||
backend/
|
||||
│
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── health.py
|
||||
ARC/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── agents/ # Gestion agents IA
|
||||
│ │ │ ├── pm_agent.py
|
||||
│ │ │ ├── dev_agent.py
|
||||
│ │ │ └── qa_agent.py
|
||||
│ │ │
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── routes/
|
||||
│ │ │ │ ├── health.py
|
||||
│ │ │ │ └── workflow.py
|
||||
│ │ │ └── deps.py
|
||||
│ │ │
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── config.py
|
||||
│ │ │ ├── logging.py
|
||||
│ │ │ └── security.py
|
||||
│ │ │
|
||||
│ │ ├── graph/ # LangGraph
|
||||
│ │ │ ├── state.py
|
||||
│ │ │ ├── nodes.py
|
||||
│ │ │ └── workflow.py
|
||||
│ │ └── deps.py
|
||||
│ │ │
|
||||
│ │ ├── 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
|
||||
│ │ │ ├── redis_repository.py
|
||||
│ │ │ └── project_repository.py
|
||||
│ │ │
|
||||
│ │ ├── sandbox/
|
||||
│ │ │ └── docker_runner.py
|
||||
│ │ │
|
||||
│ │ ├── schemas/ # Pydantic
|
||||
│ │ │ ├── api.py
|
||||
│ │ │ ├── spec.py
|
||||
│ │ │ ├── code_output.py
|
||||
│ │ │ └── project.py
|
||||
│ │ │
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── workflow_service.py
|
||||
│ │ │ ├── embedding_service.py
|
||||
│ │ │ ├── retrieval_service.py
|
||||
│ │ │ └── delivery_service.py
|
||||
│ │ │
|
||||
│ │ └─── main.py
|
||||
│ │
|
||||
│ ├── core/
|
||||
│ │ ├── config.py
|
||||
│ │ ├── logging.py
|
||||
│ │ └── security.py
|
||||
│ ├── public/
|
||||
│ │ ├── logo_dark.png
|
||||
│ │ └── logo_light.png
|
||||
│ │
|
||||
│ ├── graph/ # LangGraph
|
||||
│ │ ├── state.py
|
||||
│ │ ├── nodes.py
|
||||
│ │ └── workflow.py
|
||||
│ ├── tests/
|
||||
│ │ ├── test_health.py
|
||||
│ │ ├── test_workflow.py
|
||||
│ │ ├── test_agents.py
|
||||
│ │ ├── test_gemma.py
|
||||
│ │ ├── test_mistral.py
|
||||
│ │ ├── test_qdrant.py
|
||||
│ │ └── test_snowflake.py
|
||||
│ │
|
||||
│ ├── agents/ # Gestion agents IA
|
||||
│ │ ├── pm_agent.py
|
||||
│ │ ├── dev_agent.py
|
||||
│ │ └── qa_agent.py
|
||||
│ │
|
||||
│ ├── schemas/ # Pydantic
|
||||
│ │ ├── api.py
|
||||
│ │ ├── spec.py
|
||||
│ │ └── project.py
|
||||
│ │
|
||||
│ ├── models/ # modèles métier / persistance (métadonnées d’un projet/version/statut/lien Git/hash/tags)
|
||||
│ │ └── project.py
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── workflow_service.py
|
||||
│ │ ├── embedding_service.py
|
||||
│ │ ├── retrieval_service.py
|
||||
│ │ └── delivery_service.py
|
||||
│ │
|
||||
│ ├── repositories/ # accès externes, Qdrant / Redis / stockage
|
||||
│ │ ├── qdrant_repository.py
|
||||
│ │ ├── redis_repository.py
|
||||
│ │ └── project_repository.py
|
||||
│ │
|
||||
│ ├── llm/ # appels modèles
|
||||
│ │ ├── client.py # wrapper d’appel
|
||||
│ │ ├── prompts.py # prompts centralisés
|
||||
│ │ └── providers.py # Gemma/llama.cpp....
|
||||
│ │
|
||||
│ ├── sandbox/
|
||||
│ │ └── docker_runner.py
|
||||
│ │
|
||||
│ ├── main.py
|
||||
│ └── __init__.py
|
||||
│ ├── .env
|
||||
│ ├── chainlit_app.py
|
||||
│ ├── chainlit_fr_FR.md
|
||||
│ ├── docker-compose-ai.yml # Conteneurs pour les agents et l'embedding
|
||||
│ ├── docker-compose-infra.yml # Conteneurs pour les BDD
|
||||
│ ├── docker-compose.yml # Conteneur de l'application
|
||||
│ ├── Dockerfile
|
||||
│ ├── README.md
|
||||
│ ├── requirements.txt
|
||||
│ └── start.sh # Lance les applications
|
||||
│
|
||||
├── chainlit_app.py
|
||||
├── tests/
|
||||
│ ├── test_health.py
|
||||
│ ├── test_workflow.py
|
||||
│ └── test_agents.py
|
||||
│
|
||||
├── .env
|
||||
├── requirements.txt
|
||||
├── Dockerfile
|
||||
└── README.md
|
||||
└── orchestrator.py # Lance le projet
|
||||
```
|
||||
@@ -12,6 +12,7 @@ RUN pip install --no-cache-dir \
|
||||
--trusted-host pypi.python.org \
|
||||
--trusted-host files.pythonhosted.org \
|
||||
-r requirements.txt
|
||||
RUN apt-get update && apt-get install -y git
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -25,7 +25,20 @@ chainlit run chainlit_app.py --port 8001
|
||||
## Lancement auto
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage, HumanMessage
|
||||
from app.core.config import settings
|
||||
@@ -44,14 +47,142 @@ async def run_dev_agent(spec: dict, qa_feedback: list = None) -> dict:
|
||||
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()
|
||||
code_data = await structured_llm.ainvoke(messages)
|
||||
logger.info(f"[Dev Agent] ✅ Code généré ({len(code_data.files)} fichiers)")
|
||||
return await _deploy_to_gitea(code_data, spec.get("title", "project"))
|
||||
|
||||
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)
|
||||
|
||||
async def _ensure_gitea_repo(base_name: str) -> str:
|
||||
"""
|
||||
Vérifie si le repo existe. Si oui, incrémente un suffixe (_1, _2...)
|
||||
jusqu'à trouver un nom libre, puis le crée et retourne ce nom unique.
|
||||
"""
|
||||
async with httpx.AsyncClient(verify=False) as client:
|
||||
headers = {"Authorization": f"token {settings.gitea_token}"}
|
||||
|
||||
repo_name = base_name
|
||||
counter = 1
|
||||
|
||||
# 1. Boucle de recherche d'un nom disponible via l'endpoint direct du repo
|
||||
while True:
|
||||
check_url = f"{settings.gitea_api_url}/repos/{settings.gitea_admin_user}/{repo_name}"
|
||||
response = await client.get(check_url, headers=headers)
|
||||
|
||||
if response.status_code == 404:
|
||||
# Parfait ! Le 404 signifie que ce nom n'existe pas encore, il est libre.
|
||||
break
|
||||
elif response.status_code == 200:
|
||||
# Le nom est déjà pris, on tente avec le compteur suivant
|
||||
repo_name = f"{base_name}_{counter}"
|
||||
counter += 1
|
||||
else:
|
||||
# Sécurité si l'API renvoie autre chose (ex: 401 Unauthorized)
|
||||
raise Exception(f"Erreur Gitea lors de la vérification ({response.status_code}) : {response.text}")
|
||||
|
||||
# 2. Si on arrive ici, repo_name est garanti unique et disponible. On le crée.
|
||||
logger.info(f"[Gitea] Création du nouveau dépôt unique '{repo_name}'...")
|
||||
create_url = f"{settings.gitea_api_url}/user/repos"
|
||||
payload = {
|
||||
"name": repo_name,
|
||||
"auto_init": True,
|
||||
"description": "Generated by AI Dev Agent (Unique Instance)"
|
||||
}
|
||||
create_res = await client.post(create_url, headers=headers, json=payload)
|
||||
|
||||
if create_res.status_code in [201, 200]:
|
||||
logger.info(f"[Gitea] Dépôt '{repo_name}' créé avec succès.")
|
||||
return repo_name
|
||||
else:
|
||||
logger.error(f"[Gitea] Erreur lors de la création : {create_res.text}")
|
||||
raise Exception(f"Impossible de créer le dépôt '{repo_name}' sur Gitea.")
|
||||
|
||||
async def _deploy_to_gitea(code_data: ProjectCodeOutput, project_title: str) -> dict:
|
||||
"""Gère le cycle de vie Git : Clone/Init -> Write -> Commit -> Push."""
|
||||
|
||||
base_name = "".join(c for c in project_title.lower().replace(" ", "_") if c.isalnum() or c == "_")
|
||||
|
||||
# --- MODIFICATION ICI : On récupère le nom unique validé et créé par Gitea ---
|
||||
try:
|
||||
repo_name = await _ensure_gitea_repo(base_name)
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "partial_success_git_failed",
|
||||
"error": str(e),
|
||||
"spec_title": code_data.spec_title,
|
||||
"tree": code_data.tree
|
||||
}
|
||||
|
||||
work_dir = os.path.abspath(f"./temp_repos/{repo_name}")
|
||||
|
||||
if os.path.exists(work_dir):
|
||||
shutil.rmtree(work_dir)
|
||||
os.makedirs(work_dir)
|
||||
|
||||
gitea_host = settings.gitea_base_url.replace("http://", "").replace("https://", "")
|
||||
remote_url = f"http://{settings.gitea_admin_user}:{settings.gitea_token}@{gitea_host}/{settings.gitea_admin_user}/{repo_name}.git"
|
||||
|
||||
try:
|
||||
# 2. Vérifier si on doit Cloner ou faire un Init (Gitea ayant auto_init=True, il va cloner)
|
||||
check_remote = subprocess.run(["git", "ls-remote", remote_url], cwd=work_dir, capture_output=True)
|
||||
|
||||
if check_remote.returncode == 0:
|
||||
logger.info(f"[Git] Dépôt distant initialisé. Clonage de la structure de base...")
|
||||
subprocess.run(["git", "clone", remote_url, "."], cwd=work_dir, check=True, capture_output=True)
|
||||
is_update = True
|
||||
else:
|
||||
logger.info(f"[Git] Initialisation locale...")
|
||||
subprocess.run(["git", "init"], cwd=work_dir, check=True, capture_output=True)
|
||||
subprocess.run(["git", "remote", "add", "origin", remote_url], cwd=work_dir, check=True, capture_output=True)
|
||||
is_update = False
|
||||
|
||||
subprocess.run(["git", "config", "user.name", "ARC Dev Agent"], cwd=work_dir, check=True, capture_output=True)
|
||||
subprocess.run(["git", "config", "user.email", "dev_agent@arc.local"], cwd=work_dir, check=True, capture_output=True)
|
||||
|
||||
# 3. Écriture des fichiers générés par le LLM
|
||||
for file_info in code_data.files:
|
||||
file_path = os.path.join(work_dir, file_info.path)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(file_info.content)
|
||||
|
||||
# 4. Commit et Push
|
||||
subprocess.run(["git", "add", "."], cwd=work_dir, check=True, capture_output=True)
|
||||
|
||||
status = subprocess.run(["git", "status", "--porcelain"], cwd=work_dir, capture_output=True, text=True)
|
||||
if status.stdout.strip():
|
||||
subprocess.run(["git", "commit", "-m", "Update from Dev Agent (AI Generation)"], cwd=work_dir, check=True, capture_output=True)
|
||||
|
||||
if is_update:
|
||||
subprocess.run(["git", "pull", "origin", "main", "--rebase"], cwd=work_dir, check=True, capture_output=True)
|
||||
|
||||
subprocess.run(["git", "push", "origin", "main"], cwd=work_dir, check=True, capture_output=True)
|
||||
logger.info(f"[Git] ✅ Push réussi sur {repo_name}")
|
||||
else:
|
||||
logger.info("[Git] Aucun changement détecté, skip commit/push.")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"repo_url": f"{settings.gitea_base_url}/{settings.gitea_admin_user}/{repo_name}",
|
||||
"files_count": len(code_data.files),
|
||||
"spec_title": code_data.spec_title,
|
||||
"tree": code_data.tree
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Git Deployment Error] {str(e)}")
|
||||
return {
|
||||
"status": "partial_success_git_failed",
|
||||
"error": str(e),
|
||||
"spec_title": code_data.spec_title,
|
||||
"tree": code_data.tree
|
||||
}
|
||||
finally:
|
||||
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."""
|
||||
logger.warning("[Dev Agent] Génération du package de secours (Fallback)")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Request
|
||||
from app.schemas.api import WorkflowRequest, WorkflowResponse
|
||||
from app.services.workflow_service import run_arc_workflow
|
||||
|
||||
@@ -6,7 +6,9 @@ router = APIRouter(tags=["workflow"])
|
||||
|
||||
|
||||
@router.post("/workflow/run", response_model=WorkflowResponse)
|
||||
async def run_workflow(payload: WorkflowRequest):
|
||||
result = await run_arc_workflow(payload.model_dump())
|
||||
async def run_workflow(payload: WorkflowRequest, request: Request):
|
||||
qdrant_repo = request.app.state.qdrant_repo
|
||||
|
||||
result = await run_arc_workflow(payload.model_dump(), qdrant_repo)
|
||||
|
||||
return WorkflowResponse(**result)
|
||||
@@ -1,3 +1,5 @@
|
||||
import http
|
||||
from typing import Optional
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -18,11 +20,19 @@ class Settings(BaseSettings):
|
||||
|
||||
embedding_base_url: str = "http://localhost:8002/v1"
|
||||
embedding_model: str = "snowflake-arctic-embed-m-v1.5"
|
||||
|
||||
gitea_base_url: str
|
||||
gitea_api_url: str
|
||||
gitea_admin_user: str
|
||||
gitea_admin_password: str
|
||||
gitea_token: Optional[str] = None
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore"
|
||||
)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -4,6 +4,7 @@ from app.agents.dev_agent import run_dev_agent
|
||||
from app.agents.qa_agent import run_qa_agent
|
||||
from app.services.retrieval_service import find_existing_project
|
||||
from app.graph.state import WorkflowState
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
async def pm_node(state: WorkflowState):
|
||||
history = state.get("chat_history", []) or []
|
||||
@@ -32,8 +33,13 @@ async def pm_node(state: WorkflowState):
|
||||
"loop_count": 0,
|
||||
}
|
||||
|
||||
async def retrieval_node(state: WorkflowState):
|
||||
existing_project = await find_existing_project(state["user_input"])
|
||||
async def retrieval_node(state: WorkflowState, config: RunnableConfig):
|
||||
qdrant_repo = config.get("configurable", {}).get("qdrant_repo")
|
||||
if not qdrant_repo:
|
||||
raise ValueError("❌ Erreur : Le repository Qdrant n'a pas été transmis au graphe.")
|
||||
|
||||
existing_project = await find_existing_project(qdrant_repo, state["user_input"])
|
||||
|
||||
return {
|
||||
"existing_project": existing_project,
|
||||
"status": "existing_found" if existing_project else "no_existing_project",
|
||||
@@ -69,45 +75,4 @@ async def human_review_node(state: WorkflowState):
|
||||
"existing_project_approved": True,
|
||||
"is_completed": True,
|
||||
"status": "approved_by_human"
|
||||
}
|
||||
|
||||
# def inspect_specifications_gaps(spec_dict: dict) -> list[str]:
|
||||
# missing_fields = []
|
||||
|
||||
# # 1. Vérifications globales (clés alignées sur ProjectSpec)
|
||||
# if not spec_dict.get("title"): missing_fields.append("title")
|
||||
# if not spec_dict.get("description"): missing_fields.append("description")
|
||||
# if not spec_dict.get("requirements"): missing_fields.append("requirements")
|
||||
|
||||
# # 2. Entrées/Sorties
|
||||
# io = spec_dict.get("io_config", {})
|
||||
# if io.get("has_inputs") is True and not io.get("input_type"):
|
||||
# missing_fields.append("io_config.input_type")
|
||||
# if io.get("has_outputs") is True and not io.get("output_type"):
|
||||
# missing_fields.append("io_config.output_type")
|
||||
|
||||
# # 3. Authentification
|
||||
# auth = spec_dict.get("auth_config", {})
|
||||
# if auth.get("requires_auth") is True and not auth.get("auth_method"):
|
||||
# missing_fields.append("auth_config.auth_method")
|
||||
|
||||
# return missing_fields
|
||||
|
||||
# def generate_clarifying_questions_prompt(missing_fields: list[str]) -> str:
|
||||
# # Le mapping utilise désormais les EXACTES mêmes clés
|
||||
# mapping_instructions = {
|
||||
# "title": "- Préciser un titre pour le projet.",
|
||||
# "description": "- Expliquer le but global du script.",
|
||||
# "requirements": "- Lister les fonctionnalités attendues étape par étape.",
|
||||
# "io_config.input_type": "- Préciser le type et le format des fichiers/données d'entrée (CSV, dossier, etc.).",
|
||||
# "io_config.output_type": "- Préciser le type et le format attendus en sortie (Excel, PDF, log, etc.).",
|
||||
# "auth_config.auth_method": "- Clarifier la méthode d'accès ou d'authentification exigée pour l'outil tiers."
|
||||
# }
|
||||
|
||||
# bullet_points = "\n".join([mapping_instructions[field] for field in missing_fields if field in mapping_instructions])
|
||||
|
||||
# return f"""
|
||||
# ATTENTION : Les données suivantes sont obligatoires mais absentes.
|
||||
# Vous devez formuler une question naturelle et polie pour demander à l'utilisateur de préciser :
|
||||
# {bullet_points}
|
||||
# """
|
||||
}
|
||||
@@ -279,16 +279,22 @@ Votre objectif est de générer l'intégralité du code source d'un projet basé
|
||||
|
||||
---
|
||||
|
||||
### RÈGLE ABSOLUE DE TRANSPOSITION DU LANGAGE :
|
||||
Tu dois impérativement identifier le langage cible défini dans la `ProjectSpec` (champ `"language"`). Tu as l'interdiction stricte d'utiliser un autre langage.
|
||||
- **Extensions :** Adapte l'extension du point d'entrée et des tests (.rb pour Ruby, .py pour Python, .js pour JS, .go pour Go, etc.).
|
||||
- **Gestionnaire de dépendances :** Utilise le fichier standard du langage (ex: `Gemfile` pour Ruby, `requirements.txt` ou `pyproject.toml` pour Python, `package.json` pour Node.js).
|
||||
- **Commandes du README :** Les instructions d'installation et d'exécution doivent correspondre EXACTEMENT au langage (ex: `bundle install` et `ruby main.rb` pour Ruby).
|
||||
|
||||
### 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é.
|
||||
- À la racine : Le fichier de documentation (`README.md`), le fichier de dépendances adapté (ex: `Gemfile`, `requirements.txt`), le script unique (ex: `main.rb`, `main.py`), et le dossier de tests (ex: `/tests`, `/spec`).
|
||||
|
||||
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).
|
||||
- À la racine : Documentation, configurations globales, gestionnaire de dépendances, et le POINT D'ENTRÉE PRINCIPAL (ex: `main.rb`, `app.py`, `index.js`).
|
||||
- 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`).
|
||||
@@ -297,12 +303,14 @@ Tu dois appliquer STRICTEMENT l'une des deux structures suivantes selon des crit
|
||||
|
||||
### PARADIGMES ET QUALITÉ DE CODE (CLEAN CODE) :
|
||||
|
||||
1. PROGRAMMATION ORIENTÉE OBJET (POO) & MODULARITÉ :
|
||||
1. ADAPTABILITÉ : Si le projet est un script simple (Arborescence Simple), n'applique pas de Design Patterns complexes ou de POO inutile si le langage favorise une approche procédurale ou fonctionnelle simple. Reste pragmatique.
|
||||
|
||||
2. 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) :
|
||||
3. 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.
|
||||
@@ -323,6 +331,14 @@ Tu dois appliquer STRICTEMENT l'une des deux structures suivantes selon des crit
|
||||
- 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).
|
||||
|
||||
4. DOCUMENTATION POUR LA QA (OBLIGATOIRE) :
|
||||
- Tu dois obligatoirement générer un fichier 'README.md' à la racine.
|
||||
- Ce fichier doit contenir :
|
||||
- Une description claire du projet.
|
||||
- Les instructions d'installation (ex: pip install -r requirements.txt).
|
||||
- Les instructions de lancement (ex: python main.py).
|
||||
- Une section "TESTING" expliquant précisément comment la QA peut vérifier que le code fonctionne (ex: commandes de tests, scénarios de test attendus).
|
||||
|
||||
---
|
||||
|
||||
### SÉCURITÉ ET VÉRIFICATION DU FORMAT DE SORTIE :
|
||||
|
||||
@@ -10,12 +10,17 @@ setup_logging()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
print("[Startup] Initialisation automatique de Qdrant dans Docker...")
|
||||
print("[Startup] Initialisation de Qdrant...")
|
||||
# On crée l'instance UNE SEULE FOIS
|
||||
qdrant_repo = QdrantRepository()
|
||||
try:
|
||||
await qdrant_repo.init_collection(vector_size=1024)
|
||||
await qdrant_repo.init_collection(vector_size=1024)
|
||||
app.state.qdrant_repo = qdrant_repo
|
||||
print("[Startup] Qdrant prêt et attaché à l'application.")
|
||||
except Exception as e:
|
||||
print(f"[Startup] Erreur lors de l'initialisation de Qdrant : {e}")
|
||||
print(f"[Startup] Erreur fatale lors de l'initialisation de Qdrant : {e}")
|
||||
raise e
|
||||
|
||||
yield
|
||||
|
||||
print("[Shutdown] Fermeture propre de la connexion Qdrant...")
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from app.repositories.qdrant_repository import QdrantRepository
|
||||
from app.services.embedding_service import build_embedding
|
||||
|
||||
|
||||
qdrant_repository = QdrantRepository()
|
||||
|
||||
|
||||
async def find_existing_project(user_input: str):
|
||||
async def find_existing_project(qdrant_repo: QdrantRepository, user_input: str):
|
||||
# query_vector = await build_embedding.get_mesh_embedding(user_input)
|
||||
dummy_vector = [0.0] * 1024 # A modifier avec un vrai embedding plus tard TODO
|
||||
return await qdrant_repository.search_similar_project(query_vector=dummy_vector)
|
||||
dummy_vector = [0.0] * 1024
|
||||
return await qdrant_repo.search_similar_project(query_vector=dummy_vector)
|
||||
@@ -1,11 +1,12 @@
|
||||
from app.graph.workflow import compiled_graph
|
||||
from app.repositories.qdrant_repository import QdrantRepository
|
||||
|
||||
async def run_arc_workflow(state_data: dict) -> dict:
|
||||
async def run_arc_workflow(state_data: dict, qdrant_repo: QdrantRepository) -> dict:
|
||||
"""
|
||||
Prend le state actuel (provenant de l'API/Chainlit),
|
||||
exécute le graphe jusqu'au prochain point d'arrêt (END),
|
||||
et retourne le state mis à jour.
|
||||
"""
|
||||
final_state = await compiled_graph.ainvoke(state_data)
|
||||
final_state = await compiled_graph.ainvoke(state_data, config={"configurable": {"qdrant_repo": qdrant_repo}})
|
||||
|
||||
return dict(final_state)
|
||||
61
backend/docker-compose-ai.yml
Normal file
61
backend/docker-compose-ai.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
services:
|
||||
download-model:
|
||||
image: alpine:latest
|
||||
container_name: download-embedding-model
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
command: >
|
||||
sh -c "if [ ! -f /models/snowflake-arctic-embed-m-v1.5-f16.gguf ]; then
|
||||
wget --no-check-certificate 'https://huggingface.co/Snowflake/snowflake-arctic-embed-m-v1.5/resolve/main/gguf/snowflake-arctic-embed-m-v1.5-f16.gguf' -O /models/snowflake-arctic-embed-m-v1.5-f16.gguf;
|
||||
fi"
|
||||
networks:
|
||||
- arc-network
|
||||
|
||||
embedding-server:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
container_name: embedding-arc
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
ports:
|
||||
- "8002:8080"
|
||||
command: "-m /models/snowflake-arctic-embed-m-v1.5-f16.gguf --embedding --host 0.0.0.0 --port 8080"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- arc-network
|
||||
depends_on:
|
||||
download-model:
|
||||
condition: service_completed_successfully
|
||||
|
||||
download-gemma:
|
||||
image: alpine:latest
|
||||
container_name: download-gemma-model
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
command: >
|
||||
sh -c "if [ ! -f /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf ]; then
|
||||
wget --no-check-certificate 'https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-UD-Q4_K_XL.gguf' -O /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf;
|
||||
fi"
|
||||
networks:
|
||||
- arc-network
|
||||
|
||||
gemma-server:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
container_name: gemma-arc
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
ports:
|
||||
- "8003:8080"
|
||||
command: "-m /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf --host 0.0.0.0 --port 8080 -c 8192"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- arc-network
|
||||
depends_on:
|
||||
download-gemma:
|
||||
condition: service_completed_successfully
|
||||
|
||||
volumes:
|
||||
model_storage:
|
||||
|
||||
networks:
|
||||
arc-network:
|
||||
external: true
|
||||
37
backend/docker-compose-infra.yml
Normal file
37
backend/docker-compose-infra.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
services:
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
container_name: qdrant-arc
|
||||
ports:
|
||||
- "6333:6333"
|
||||
- "6334:6334"
|
||||
environment:
|
||||
- QDRANT__TELEMETRY_DISABLED=true
|
||||
volumes:
|
||||
- qdrant_storage:/qdrant/storage
|
||||
networks:
|
||||
- arc-network
|
||||
|
||||
git-server:
|
||||
image: gitea/gitea:latest
|
||||
container_name: git-arc
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- arc-network
|
||||
volumes:
|
||||
- gitea_data:/data
|
||||
environment:
|
||||
- GITEA__security__INSTALL_LOCK=true
|
||||
- GITEA__database__DB_TYPE=sqlite3
|
||||
- GITEA__database__PATH=/data/gitea/gitea.db
|
||||
- GITEA__server__ROOT_URL=http://localhost:3000/
|
||||
- GITEA__server__SSH_PORT=22
|
||||
|
||||
volumes:
|
||||
qdrant_storage:
|
||||
gitea_data:
|
||||
|
||||
networks:
|
||||
arc-network:
|
||||
external: true
|
||||
@@ -1,82 +1,8 @@
|
||||
services:
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
container_name: qdrant-arc
|
||||
ports:
|
||||
- "6333:6333"
|
||||
- "6334:6334"
|
||||
environment:
|
||||
- QDRANT__TELEMETRY_DISABLED=true
|
||||
volumes:
|
||||
- qdrant_storage:/qdrant/storage
|
||||
networks:
|
||||
- arc-network
|
||||
|
||||
download-model:
|
||||
image: alpine:latest
|
||||
container_name: download-embedding-model
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
command: >
|
||||
sh -c "
|
||||
if [ ! -f /models/snowflake-arctic-embed-m-v1.5-f16.gguf ]; then
|
||||
echo 'Téléchargement du modèle (Contournement SSL Proxy activé)...';
|
||||
wget --no-check-certificate 'https://huggingface.co/Snowflake/snowflake-arctic-embed-m-v1.5/resolve/main/gguf/snowflake-arctic-embed-m-v1.5-f16.gguf' -O /models/snowflake-arctic-embed-m-v1.5-f16.gguf;
|
||||
echo 'Téléchargement terminé avec succès !';
|
||||
else
|
||||
echo 'Le modèle est déjà présent.';
|
||||
fi
|
||||
"
|
||||
|
||||
embedding-server:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
container_name: embedding-arc
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
ports:
|
||||
- "8002:8080"
|
||||
command: "-m /models/snowflake-arctic-embed-m-v1.5-f16.gguf --embedding --host 0.0.0.0 --port 8080"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- arc-network
|
||||
depends_on:
|
||||
download-model:
|
||||
condition: service_completed_successfully
|
||||
|
||||
download-gemma:
|
||||
image: alpine:latest
|
||||
container_name: download-gemma-model
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
command: >
|
||||
sh -c "
|
||||
if [ ! -f /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf ]; then
|
||||
echo 'Téléchargement de Gemma 4 (Contournement SSL Proxy)...';
|
||||
wget --no-check-certificate 'https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-UD-Q4_K_XL.gguf' -O /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf;
|
||||
echo 'Téléchargement de Gemma 4 terminé !';
|
||||
else
|
||||
echo 'Le modèle Gemma 4 est déjà présent.';
|
||||
fi
|
||||
"
|
||||
|
||||
gemma-server:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
container_name: gemma-arc
|
||||
volumes:
|
||||
- model_storage:/models
|
||||
ports:
|
||||
- "8003:8080"
|
||||
command: "-m /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf --host 0.0.0.0 --port 8080 -c 8192"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- arc-network
|
||||
depends_on:
|
||||
download-gemma:
|
||||
condition: service_completed_successfully
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: arc-app
|
||||
image: arc-app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8001:8001"
|
||||
@@ -84,19 +10,14 @@ services:
|
||||
- .:/workspace
|
||||
environment:
|
||||
- PYTHONPATH=/workspace
|
||||
- QDRANT_URL=http://qdrant:6333
|
||||
- QDRANT_URL=http://qdrant-arc:6333
|
||||
- QDRANT_COLLECTION=arc_projects
|
||||
- EMBEDDING_SERVER_URL=http://embedding-server:8080
|
||||
depends_on:
|
||||
- qdrant
|
||||
- embedding-server
|
||||
- EMBEDDING_SERVER_URL=http://embedding-arc:8080
|
||||
- GITEA_API_URL=http://git-arc:3000/api/v1
|
||||
- GITEA_TOKEN=${GITEA_TOKEN}
|
||||
networks:
|
||||
- arc-network
|
||||
|
||||
volumes:
|
||||
qdrant_storage:
|
||||
model_storage:
|
||||
|
||||
networks:
|
||||
arc-network:
|
||||
driver: bridge
|
||||
external: true
|
||||
184
orchestrator.py
Normal file
184
orchestrator.py
Normal file
@@ -0,0 +1,184 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import subprocess
|
||||
import httpx
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
backend_dir = os.path.join(current_dir, "backend")
|
||||
|
||||
if not os.path.exists(backend_dir):
|
||||
print(f"❌ Erreur : Le dossier '{backend_dir}' est introuvable.")
|
||||
sys.exit(1)
|
||||
os.chdir(backend_dir)
|
||||
sys.path.append(".")
|
||||
try:
|
||||
from app.core.config import settings
|
||||
except ImportError as e:
|
||||
print(f"❌ Erreur d'importation des settings : {e}")
|
||||
print(f"Vérifie que ton dossier 'app' est bien dans '{backend_dir}'")
|
||||
sys.exit(1)
|
||||
|
||||
COMPOSE_DIR = "."
|
||||
NETWORK_NAME = "arc-network"
|
||||
TOKEN_FILE = ".gitea_token"
|
||||
PROJECTS = {
|
||||
"infra": "arc-infra",
|
||||
"ai": "arc-ai",
|
||||
"app": "arc-app"
|
||||
}
|
||||
|
||||
def run_command(command, cwd=None):
|
||||
"""Lance une commande système de manière synchrone."""
|
||||
print(f"🚀 Exécution : {' '.join(command) if isinstance(command, list) else command}")
|
||||
try:
|
||||
subprocess.run(command, cwd=cwd, check=True, capture_output=False)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Erreur : {e}")
|
||||
return False
|
||||
|
||||
async def wait_for_gitea(local_api_url):
|
||||
"""Attend que l'API HTTP de Gitea soit pleinement initialisée."""
|
||||
public_ping_url = f"{local_api_url}/version"
|
||||
max_retries = 20
|
||||
print("⏳ Attente du démarrage complet de Gitea et des migrations de la base de données...")
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
async with httpx.AsyncClient(verify=False) as client:
|
||||
resp = await client.get(public_ping_url)
|
||||
if resp.status_code == 200:
|
||||
print("✅ Gitea est prêt et opérationnel.")
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
if i < max_retries - 1:
|
||||
await asyncio.sleep(3)
|
||||
raise Exception(f"Gitea n'a pas répondu sur {public_ping_url} après plusieurs tentatives.")
|
||||
|
||||
async def bootstrap_gitea_agent(settings_obj, local_api_url):
|
||||
"""Crée un token d'accès d'application pour l'admin."""
|
||||
if os.path.exists(TOKEN_FILE):
|
||||
with open(TOKEN_FILE, "r") as f:
|
||||
token = f.read().strip()
|
||||
if token:
|
||||
return token
|
||||
|
||||
token_url = f"{local_api_url}/users/{settings_obj.gitea_admin_user}/tokens"
|
||||
|
||||
async with httpx.AsyncClient(verify=False) as client:
|
||||
resp = await client.post(
|
||||
token_url,
|
||||
auth=(settings_obj.gitea_admin_user, settings_obj.gitea_admin_password),
|
||||
json={
|
||||
"name": "dev-agent-token",
|
||||
"scopes": ["all"]
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code in [200, 201]:
|
||||
token_data = resp.json()
|
||||
token = token_data.get('token') or token_data.get('sha1')
|
||||
if not token:
|
||||
raise Exception(f"Format de réponse inconnu pour le token : {token_data}")
|
||||
with open(TOKEN_FILE, "w") as f:
|
||||
f.write(token)
|
||||
return token
|
||||
elif resp.status_code in [400, 409]:
|
||||
print("⚠️ Le token existe déjà dans Gitea. Réinitialisation...")
|
||||
subprocess.run([
|
||||
"docker", "exec", "-u", "git", "git-arc",
|
||||
"gitea", "admin", "token", "delete",
|
||||
"--username", settings_obj.gitea_admin_user,
|
||||
"--name", "dev-agent-token"
|
||||
], capture_output=True)
|
||||
return await bootstrap_gitea_agent(settings_obj, local_api_url)
|
||||
else:
|
||||
raise Exception(f"Échec création token : {resp.status_code} - {resp.text}")
|
||||
|
||||
async def main():
|
||||
# 1. Réseau
|
||||
print("--- ÉTAPE 1 : RÉSEAU ---")
|
||||
create_net = subprocess.run(
|
||||
["docker", "network", "create", NETWORK_NAME],
|
||||
capture_output=True, text=True, shell=(os.name == 'nt')
|
||||
)
|
||||
if create_net.returncode == 0:
|
||||
print(f"✅ Réseau '{NETWORK_NAME}' créé.")
|
||||
else:
|
||||
print(f"✅ Réseau '{NETWORK_NAME}' déjà présent ou géré.")
|
||||
|
||||
# 2. Infrastructure
|
||||
print(f"\n--- ÉTAPE 2 : INFRASTRUCTURE (Projet: {PROJECTS['infra']}) ---")
|
||||
infra_cmd = [
|
||||
"docker", "compose",
|
||||
"-p", PROJECTS["infra"],
|
||||
"-f", f"{COMPOSE_DIR}/docker-compose-infra.yml",
|
||||
"up", "-d"
|
||||
]
|
||||
if not run_command(infra_cmd):
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Provisioning Gitea
|
||||
print("\n--- ÉTAPE 3 : PROVISIONING GITEA ---")
|
||||
|
||||
local_api_url = settings.gitea_api_url.replace("git-arc", "localhost")
|
||||
|
||||
# Étape critique : on attend d'abord que le service web et la DB soient OK
|
||||
try:
|
||||
await wait_for_gitea(local_api_url)
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur critique de démarrage : {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("👤 Création / Vérification du compte administrateur Gitea...")
|
||||
result = subprocess.run([
|
||||
"docker", "exec", "-u", "git", "git-arc",
|
||||
"gitea", "admin", "user", "create",
|
||||
"--username", settings.gitea_admin_user,
|
||||
"--password", settings.gitea_admin_password,
|
||||
"--email", "admin@arc.local",
|
||||
"--admin"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("👤 Compte administrateur créé avec succès.")
|
||||
elif "user already exists" in result.stderr:
|
||||
print("👤 Compte administrateur existant et valide.")
|
||||
else:
|
||||
print(f"⚠️ Note Gitea CLI : {result.stderr.strip()}")
|
||||
|
||||
try:
|
||||
agent_token = await bootstrap_gitea_agent(settings, local_api_url)
|
||||
os.environ["GITEA_TOKEN"] = agent_token
|
||||
print("✅ Provisioning Gitea réussi, token généré.")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur critique de provisioning : {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 4. IA
|
||||
print(f"\n--- ÉTAPE 4 : IA (Projet: {PROJECTS['ai']}) ---")
|
||||
ai_cmd = [
|
||||
"docker", "compose",
|
||||
"-p", PROJECTS["ai"],
|
||||
"-f", f"{COMPOSE_DIR}/docker-compose-ai.yml",
|
||||
"up", "-d"
|
||||
]
|
||||
if not run_command(ai_cmd):
|
||||
sys.exit(1)
|
||||
|
||||
# 5. Application
|
||||
print(f"\n--- ÉTAPE 5 : APPLICATION (Projet: {PROJECTS['app']}) ---")
|
||||
app_cmd = [
|
||||
"docker", "compose",
|
||||
"-p", PROJECTS["app"],
|
||||
"-f", f"{COMPOSE_DIR}/docker-compose.yml",
|
||||
"up", "-d", "--build"
|
||||
]
|
||||
run_command(app_cmd)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Arrêt demandé par l'utilisateur.")
|
||||
50
stop_project.py
Normal file
50
stop_project.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
|
||||
COMPOSE_DIR = "backend"
|
||||
NETWORK_NAME = "arc-network"
|
||||
|
||||
PROJECTS = {
|
||||
"infra": "arc-infra",
|
||||
"ai": "arc-ai",
|
||||
"app": "arc-app"
|
||||
}
|
||||
|
||||
def run_command(command):
|
||||
"""Exécute une commande système."""
|
||||
print(f"🚀 Exécution : {' '.join(command) if isinstance(command, list) else command}")
|
||||
is_windows = os.name == 'nt'
|
||||
try:
|
||||
subprocess.run(command, shell=is_windows, check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("🛑 Démarrage de l'arrêt complet du projet...")
|
||||
|
||||
# 1. Arrêter l'Application
|
||||
print(f"\n--- ARRÊT DE L'APPLICATION ({PROJECTS['app']}) ---")
|
||||
run_command(["docker", "compose", "-p", PROJECTS["app"], "-f", f"{COMPOSE_DIR}/docker-compose.yml", "down"])
|
||||
|
||||
# 2. Arrêter l'IA
|
||||
print(f"\n--- ARRÊT DE L'IA ({PROJECTS['ai']}) ---")
|
||||
run_command(["docker", "compose", "-p", PROJECTS["ai"], "-f", f"{COMPOSE_DIR}/docker-compose-ai.yml", "down"])
|
||||
|
||||
# 3. Arrêter l'Infrastructure
|
||||
print(f"\n--- ARRÊT DE L'INFRASTRUCTURE ({PROJECTS['infra']}) ---")
|
||||
run_command(["docker", "compose", "-p", PROJECTS["infra"], "-f", f"{COMPOSE_DIR}/docker-compose-infra.yml", "down"])
|
||||
|
||||
# 4. Optionnel : Supprimer le réseau
|
||||
print(f"\n--- NETTOYAGE DU RÉSEAU ---")
|
||||
# On essaie de supprimer le réseau, si il est utilisé par d'autres conteneurs, Docker ne le supprimera pas.
|
||||
run_command(["docker", "network", "rm", NETWORK_NAME])
|
||||
|
||||
print("\n✅ Tout est arrêté proprement.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Arrêt forcé par l'utilisateur.")
|
||||
Reference in New Issue
Block a user