Etape 3 + refonte visuelle + corrections etape 2

This commit is contained in:
Chevallier
2026-07-07 09:57:21 +02:00
parent 17e887b268
commit 7b1a975f06
48 changed files with 6551 additions and 59 deletions

View File

@@ -16,10 +16,11 @@ logger = logging.getLogger(__name__)
sync_client = httpx.Client(verify=False)
async_client = httpx.AsyncClient(verify=False)
async def run_dev_agent(spec: dict, qa_feedback: list = None) -> dict:
async def run_dev_agent(spec: dict, qa_feedback: list = None, repo_url: str = None, files: list = None) -> dict:
"""
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.
S'appuie sur repo_url si on est dans une boucle de correction.
"""
logger.info(f"[Dev Agent] Début de la génération pour : {spec.get('title', 'Sans titre')}")
@@ -40,6 +41,8 @@ async def run_dev_agent(spec: dict, qa_feedback: list = None) -> dict:
]
user_content = f"CAHIER DES CHARGES (ProjectSpec) :\n{json.dumps(spec, indent=2, ensure_ascii=False)}\n\n"
if files:
user_content += f"💻 CODE ACTUEL (Version précédente à corriger) :\n{json.dumps(files, 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"
@@ -49,17 +52,22 @@ async def run_dev_agent(spec: dict, qa_feedback: list = None) -> dict:
try:
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"))
return await _deploy_to_gitea(code_data, spec.get("title", "project"), repo_url=repo_url)
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:
async def _ensure_gitea_repo(base_name: str, repo_url: str = None) -> 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.
"""
if repo_url:
repo_name = repo_url.rstrip("/").split("/")[-1]
logger.info(f"[Gitea] Mode correction : réutilisation du dépôt existant '{repo_name}'")
return repo_name
async with httpx.AsyncClient(verify=False) as client:
headers = {"Authorization": f"token {settings.gitea_token}"}
@@ -72,14 +80,11 @@ async def _ensure_gitea_repo(base_name: str) -> str:
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.
@@ -99,24 +104,24 @@ async def _ensure_gitea_repo(base_name: str) -> str:
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:
async def _deploy_to_gitea(code_data: ProjectCodeOutput, project_title: str, repo_url: str = None) -> 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 == "_")
files_list = [f.model_dump() for f in code_data.files]
# --- MODIFICATION ICI : On récupère le nom unique validé et créé par Gitea ---
try:
repo_name = await _ensure_gitea_repo(base_name)
repo_name = await _ensure_gitea_repo(base_name, repo_url=repo_url)
except Exception as e:
return {
"status": "partial_success_git_failed",
"error": str(e),
"spec_title": code_data.spec_title,
"tree": code_data.tree
"tree": code_data.tree,
"files": files_list
}
work_dir = os.path.abspath(f"./temp_repos/{repo_name}")
if os.path.exists(work_dir):
shutil.rmtree(work_dir)
os.makedirs(work_dir)
@@ -150,7 +155,6 @@ async def _deploy_to_gitea(code_data: ProjectCodeOutput, project_title: str) ->
# 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)
@@ -168,7 +172,8 @@ async def _deploy_to_gitea(code_data: ProjectCodeOutput, project_title: str) ->
"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
"tree": code_data.tree,
"files": files_list
}
except Exception as e:
@@ -177,7 +182,8 @@ async def _deploy_to_gitea(code_data: ProjectCodeOutput, project_title: str) ->
"status": "partial_success_git_failed",
"error": str(e),
"spec_title": code_data.spec_title,
"tree": code_data.tree
"tree": code_data.tree,
"files": files_list
}
finally:
if os.path.exists(work_dir):