Correction QA: Validation arborescence + qualité/sécurité, ajout logique de fin complète, suppression repo non conforme
This commit is contained in:
@@ -3,8 +3,8 @@ 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.schemas.qa_report import QAEvaluation, StructureEvaluation
|
||||
from app.llm.prompts import QA_AGENT_PROMPT, QA_STRUCTURE_AGENT_PROMPT
|
||||
from app.sandbox.qa_client import run_project_qa
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,8 +15,9 @@ async_client = httpx.AsyncClient(verify=False)
|
||||
async def run_qa_agent(project_title: str, dev_output: dict) -> QAEvaluation:
|
||||
"""
|
||||
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.
|
||||
Analyse le code généré à l'aide de l'API externe de la Sandbox (Semgrep uniquement),
|
||||
valide la structure minimale du projet à l'aide d'un second appel LLM,
|
||||
puis produit le rapport d'évaluation fusionné.
|
||||
"""
|
||||
logger.info(f"[QA Agent] Début du processus d'audit statique pour le projet : {project_title}")
|
||||
|
||||
@@ -39,12 +40,12 @@ async def run_qa_agent(project_title: str, dev_output: dict) -> QAEvaluation:
|
||||
http_async_client=async_client,
|
||||
)
|
||||
|
||||
# 2. Exécution de l'analyse statique via le service Sandbox indépendant
|
||||
# 1. 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)
|
||||
raw_results = await run_project_qa(repo_url=repo_url)
|
||||
|
||||
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)}")
|
||||
@@ -57,46 +58,84 @@ async def run_qa_agent(project_title: str, dev_output: dict) -> QAEvaluation:
|
||||
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))
|
||||
|
||||
# 2. Premier appel LLM : Audit de sécurité (Semgrep)
|
||||
qa_evaluation = None
|
||||
try:
|
||||
qa_evaluation = await structured_llm.ainvoke(messages)
|
||||
logger.info(f"[QA Agent] 📝 Justification du LLM : {qa_evaluation.global_summary}")
|
||||
logger.info("[QA Agent] Lancement de l'audit de sécurité et de qualité (Semgrep)...")
|
||||
structured_security_llm = llm.with_structured_output(QAEvaluation, strict=True)
|
||||
|
||||
security_messages = [
|
||||
SystemMessage(content=QA_AGENT_PROMPT),
|
||||
HumanMessage(content=(
|
||||
f"PROJET À AUDITER : {project_title}\n"
|
||||
f"LIEN DU DÉPÔT : {repo_url}\n"
|
||||
f"RÉSULTATS DE L'ANALYSE STATIQUE DU CODE (Semgrep) :\n"
|
||||
f"{raw_results.model_dump_json(indent=2)}\n\n"
|
||||
f"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é."
|
||||
))
|
||||
]
|
||||
|
||||
qa_evaluation = await structured_security_llm.ainvoke(security_messages)
|
||||
|
||||
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:
|
||||
reason = "Erreur d'exécution ou non-conformité" if "exit_code" in str(qa_evaluation) else "Failles de sécurité"
|
||||
logger.warning(f"[QA Agent] ❌ Projet '{project_title}' REJETÉ ({reason}).")
|
||||
|
||||
return qa_evaluation
|
||||
except Exception as e:
|
||||
logger.error(f"[QA Agent] ❌ Échec lors de l'audit de sécurité LLM : {type(e).__name__}: {str(e)}")
|
||||
qa_evaluation = QAEvaluation(
|
||||
is_complete_and_safe=False,
|
||||
global_summary="Erreur lors de la génération du rapport de sécurité par le LLM.",
|
||||
technical_feedback=[f"Le module de sécurité a échoué : {type(e).__name__}"]
|
||||
)
|
||||
|
||||
# 3. Second appel LLM : Validation de la structure minimale (Arborescence)
|
||||
structure_evaluation = None
|
||||
try:
|
||||
logger.info("[QA Agent] Lancement de l'audit de conformité de l'arborescence...")
|
||||
structured_structure_llm = llm.with_structured_output(StructureEvaluation, strict=True)
|
||||
|
||||
repo_structure = raw_results.runtime.stdout or "Arborescence non détectée ou vide."
|
||||
|
||||
structure_messages = [
|
||||
SystemMessage(content=QA_STRUCTURE_AGENT_PROMPT),
|
||||
HumanMessage(content=(
|
||||
f"PROJET À ANALYSER : {project_title}\n"
|
||||
f"ARBORESCENCE DU RÉPERTOIRE CLONÉ :\n"
|
||||
f"{repo_structure}\n\n"
|
||||
f"Vérifie si la structure respecte les exigences minimales et fournis ton évaluation structurée."
|
||||
))
|
||||
]
|
||||
|
||||
structure_evaluation = await structured_structure_llm.ainvoke(structure_messages)
|
||||
logger.info(f"[QA Agent] 📁 Structure détectée : {structure_evaluation.detected_language} | Valide : {structure_evaluation.is_valid}")
|
||||
|
||||
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__}."]
|
||||
)
|
||||
logger.error(f"[QA Agent] ❌ Échec lors de la validation de structure LLM : {type(e).__name__}: {str(e)}")
|
||||
structure_evaluation = StructureEvaluation(
|
||||
is_valid=False,
|
||||
detected_language="Inconnu",
|
||||
missing_elements=["Erreur d'analyse structurelle"],
|
||||
feedback="Le module d'analyse d'arborescence a rencontré une erreur de traitement."
|
||||
)
|
||||
|
||||
# 4. Fusion des résultats des deux audits
|
||||
final_is_complete_and_safe = qa_evaluation.is_complete_and_safe and structure_evaluation.is_valid
|
||||
final_feedback = list(qa_evaluation.technical_feedback)
|
||||
if not structure_evaluation.is_valid:
|
||||
for missing in structure_evaluation.missing_elements:
|
||||
final_feedback.append(f"[Structure] Élément obligatoire manquant : {missing}")
|
||||
|
||||
final_summary = (
|
||||
f"{qa_evaluation.global_summary}\n\n"
|
||||
f"--- 📁 Validation de la Structure ({structure_evaluation.detected_language}) ---\n"
|
||||
f"Statut : {'Conforme' if structure_evaluation.is_valid else 'Non conforme'}\n"
|
||||
f"Feedback : {structure_evaluation.feedback}"
|
||||
)
|
||||
|
||||
if final_is_complete_and_safe:
|
||||
logger.info(f"[QA Agent] ✅ Projet '{project_title}' VALIDÉ (Sécurité & Structure OK).")
|
||||
else:
|
||||
logger.warning(f"[QA Agent] ❌ Projet '{project_title}' REJETÉ (Non-conformité détectée).")
|
||||
|
||||
return QAEvaluation(
|
||||
is_complete_and_safe=final_is_complete_and_safe,
|
||||
global_summary=final_summary,
|
||||
technical_feedback=final_feedback
|
||||
)
|
||||
@@ -436,4 +436,17 @@ Tu disposes des données brutes suivantes :
|
||||
|
||||
### Ton Style de Feedback :
|
||||
Sois ultra-précis et technique. Ne dis pas "Il y a une erreur dans le code", dis plutôt : "La fonction X à la ligne Y lève une exception de type ValueError car la variable Z est passée à None". Traduis chaque erreur technique brute en une instruction claire et actionnable pour l'Agent Dev.
|
||||
"""
|
||||
|
||||
QA_STRUCTURE_AGENT_PROMPT = """
|
||||
Tu es un agent expert en architecture et standardisation logicielle.
|
||||
Ton rôle est d'analyser l'arborescence de fichiers d'un dépôt Git et de valider si la structure respecte rigoureusement les standards minimaux de livraison.
|
||||
|
||||
Critères stricts d'un projet conforme :
|
||||
1. DOCUMENTATION : Présence obligatoire d'un fichier de documentation (ex: README.md, README.txt).
|
||||
2. DÉPENDANCES : Présence obligatoire d'un fichier de gestion des dépendances adapté au langage (ex: requirements.txt ou pyproject.toml pour Python, package.json pour Node, go.mod pour Go, cargo.toml pour Rust, etc.). Ce fichier doit exister à la racine, même s'il est vide.
|
||||
3. POINT D'ENTRÉE : Un script principal ou fichier de démarrage cohérent (ex: main.py/app.py pour Python, index.js/server.js pour Node, main.go pour Go, etc.).
|
||||
4. SUITE DE TESTS : Présence obligatoire d'un dossier dédié aux tests (généralement nommé 'tests' ou 'test') contenant au moins un script de test (ex: test_script.py, app.test.js, etc.).
|
||||
|
||||
IMPORTANT : Sois intransigeant sur ces quatre piliers. Si le fichier de dépendances est absent (même si le script n'a pas de dépendances externes) ou si le dossier 'tests' est manquant ou vide, le projet doit être déclaré NON conforme (is_valid = False).
|
||||
"""
|
||||
@@ -3,7 +3,7 @@ FROM semgrep/semgrep:latest
|
||||
USER root
|
||||
|
||||
RUN sed -i 's/https/http/g' /etc/apk/repositories && \
|
||||
apk update && apk add --no-cache python3 py3-pip
|
||||
apk update && apk add --no-cache python3 py3-pip git
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
--trusted-host pypi.org \
|
||||
@@ -13,6 +13,10 @@ RUN pip install --no-cache-dir \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# COPY r2c-security-audit.yaml /app/r2c-security-audit.yaml
|
||||
COPY rules/ /app/rules/
|
||||
RUN sed -i 's/adjust_for_docker()/pass/g' /usr/lib/python3.12/site-packages/semgrep/commands/scan.py
|
||||
|
||||
COPY main.py .
|
||||
|
||||
EXPOSE 8004
|
||||
|
||||
@@ -1,85 +1,179 @@
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
from fastapi import FastAPI
|
||||
import tempfile
|
||||
import os
|
||||
import logging
|
||||
import traceback
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
SEMGREP_TIMEOUT = 300.0
|
||||
|
||||
@app.post("/scan/{repo_name}")
|
||||
async def scan_repository(repo_name: str):
|
||||
repo_path = f"/src/{repo_name}"
|
||||
issues_list = []
|
||||
SEMGREP_TIMEOUT = 300.0
|
||||
SEMGREP_RULES_DIR = "/app/rules/"
|
||||
|
||||
def generate_tree_string(path: str, max_depth: int = 3) -> str:
|
||||
"""Génère une représentation visuelle de l'arborescence (exclut le bruit)."""
|
||||
lines = []
|
||||
exclude_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "env"}
|
||||
|
||||
def _walk(current_path, depth):
|
||||
if depth > max_depth:
|
||||
return
|
||||
try:
|
||||
entries = sorted(os.listdir(current_path))
|
||||
except Exception:
|
||||
return
|
||||
|
||||
for entry in entries:
|
||||
if entry in exclude_dirs or entry.startswith('.'):
|
||||
continue
|
||||
full_path = os.path.join(current_path, entry)
|
||||
indent = " " * depth
|
||||
if os.path.isdir(full_path):
|
||||
lines.append(f"{indent}📁 {entry}/")
|
||||
_walk(full_path, depth + 1)
|
||||
else:
|
||||
lines.append(f"{indent}📄 {entry}")
|
||||
|
||||
_walk(path, 0)
|
||||
return "\n".join(lines)
|
||||
|
||||
@app.post("/scan")
|
||||
async def scan_repository(repo_url: str):
|
||||
start_time = time.time()
|
||||
issues_list = []
|
||||
|
||||
try:
|
||||
secure_command = f"ulimit -f 10240 && semgrep scan --config=p/r2c-security-audit --json --quiet {repo_path}"
|
||||
|
||||
result = subprocess.run(
|
||||
secure_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=SEMGREP_TIMEOUT
|
||||
)
|
||||
|
||||
duration = round(time.time() - start_time, 2)
|
||||
exit_code = result.returncode
|
||||
stderr_output = result.stderr
|
||||
timeout_triggered = False
|
||||
is_executable = True
|
||||
# 1. CRÉATION DU DOSSIER TEMPORAIRE ET CLONAGE DU PROJET À SCANNER
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
repo_path = os.path.join(tmp_dir, "repo")
|
||||
logger.info(f"[Sandbox] Clonage dynamique du projet {repo_url} dans {repo_path}...")
|
||||
|
||||
clone_cmd = f"git clone --depth 1 {repo_url} {repo_path}"
|
||||
clone_result = subprocess.run(clone_cmd, shell=True, capture_output=True, text=True)
|
||||
|
||||
if clone_result.returncode != 0:
|
||||
logger.error(f"[Sandbox] Échec du git clone du projet : {clone_result.stderr}")
|
||||
return {
|
||||
"results": {
|
||||
"is_executable": False,
|
||||
"runtime": {
|
||||
"exit_code": clone_result.returncode,
|
||||
"stdout": "",
|
||||
"stderr": f"Impossible de cloner le projet : {clone_result.stderr}",
|
||||
"duration_seconds": round(time.time() - start_time, 2),
|
||||
"timeout_triggered": False
|
||||
},
|
||||
"issues": []
|
||||
}
|
||||
}
|
||||
|
||||
repo_tree = generate_tree_string(repo_path)
|
||||
|
||||
try:
|
||||
cloned_files = os.listdir(repo_path)
|
||||
logger.info(f"[Sandbox] 📁 Fichiers récupérés après clone : {cloned_files}")
|
||||
if not cloned_files or cloned_files == ['.git']:
|
||||
logger.warning("[Sandbox] ⚠️ Le dossier cloné est vide (ou ne contient que .git) !")
|
||||
except Exception as dir_err:
|
||||
logger.error(f"[Sandbox] Impossible de lister le contenu du dossier cloné : {dir_err}")
|
||||
|
||||
except subprocess.TimeoutExpired as te:
|
||||
logger.error(f"[Sandbox] Semgrep a dépassé le timeout sur {repo_name}")
|
||||
duration = round(time.time() - start_time, 2)
|
||||
exit_code = -1
|
||||
stderr_output = f"L'analyse statique a été coupée : Timeout de {SEMGREP_TIMEOUT}s dépassé."
|
||||
timeout_triggered = True
|
||||
is_executable = False
|
||||
# 2. EXÉCUTION DE SEMGREP AVEC LA RÈGLE UNIQUE
|
||||
try:
|
||||
if os.path.exists(SEMGREP_RULES_DIR):
|
||||
logger.info(f"[Sandbox] Répertoire de règles détecté : {SEMGREP_RULES_DIR}")
|
||||
else:
|
||||
logger.error(f"[Sandbox] ❌ Répertoire de règles introuvable à l'emplacement : {SEMGREP_RULES_DIR}")
|
||||
|
||||
logger.info(f"[Sandbox] Lancement du scan Semgrep avec l'audit unique ({SEMGREP_RULES_DIR})...")
|
||||
secure_command = f"semgrep scan --config={SEMGREP_RULES_DIR} --json --quiet ."
|
||||
|
||||
result = subprocess.run(
|
||||
secure_command,
|
||||
shell=True,
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=SEMGREP_TIMEOUT
|
||||
)
|
||||
|
||||
duration = round(time.time() - start_time, 2)
|
||||
exit_code = result.returncode
|
||||
stderr_output = result.stderr
|
||||
timeout_triggered = False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("[Sandbox] Timeout Semgrep dépassé.")
|
||||
return {
|
||||
"results": {
|
||||
"is_executable": False,
|
||||
"runtime": {
|
||||
"exit_code": -1,
|
||||
"stdout": "",
|
||||
"stderr": "L'analyse statique a dépassé le timeout global.",
|
||||
"duration_seconds": round(time.time() - start_time, 2),
|
||||
"timeout_triggered": True
|
||||
},
|
||||
"issues": []
|
||||
}
|
||||
}
|
||||
|
||||
# 3. PARSING DES RÉSULTATS
|
||||
try:
|
||||
if result.returncode == 0:
|
||||
is_executable = True
|
||||
stdout_msg = f"Scan statique réussi.\n\n[STRUCTURE DETECTEE] :\n{repo_tree}"
|
||||
if result.stdout.strip():
|
||||
scan_data = json.loads(result.stdout)
|
||||
for item in scan_data.get("results", []):
|
||||
issues_list.append({
|
||||
"tool": "Semgrep",
|
||||
"file": item.get("path", "").replace(f"{repo_path}/", ""),
|
||||
"line": item.get("start", {}).get("line"),
|
||||
"code": item.get("check_id"),
|
||||
"severity": item.get("extra", {}).get("severity", "MEDIUM").upper(),
|
||||
"message": item.get("extra", {}).get("message", "")
|
||||
})
|
||||
else:
|
||||
is_executable = False
|
||||
stdout_msg = f"Échec du moteur de scan Semgrep (Exit code {result.returncode})."
|
||||
logger.error(f"[Sandbox] Semgrep a échoué ! Code: {result.returncode}\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}")
|
||||
|
||||
except Exception as e:
|
||||
is_executable = False
|
||||
stdout_msg = "Erreur lors du traitement des résultats du scan."
|
||||
stderr_output = f"Erreur parsing JSON Semgrep: {str(e)}\n{result.stderr}"
|
||||
|
||||
return {
|
||||
"results": {
|
||||
"is_executable": is_executable,
|
||||
"runtime": {
|
||||
"exit_code": exit_code,
|
||||
"stdout": stdout_msg,
|
||||
"stderr": stderr_output,
|
||||
"duration_seconds": duration,
|
||||
"timeout_triggered": timeout_triggered
|
||||
},
|
||||
"issues": issues_list
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as global_e:
|
||||
error_trace = traceback.format_exc()
|
||||
logger.error(f"[Sandbox] CRASH INTERNE CRITIQUE :\n{error_trace}")
|
||||
return {
|
||||
"results": {
|
||||
"is_executable": is_executable,
|
||||
"is_executable": False,
|
||||
"runtime": {
|
||||
"exit_code": exit_code,
|
||||
"stdout": "",
|
||||
"stderr": stderr_output,
|
||||
"duration_seconds": duration,
|
||||
"timeout_triggered": timeout_triggered
|
||||
"exit_code": -99,
|
||||
"stdout": "Crash critique du conteneur de la Sandbox.",
|
||||
"stderr": f"Exception Python : {str(global_e)}\n\nTraceback complet :\n{error_trace}",
|
||||
"duration_seconds": round(time.time() - start_time, 2),
|
||||
"timeout_triggered": False
|
||||
},
|
||||
"issues": []
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
if result.stdout.strip():
|
||||
scan_data = json.loads(result.stdout)
|
||||
for item in scan_data.get("results", []):
|
||||
issues_list.append({
|
||||
"tool": "Semgrep",
|
||||
"file": item.get("path", "").replace(f"{repo_path}/", ""),
|
||||
"line": item.get("start", {}).get("line"),
|
||||
"code": item.get("check_id"),
|
||||
"severity": item.get("extra", {}).get("severity", "MEDIUM").upper(),
|
||||
"message": item.get("extra", {}).get("message", "")
|
||||
})
|
||||
is_executable = True
|
||||
stderr_output = result.stderr
|
||||
except Exception as e:
|
||||
is_executable = False
|
||||
stderr_output = f"Erreur parsing JSON Semgrep: {str(e)}\n{result.stderr}"
|
||||
|
||||
return {
|
||||
"results": {
|
||||
"is_executable": is_executable,
|
||||
"runtime": {
|
||||
"exit_code": exit_code,
|
||||
"stdout": "Scan statique universel effectué avec succès.",
|
||||
"stderr": stderr_output,
|
||||
"duration_seconds": duration,
|
||||
"timeout_triggered": timeout_triggered
|
||||
},
|
||||
"issues": issues_list
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,42 @@ from app.core.config import settings
|
||||
from app.schemas.qa_report import QARawResults, RuntimeOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async_client = httpx.AsyncClient(verify=False)
|
||||
|
||||
async def run_project_qa(repo_name: str) -> QARawResults:
|
||||
async def run_project_qa(repo_url: str) -> QARawResults:
|
||||
"""
|
||||
Sollicite l'API du conteneur persistant arc-sandbox pour effectuer
|
||||
une analyse de qualité et sécurité statique (Semgrep).
|
||||
Sollicite l'API du conteneur arc-sandbox pour effectuer
|
||||
une analyse de qualité et sécurité statique (Semgrep) en clonant le dépôt.
|
||||
"""
|
||||
sandbox_url = f"http://arc-sandbox:8004/scan/{repo_name}"
|
||||
logger.info(f"[QA Client] Envoi de la requête de scan à la sandbox : {sandbox_url}")
|
||||
sandbox_url = "http://arc-sandbox:8004/scan"
|
||||
logger.info(f"[QA Client] Envoi de la requête de scan pour le dépôt : {repo_url}")
|
||||
|
||||
try:
|
||||
response = await async_client.post(sandbox_url, timeout=120.0)
|
||||
response = await async_client.post(sandbox_url, params={"repo_url": repo_url}, timeout=150.0)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return QARawResults.model_validate(data.get("results"))
|
||||
|
||||
if data is None:
|
||||
logger.error("[QA Client] ❌ La Sandbox a répondu 200 OK mais a renvoyé une réponse vide (null).")
|
||||
return QARawResults(
|
||||
is_executable=False,
|
||||
runtime=RuntimeOutput(
|
||||
exit_code=-1,
|
||||
stdout="",
|
||||
stderr="Erreur interne Sandbox : Réponse JSON vide.",
|
||||
duration_seconds=0,
|
||||
timeout_triggered=False
|
||||
),
|
||||
issues=[]
|
||||
)
|
||||
|
||||
results = data.get("results", {})
|
||||
if not results.get("is_executable"):
|
||||
logger.error(f"[QA Client] ❌ Erreur interne Sandbox (Semgrep STDERR) : {results.get('runtime', {}).get('stderr')}")
|
||||
|
||||
return QARawResults.model_validate(results)
|
||||
|
||||
else:
|
||||
logger.error(f"[QA Client] Erreur de la Sandbox (Status {response.status_code}): {response.text}")
|
||||
return QARawResults(
|
||||
|
||||
10094
backend/app/sandbox/rules/ci.yaml
Normal file
10094
backend/app/sandbox/rules/ci.yaml
Normal file
File diff suppressed because it is too large
Load Diff
20054
backend/app/sandbox/rules/cwe-top-25.yaml
Normal file
20054
backend/app/sandbox/rules/cwe-top-25.yaml
Normal file
File diff suppressed because it is too large
Load Diff
70779
backend/app/sandbox/rules/default.yaml
Normal file
70779
backend/app/sandbox/rules/default.yaml
Normal file
File diff suppressed because it is too large
Load Diff
41421
backend/app/sandbox/rules/owasp-top-ten.yaml
Normal file
41421
backend/app/sandbox/rules/owasp-top-ten.yaml
Normal file
File diff suppressed because it is too large
Load Diff
13698
backend/app/sandbox/rules/security-audit.yaml
Normal file
13698
backend/app/sandbox/rules/security-audit.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,4 +29,11 @@ class QAEvaluation(BaseModel):
|
||||
technical_feedback: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="Instructions de correction ultra-précises destinées au Dev Agent"
|
||||
)
|
||||
)
|
||||
|
||||
class StructureEvaluation(BaseModel):
|
||||
is_valid: bool = Field(description="True si le projet possède un README, un fichier de dépendance cohérent et un point d'entrée d'exécution.")
|
||||
detected_language: str = Field(description="Le langage principal identifié d'après la structure (ex: Python, Node, Go, Rust...).")
|
||||
missing_elements: list[str] = Field(description="Liste des fichiers obligatoires manquants (ex: ['README.md', 'requirements.txt']). Vide si tout est OK.")
|
||||
feedback: str = Field(description="Courte explication des manquements ou félicitations sur la structure.")
|
||||
|
||||
@@ -6,7 +6,166 @@ import json
|
||||
import os
|
||||
import io
|
||||
|
||||
# --- FONCTION DE RENDU DE L'UI SELON LE STATUT ---
|
||||
# --- CONFIGURATION GITEA ---
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "ton_token_gitea_ici")
|
||||
|
||||
async def push_readme_update_to_gitea(repo_url: str, readme_content: str):
|
||||
"""
|
||||
Met à jour ou crée le fichier README.md directement sur Gitea via son API REST.
|
||||
"""
|
||||
if not repo_url:
|
||||
return
|
||||
|
||||
import base64
|
||||
clean_url = repo_url.replace(".git", "")
|
||||
parts = clean_url.rstrip("/").split("/")
|
||||
|
||||
if len(parts) >= 2:
|
||||
repo_name = parts[-1]
|
||||
owner = parts[-2]
|
||||
|
||||
# API Gitea pour récupérer/modifier le contenu d'un fichier
|
||||
gitea_file_url = f"http://git-arc:3000/api/v1/repos/{owner}/{repo_name}/contents/README.md"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
if GITEA_TOKEN:
|
||||
headers["Authorization"] = f"token {GITEA_TOKEN}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
sha = None
|
||||
# 1. On tente de récupérer le SHA du README actuel s'il existe (requis par Gitea pour les updates)
|
||||
try:
|
||||
get_resp = await client.get(gitea_file_url, headers=headers, timeout=5.0)
|
||||
if get_resp.status_code == 200:
|
||||
sha = get_resp.json().get("sha")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Impossible de récupérer le README actuel (il n'existe peut-être pas) : {e}")
|
||||
|
||||
# 2. Encodage du contenu en base64 (requis par l'API Gitea)
|
||||
encoded_content = base64.b64encode(readme_content.encode("utf-8")).decode("utf-8")
|
||||
|
||||
payload = {
|
||||
"content": encoded_content,
|
||||
"message": "docs: ajouter le statut de conformité QA au README",
|
||||
"branch": "main" # ou "master" selon ta configuration par défaut
|
||||
}
|
||||
if sha:
|
||||
payload["sha"] = sha # Requis si le fichier existe déjà
|
||||
|
||||
# 3. Écriture (PUT crée ou met à jour le fichier)
|
||||
try:
|
||||
put_resp = await client.put(gitea_file_url, headers=headers, json=payload, timeout=10.0)
|
||||
if put_resp.status_code in [200, 201]:
|
||||
print("📝 README.md mis à jour avec succès sur Gitea !")
|
||||
else:
|
||||
print(f"⚠️ Échec de la mise à jour du README sur Gitea : {put_resp.status_code} - {put_resp.text}")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de l'écriture du README sur Gitea : {e}")
|
||||
|
||||
async def delete_gitea_repository(repo_url: str):
|
||||
"""
|
||||
Supprime le dépôt sur Gitea via son API REST en cas d'échec ou d'abandon.
|
||||
"""
|
||||
if not repo_url:
|
||||
return
|
||||
|
||||
# Nettoyage de l'URL pour extraire le owner et le repo_name
|
||||
clean_url = repo_url.replace(".git", "")
|
||||
parts = clean_url.rstrip("/").split("/")
|
||||
|
||||
if len(parts) >= 2:
|
||||
repo_name = parts[-1]
|
||||
owner = parts[-2]
|
||||
|
||||
# URL interne au réseau Docker pour appeler l'API Gitea
|
||||
gitea_api_url = f"http://git-arc:3000/api/v1/repos/{owner}/{repo_name}"
|
||||
|
||||
headers = {}
|
||||
if GITEA_TOKEN:
|
||||
headers["Authorization"] = f"token {GITEA_TOKEN}"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.delete(gitea_api_url, headers=headers, timeout=10.0)
|
||||
if response.status_code == 204:
|
||||
print(f"🗑️ Dépôt Gitea {owner}/{repo_name} supprimé avec succès.")
|
||||
else:
|
||||
print(f"⚠️ Échec de la suppression Gitea : {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la suppression du dépôt Gitea : {e}")
|
||||
|
||||
|
||||
def inject_conforme_badge(state: dict) -> dict:
|
||||
"""
|
||||
Injecte la mention 'Conforme' dans le README.md du projet
|
||||
dans l'état de session (gère le format liste de dict ou dictionnaire plat).
|
||||
"""
|
||||
dev_data = state.get("generated_code", {}) or {}
|
||||
raw_files = dev_data.get("files", {})
|
||||
badge_text = "\n\n---\n🛡️ **Statut QA :** ✅ Conforme\n"
|
||||
|
||||
if isinstance(raw_files, list):
|
||||
readme_found = False
|
||||
for f in raw_files:
|
||||
if isinstance(f, dict):
|
||||
path = f.get("path") or f.get("filename") or f.get("name") or ""
|
||||
if path.lower() in ["readme.md", "readme"]:
|
||||
content = f.get("content") or f.get("code") or ""
|
||||
if "✅ Conforme" not in content:
|
||||
new_content = content + badge_text
|
||||
if "content" in f:
|
||||
f["content"] = new_content
|
||||
elif "code" in f:
|
||||
f["code"] = new_content
|
||||
readme_found = True
|
||||
break
|
||||
if not readme_found:
|
||||
raw_files.append({
|
||||
"path": "README.md",
|
||||
"content": "# Projet\n" + badge_text
|
||||
})
|
||||
|
||||
elif isinstance(raw_files, dict):
|
||||
readme_key = None
|
||||
for k in raw_files.keys():
|
||||
if k.lower() in ["readme.md", "readme"]:
|
||||
readme_key = k
|
||||
break
|
||||
if readme_key:
|
||||
content = raw_files[readme_key] or ""
|
||||
if "✅ Conforme" not in content:
|
||||
raw_files[readme_key] = content + badge_text
|
||||
else:
|
||||
raw_files["README.md"] = "# Projet" + badge_text
|
||||
|
||||
return state
|
||||
|
||||
|
||||
async def reset_to_start(message_text: str = "Bonjour 👋 Je suis ARC. Décris-moi ton besoin logiciel."):
|
||||
"""
|
||||
Réinitialise complètement l'état de la session utilisateur.
|
||||
"""
|
||||
initial_state = {
|
||||
"user_input": "",
|
||||
"user_feedback": None,
|
||||
"chat_history": [],
|
||||
"spec": {},
|
||||
"status": "start",
|
||||
"loop_count": 0,
|
||||
"existing_project": None,
|
||||
"generated_code": None,
|
||||
"qa_result": None,
|
||||
"is_completed": False
|
||||
}
|
||||
cl.user_session.set("graph_state", initial_state)
|
||||
cl.user_session.set("current_files", {})
|
||||
cl.user_session.set("side_code_viewer", None)
|
||||
cl.user_session.set("files_message", None)
|
||||
|
||||
await cl.Message(content=message_text).send()
|
||||
|
||||
async def render_workflow_state(new_state: dict):
|
||||
"""
|
||||
Fonction centrale pour aiguiller l'affichage Chainlit
|
||||
@@ -62,7 +221,6 @@ async def render_workflow_state(new_state: dict):
|
||||
final_state = response.json()
|
||||
cl.user_session.set("graph_state", final_state)
|
||||
|
||||
# Rappel de la fonction pour traiter le nouvel état de review
|
||||
await render_workflow_state(final_state)
|
||||
|
||||
else:
|
||||
@@ -74,114 +232,175 @@ async def render_workflow_state(new_state: dict):
|
||||
).send()
|
||||
|
||||
elif current_status in ["wait_human_review", "approved_by_human"]:
|
||||
dev_data = new_state.get("generated_code", {})
|
||||
qa_res = new_state.get("qa_result", {})
|
||||
|
||||
raw_files = dev_data.get("files", {})
|
||||
files = {}
|
||||
if isinstance(raw_files, list):
|
||||
for f in raw_files:
|
||||
if isinstance(f, dict):
|
||||
path = f.get("path") or f.get("filename") or f.get("name")
|
||||
content = f.get("content") or f.get("code") or ""
|
||||
if path:
|
||||
files[path] = content
|
||||
elif isinstance(raw_files, dict):
|
||||
files = raw_files
|
||||
|
||||
is_safe = qa_res.get("is_complete_and_safe", False)
|
||||
badge_qa = "✅ CONFORME" if is_safe else "⚠️ SÉCURITÉ/QUALITÉ À VÉRIFIER"
|
||||
dev_data = new_state.get("generated_code", {}) or {}
|
||||
qa_res = new_state.get("qa_result", {}) or {}
|
||||
|
||||
gitea_url = dev_data.get("repo_url", "")
|
||||
# Correction de l'URL pour y accéder depuis ton navigateur (hors réseau Docker)
|
||||
if gitea_url and "git-arc:3000" in gitea_url:
|
||||
gitea_url = gitea_url.replace("git-arc:3000", "localhost:3000")
|
||||
|
||||
access_md = f"**Résultat global QA :** {badge_qa}\n\n"
|
||||
access_md += "### 🔗 Accès au projet\n\n"
|
||||
if gitea_url:
|
||||
access_md += f"🔗 **Lien vers le dépôt Gitea :** [Accéder au dépôt]({gitea_url})\n\n"
|
||||
|
||||
# 2. Gestion du bouton de téléchargement ZIP (Correction finale de la variable)
|
||||
project_title = new_state.get("generated_code", {}).get("spec_title", "projet")
|
||||
message_elements = []
|
||||
|
||||
if files:
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for filepath, file_content in files.items():
|
||||
zip_file.writestr(filepath, file_content)
|
||||
zip_bytes = zip_buffer.getvalue()
|
||||
|
||||
message_elements.append(
|
||||
cl.File(name=f"{project_title}.zip", content=zip_bytes, display="inline")
|
||||
)
|
||||
else:
|
||||
access_md += "⚠️ Aucun fichier source trouvé pour générer le ZIP.\n"
|
||||
|
||||
await cl.Message(content=access_md, elements=message_elements).send()
|
||||
is_safe = qa_res.get("is_complete_and_safe", False)
|
||||
|
||||
# 3. Code Source Produit (Regroupé dans un seul message avec ses actions)
|
||||
cl.user_session.set("current_files", files)
|
||||
|
||||
file_actions = []
|
||||
for filename in files.keys():
|
||||
file_actions.append(
|
||||
cl.Action(
|
||||
name="open_file",
|
||||
payload={"filename": filename},
|
||||
label=f"📄 {filename}"
|
||||
# ==================== PARCOURS 1 : LE PROJET EST CONFORME ====================
|
||||
if is_safe:
|
||||
raw_files = dev_data.get("files", {})
|
||||
files = {}
|
||||
if isinstance(raw_files, list):
|
||||
for f in raw_files:
|
||||
if isinstance(f, dict):
|
||||
path = f.get("path") or f.get("filename") or f.get("name")
|
||||
content = f.get("content") or f.get("code") or ""
|
||||
if path:
|
||||
files[path] = content
|
||||
elif isinstance(raw_files, dict):
|
||||
files = raw_files
|
||||
|
||||
access_md = "**Résultat global QA :** ✅ CONFORME\n\n"
|
||||
access_md += "### 🔗 Accès au projet\n\n"
|
||||
if gitea_url:
|
||||
access_md += f"🔗 **Lien vers le dépôt Gitea :** [Accéder au dépôt]({gitea_url})\n\n"
|
||||
|
||||
project_title = dev_data.get("spec_title", "projet")
|
||||
message_elements = []
|
||||
|
||||
if files:
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for filepath, file_content in files.items():
|
||||
zip_file.writestr(filepath, file_content)
|
||||
zip_bytes = zip_buffer.getvalue()
|
||||
|
||||
message_elements.append(
|
||||
cl.File(name=f"{project_title}.zip", content=zip_bytes, display="inline")
|
||||
)
|
||||
)
|
||||
|
||||
files_message = cl.Message(
|
||||
content="### 📂 Code Source Produit\n\nClique sur un fichier pour l'afficher :",
|
||||
actions=file_actions
|
||||
)
|
||||
await files_message.send()
|
||||
cl.user_session.set("files_message", files_message)
|
||||
else:
|
||||
access_md += "⚠️ Aucun fichier source trouvé pour générer le ZIP.\n"
|
||||
|
||||
# 4. Demande de validation (Boutons Valider / Refuser)
|
||||
res = await cl.AskActionMessage(
|
||||
content="**Souhaitez-vous valider et packager ce livrable ?**",
|
||||
actions=[
|
||||
cl.Action(name="valider_projet", payload={"value": "approve"}, label="✅ Valider & Livrer", description="Génère l'archive et indexe le projet"),
|
||||
cl.Action(name="refuser_projet", payload={"value": "refuse"}, label="❌ Refuser et corriger", description="Renvoie le projet au PM avec vos commentaires")
|
||||
],
|
||||
timeout=3600
|
||||
).send()
|
||||
|
||||
if res and res.get("name") == "valider_projet":
|
||||
await cl.Message(content="🎉 **Projet validé.**").send()
|
||||
new_state["status"] = "human_approved"
|
||||
await cl.Message(content=access_md, elements=message_elements).send()
|
||||
|
||||
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)
|
||||
cl.user_session.set("current_files", files)
|
||||
|
||||
final_state = resp.json()
|
||||
cl.user_session.set("graph_state", final_state)
|
||||
file_actions = []
|
||||
for filename in files.keys():
|
||||
file_actions.append(
|
||||
cl.Action(
|
||||
name="open_file",
|
||||
payload={"filename": filename},
|
||||
label=f"📄 {filename}"
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
files_message = cl.Message(
|
||||
content="### 📂 Code Source Produit\n\nClique sur un fichier pour l'afficher :",
|
||||
actions=file_actions
|
||||
)
|
||||
await files_message.send()
|
||||
cl.user_session.set("files_message", files_message)
|
||||
|
||||
res = await cl.AskActionMessage(
|
||||
content="**Souhaitez-vous valider et packager ce livrable ?**",
|
||||
actions=[
|
||||
cl.Action(name="valider_projet", payload={"value": "approve"}, label="✅ Valider & Livrer", description="Ajoute le statut conforme au README et livre l'archive"),
|
||||
cl.Action(name="refuser_projet", payload={"value": "refuse"}, label="❌ Refuser et corriger", description="Renvoie le projet au PM avec tes commentaires")
|
||||
],
|
||||
timeout=3600
|
||||
).send()
|
||||
|
||||
if feedback_user:
|
||||
new_state["status"] = "human_refused"
|
||||
new_state["user_feedback"] = feedback_user["output"]
|
||||
cl.user_session.set("graph_state", new_state)
|
||||
if res and res.get("name") == "valider_projet":
|
||||
await cl.Message(content="🎉 **Projet validé et marqué comme conforme !**").send()
|
||||
|
||||
await cl.Message(content="🔄 **Feedback transmis.** Prise en compte des modifications...").send()
|
||||
new_state = inject_conforme_badge(new_state)
|
||||
|
||||
readme_content = "# Projet\n\n---\n🛡️ **Statut QA :** ✅ Conforme\n"
|
||||
dev_data = new_state.get("generated_code", {}) or {}
|
||||
raw_files = dev_data.get("files", {})
|
||||
|
||||
if isinstance(raw_files, list):
|
||||
for f in raw_files:
|
||||
if isinstance(f, dict):
|
||||
path = f.get("path") or f.get("filename") or f.get("name") or ""
|
||||
if path.lower() in ["readme.md", "readme"]:
|
||||
readme_content = f.get("content") or f.get("code") or ""
|
||||
break
|
||||
elif isinstance(raw_files, dict):
|
||||
for k, v in raw_files.items():
|
||||
if k.lower() in ["readme.md", "readme"]:
|
||||
readme_content = v
|
||||
break
|
||||
|
||||
await push_readme_update_to_gitea(gitea_url, readme_content)
|
||||
|
||||
new_state["status"] = "human_approved"
|
||||
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)
|
||||
|
||||
loop_state = resp.json()
|
||||
cl.user_session.set("graph_state", loop_state)
|
||||
final_state = resp.json()
|
||||
cl.user_session.set("graph_state", final_state)
|
||||
|
||||
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
|
||||
).send()
|
||||
|
||||
await render_workflow_state(loop_state)
|
||||
if feedback_user:
|
||||
new_state["status"] = "human_refused"
|
||||
new_state["user_feedback"] = feedback_user["output"]
|
||||
cl.user_session.set("graph_state", new_state)
|
||||
|
||||
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)
|
||||
|
||||
loop_state = resp.json()
|
||||
cl.user_session.set("graph_state", loop_state)
|
||||
|
||||
await render_workflow_state(loop_state)
|
||||
|
||||
# ==================== PARCOURS 2 : LE PROJET EST NON CONFORME ====================
|
||||
else:
|
||||
qa_feedback = qa_res.get("feedback", "L'agent QA a détecté des anomalies majeures sans description.")
|
||||
|
||||
non_compliant_md = (
|
||||
"🚨 **Projet Non Conforme**\n\n"
|
||||
"Malheureusement, le code produit par ARC n'a pas passé les critères de sécurité ou de qualité requis.\n\n"
|
||||
f"### 📋 Rapport d'audit QA :\n"
|
||||
f"> {qa_feedback}\n\n"
|
||||
"Pour des raisons de sécurité et de qualité, le projet ne peut pas être livré en l'état.\n\n"
|
||||
)
|
||||
|
||||
await cl.Message(content=non_compliant_md).send()
|
||||
|
||||
res = await cl.AskActionMessage(
|
||||
content="**Que souhaitez-vous faire ?**",
|
||||
actions=[
|
||||
cl.Action(name="recommencer_projet", payload={"value": "restart"}, label="🔄 Recommencer de zéro", description="Supprime le dépôt et réinitialise l'analyse"),
|
||||
cl.Action(name="abandonner_projet", payload={"value": "abort"}, label="🗑️ Abandonner & Quitter", description="Supprime le dépôt et réinitialise l'application")
|
||||
],
|
||||
timeout=3600
|
||||
).send()
|
||||
|
||||
if res and res.get("name") in ["recommencer_projet", "abandonner_projet"]:
|
||||
await cl.Message(content="🗑️ **Nettoyage en cours... Suppression du dépôt Gitea...**").send()
|
||||
|
||||
# Suppression du dépôt distant
|
||||
await delete_gitea_repository(gitea_url)
|
||||
|
||||
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 : 👇"
|
||||
)
|
||||
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."
|
||||
)
|
||||
await reset_to_start(reset_msg)
|
||||
|
||||
elif current_status == "delivered":
|
||||
await cl.Message(content="✅ Ce projet a déjà été traité et livré.").send()
|
||||
@@ -222,22 +441,10 @@ async def on_open_file(action: cl.Action):
|
||||
await new_viewer.send(for_id=message_id)
|
||||
cl.user_session.set("side_code_viewer", new_viewer)
|
||||
|
||||
|
||||
@cl.on_chat_start
|
||||
async def on_chat_start():
|
||||
initial_state = {
|
||||
"user_input": "",
|
||||
"user_feedback": None,
|
||||
"chat_history": [],
|
||||
"spec": {},
|
||||
"status": "start",
|
||||
"loop_count": 0,
|
||||
"existing_project": None,
|
||||
"generated_code": None,
|
||||
"qa_result": None,
|
||||
"is_completed": False
|
||||
}
|
||||
cl.user_session.set("graph_state", initial_state)
|
||||
await cl.Message(content="Bonjour 👋 Je suis ARC. Décris-moi ton besoin logiciel.").send()
|
||||
await reset_to_start()
|
||||
|
||||
|
||||
@cl.on_message
|
||||
@@ -265,5 +472,4 @@ async def on_message(message: cl.Message):
|
||||
new_state = response.json()
|
||||
cl.user_session.set("graph_state", new_state)
|
||||
|
||||
# Appel du rendu
|
||||
await render_workflow_state(new_state)
|
||||
@@ -7,8 +7,6 @@ services:
|
||||
container_name: arc-sandbox
|
||||
ports:
|
||||
- "8004:8004"
|
||||
volumes:
|
||||
- ./temp_repos:/src:rw
|
||||
networks:
|
||||
- arc-network
|
||||
|
||||
|
||||
Reference in New Issue
Block a user