Etape 3 + refonte visuelle + corrections etape 2
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -1,10 +1,101 @@
|
||||
async def run_qa_agent(generated_code: dict) -> dict:
|
||||
import logging
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage, HumanMessage
|
||||
import httpx
|
||||
from app.core.config import settings
|
||||
from app.schemas.qa_report import QAEvaluation
|
||||
from app.llm.prompts import QA_AGENT_PROMPT
|
||||
from app.sandbox.qa_client import run_project_qa
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sync_client = httpx.Client(verify=False)
|
||||
async_client = httpx.AsyncClient(verify=False)
|
||||
|
||||
async def run_qa_agent(project_title: str, dev_output: dict) -> QAEvaluation:
|
||||
"""
|
||||
Agent QA minimal :
|
||||
- renvoie un statut de validation simulé
|
||||
Agent QA :
|
||||
Analyse le code généré à l'aide de l'API externe de la Sandbox (Semgrep uniquement)
|
||||
et produit le rapport d'évaluation structuré final.
|
||||
"""
|
||||
return {
|
||||
"status": "passed",
|
||||
"logs": [],
|
||||
"checked_files": generated_code.get("tree", []),
|
||||
}
|
||||
logger.info(f"[QA Agent] Début du processus d'audit statique pour le projet : {project_title}")
|
||||
|
||||
repo_url = dev_output.get("repo_url")
|
||||
if not repo_url:
|
||||
logger.error("[QA Agent] Aucun URL de dépôt Git trouvé dans le dev_output.")
|
||||
return QAEvaluation(
|
||||
is_complete_and_safe=False,
|
||||
global_summary="Impossible de récupérer le code source (URL Git manquante).",
|
||||
technical_feedback=["L'agent de développement n'a pas fourni d'URL pour le dépôt Gitea."]
|
||||
)
|
||||
|
||||
llm = ChatOpenAI(
|
||||
base_url=settings.llm_base_url,
|
||||
api_key=settings.llm_api_key,
|
||||
model=settings.llm_model_qa,
|
||||
temperature=0.1,
|
||||
max_retries=2,
|
||||
http_client=sync_client,
|
||||
http_async_client=async_client,
|
||||
)
|
||||
|
||||
# 2. Exécution de l'analyse statique via le service Sandbox indépendant
|
||||
try:
|
||||
repo_name = repo_url.rstrip("/").split("/")[-1]
|
||||
logger.info(f"[QA Agent] Envoi du dépôt '{repo_name}' au conteneur d'analyse statique...")
|
||||
|
||||
raw_results = await run_project_qa(repo_name=repo_name)
|
||||
|
||||
logger.info(f"[QA Agent] 📊 Retour Sandbox Statis - Status : {'Succès' if raw_results.is_executable else 'Échec'}")
|
||||
logger.info(f"[QA Agent] 📊 Retour Sandbox - Nombre d'alertes de sécurité/qualité (Semgrep): {len(raw_results.issues)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[QA Agent] ❌ Échec de la communication avec le conteneur de Scan : {str(e)}")
|
||||
return QAEvaluation(
|
||||
is_complete_and_safe=False,
|
||||
global_summary="Échec critique lors du traitement du rapport de scan statique.",
|
||||
technical_feedback=[f"L'API de la sandbox a échoué : {type(e).__name__}: {str(e)}"]
|
||||
)
|
||||
|
||||
# 3. Génération de l'évaluation finale via LLM
|
||||
structured_llm = llm.with_structured_output(QAEvaluation, strict=True)
|
||||
|
||||
messages = [
|
||||
SystemMessage(content=QA_AGENT_PROMPT),
|
||||
]
|
||||
|
||||
# Le prompt est nettoyé des mentions relatives à l'exécution de tests dynamiques
|
||||
user_content = f"PROJET À AUDITER : {project_title}\n"
|
||||
user_content += f"LIEN DU DÉPÔT : {repo_url}\n"
|
||||
user_content += f"RÉSULTATS DE L'ANALYSE STATIQUE DU CODE (Semgrep) :\n"
|
||||
user_content += f"{raw_results.model_dump_json(indent=2)}\n\n"
|
||||
user_content += "Analyse ces failles, vulnérabilités et défauts de qualité de code, puis génère ton évaluation de sécurité au format structuré demandé."
|
||||
|
||||
messages.append(HumanMessage(content=user_content))
|
||||
|
||||
try:
|
||||
qa_evaluation = await structured_llm.ainvoke(messages)
|
||||
logger.info(f"[QA Agent] 📝 Justification du LLM : {qa_evaluation.global_summary}")
|
||||
|
||||
if not qa_evaluation:
|
||||
logger.error("[QA Agent] Le LLM a renvoyé une réponse vide (None).")
|
||||
return QAEvaluation(
|
||||
is_complete_and_safe=False,
|
||||
global_summary="Erreur de formatage de l'évaluation par l'IA.",
|
||||
technical_feedback=["L'évaluation automatique n'a pas pu être structurée correctement."]
|
||||
)
|
||||
|
||||
if qa_evaluation.is_complete_and_safe:
|
||||
logger.info(f"[QA Agent] ✅ Projet '{project_title}' VALIDÉ (Analyse statique propre).")
|
||||
else:
|
||||
logger.warning(f"[QA Agent] ❌ Projet '{project_title}' REJETÉ (Failles de sécurité détectées).")
|
||||
|
||||
return qa_evaluation
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[QA Agent] ❌ Échec critique lors de l'analyse LLM : {type(e).__name__}: {str(e)}")
|
||||
return QAEvaluation(
|
||||
is_complete_and_safe=False,
|
||||
global_summary="Erreur interne de l'Agent QA lors de la génération du rapport LLM.",
|
||||
technical_feedback=[f"Le LLM a crashé avec l'erreur : {type(e).__name__}."]
|
||||
)
|
||||
Reference in New Issue
Block a user