2026-07-07 09:57:21 +02:00
|
|
|
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:
|
2026-06-12 18:16:58 +02:00
|
|
|
"""
|
2026-07-07 09:57:21 +02:00
|
|
|
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.
|
2026-06-12 18:16:58 +02:00
|
|
|
"""
|
2026-07-07 09:57:21 +02:00
|
|
|
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__}."]
|
|
|
|
|
)
|