Etape 2 fini
This commit is contained in:
@@ -1,14 +1,69 @@
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage, HumanMessage
|
||||
from app.core.config import settings
|
||||
from app.schemas.code_output import ProjectCodeOutput, GeneratedFile
|
||||
from app.llm.prompts import DEV_AGENT_PROMPT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Clients HTTPX configurés pour ignorer les blocages de certificats/révocation du lab
|
||||
sync_client = httpx.Client(verify=False)
|
||||
async_client = httpx.AsyncClient(verify=False)
|
||||
|
||||
async def run_dev_agent(spec: dict, qa_feedback: list = None) -> dict:
|
||||
"""
|
||||
Agent Dev minimal :
|
||||
- retourne une pseudo arborescence + un code exemple
|
||||
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.
|
||||
"""
|
||||
return {
|
||||
"tree": [
|
||||
"main.py",
|
||||
"README.md",
|
||||
"app/__init__.py",
|
||||
],
|
||||
"code": 'print("Hello from ARC generated project")',
|
||||
"spec_title": spec.get("title"),
|
||||
}
|
||||
logger.info(f"[Dev Agent] Début de la génération pour : {spec.get('title', 'Sans titre')}")
|
||||
|
||||
llm = ChatOpenAI(
|
||||
base_url=settings.llm_base_url,
|
||||
api_key=settings.llm_api_key,
|
||||
model=settings.llm_model_dev,
|
||||
temperature=0.2,
|
||||
max_retries=2,
|
||||
http_client=sync_client,
|
||||
http_async_client=async_client,
|
||||
model_kwargs={"response_format": {"type": "json_object"}}
|
||||
)
|
||||
structured_llm = llm.with_structured_output(ProjectCodeOutput, strict=True)
|
||||
|
||||
messages = [
|
||||
SystemMessage(content=DEV_AGENT_PROMPT),
|
||||
]
|
||||
|
||||
user_content = f"CAHIER DES CHARGES (ProjectSpec) :\n{json.dumps(spec, 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"
|
||||
|
||||
user_content += "Génère maintenant le JSON complet contenant l'arborescence ('tree') et tous les fichiers ('files') décrits."
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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)")
|
||||
title = spec.get("title", "automation_script")
|
||||
|
||||
fallback = ProjectCodeOutput(
|
||||
spec_title=title,
|
||||
tree=["main.py", "README.md", "requirements.txt"],
|
||||
files=[
|
||||
GeneratedFile(path="main.py", content="import logging\nlogging.basicConfig(level=logging.INFO)\n\ndef main():\n logging.error('Le Dev Agent a rencontré une erreur de génération.')\n\nif __name__ == '__main__':\n main()"),
|
||||
GeneratedFile(path="README.md", content=f"# {title}\nGénération en mode fallback suite à une erreur technique."),
|
||||
GeneratedFile(path="requirements.txt", content="# Aucune dépendance externe définie (Fallback)\n")
|
||||
]
|
||||
)
|
||||
return fallback.model_dump()
|
||||
@@ -1,52 +1,49 @@
|
||||
#pm_agent.py
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
import httpx
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage, HumanMessage
|
||||
from app.schemas.spec import ProjectSpec
|
||||
from app.llm.prompts import PM_AGENT_PROMPT
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
sync_client = httpx.Client(verify=False)
|
||||
async_client = httpx.AsyncClient(verify=False)
|
||||
|
||||
async def run_pm_agent(user_input: str, history: list | None = None) -> ProjectSpec:
|
||||
"""Agent PM qui remplit ProjectSpec en analysant le besoin utilisateur."""
|
||||
|
||||
|
||||
llm = ChatOpenAI(
|
||||
base_url=settings.llm_base_url,
|
||||
api_key=settings.llm_api_key,
|
||||
model="local-model",
|
||||
model=settings.llm_model,
|
||||
temperature=0.3,
|
||||
max_retries=2,
|
||||
http_client=sync_client,
|
||||
http_async_client=async_client,
|
||||
)
|
||||
structured_llm = llm.with_structured_output(ProjectSpec, strict=True)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": PM_AGENT_PROMPT},
|
||||
SystemMessage(content=PM_AGENT_PROMPT),
|
||||
]
|
||||
|
||||
if history:
|
||||
messages.extend(history)
|
||||
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": f"""En te basant sur TOUT l'historique de notre conversation et sur cette dernière précision, génère le JSON ProjectSpec valide et complètement rempli.
|
||||
messages.append(HumanMessage(
|
||||
content=f"""En te basant sur TOUT l'historique de notre conversation et sur cette dernière précision, génère le JSON ProjectSpec valide et complètement rempli.
|
||||
|
||||
DERNIÈRE PRÉCISION / INPUT UTILISATEUR :
|
||||
{user_input}
|
||||
|
||||
IMPORTANT : Tu dois impérativement combiner les contraintes énoncées précédemment (comme le langage de programmation choisi, l'OS, etc.) avec cette nouvelle information pour enrichir la spécification existante.
|
||||
Réponds UNIQUEMENT avec du JSON valide. Aucun texte avant le JSON, aucun texte après le JSON."""
|
||||
})
|
||||
))
|
||||
|
||||
try:
|
||||
response = await llm.ainvoke(messages)
|
||||
raw_response = response.content if hasattr(response, 'content') else str(response)
|
||||
logger.debug(f"[PM Agent] Réponse brute: {raw_response}")
|
||||
|
||||
parsed_spec = _extract_json_robust(raw_response)
|
||||
|
||||
project_spec = ProjectSpec(**parsed_spec)
|
||||
logger.info(f"[PM Agent] ✅ Tentative réussie (parsing JSON manuel)")
|
||||
|
||||
project_spec = await structured_llm.ainvoke(messages)
|
||||
logger.info(f"[PM Agent] ✅ Tentative réussie (Structured Output)")
|
||||
project_spec = _validate_and_sanitize(project_spec, user_input)
|
||||
return project_spec
|
||||
|
||||
@@ -56,62 +53,6 @@ Réponds UNIQUEMENT avec du JSON valide. Aucun texte avant le JSON, aucun texte
|
||||
|
||||
return _generate_fallback_spec(user_input)
|
||||
|
||||
|
||||
def _extract_json_robust(response_text: str) -> dict:
|
||||
"""
|
||||
Extrait et parse le JSON depuis la réponse du LLM.
|
||||
Robuste pour llama.cpp qui ajoute souvent du texte avant/après le JSON.
|
||||
"""
|
||||
|
||||
logger.debug(f"[JSON Extract] Réponse brute: {response_text}")
|
||||
|
||||
# Stratégie 1: Chercher un bloc JSON entre ```json et ```
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', response_text, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(1).strip()
|
||||
logger.debug(f"[JSON Extract] ✓ Trouvé via ```json block")
|
||||
return json.loads(json_str)
|
||||
|
||||
# Stratégie 2: Chercher un bloc JSON entre ``` et ``` (sans spécifier json)
|
||||
json_match = re.search(r'```\s*(.*?)\s*```', response_text, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(1).strip()
|
||||
logger.debug(f"[JSON Extract] ✓ Trouvé via ``` block")
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(f"[JSON Extract] ✗ Code block trouvé mais pas JSON valide")
|
||||
|
||||
# Stratégie 3: Chercher directement un objet JSON { ... } en étant très prudent
|
||||
first_brace = response_text.find('{')
|
||||
last_brace = response_text.rfind('}')
|
||||
|
||||
if first_brace != -1 and last_brace != -1 and last_brace > first_brace:
|
||||
json_str = response_text[first_brace:last_brace+1].strip()
|
||||
|
||||
logger.debug(f"[JSON Extract] Tentative extraction (longueur: {len(json_str)})")
|
||||
|
||||
try:
|
||||
parsed = json.loads(json_str)
|
||||
logger.debug(f"[JSON Extract] ✓ Trouvé via extraction")
|
||||
return parsed
|
||||
except json.JSONDecodeError as e:
|
||||
logger.debug(f"[JSON Extract] ✗ Extraction invalide: {e}")
|
||||
|
||||
# Stratégie 4: Parser tout le texte comme JSON (dernier recours)
|
||||
try:
|
||||
parsed = json.loads(response_text.strip())
|
||||
logger.debug(f"[JSON Extract] ✓ Trouvé via parsing direct")
|
||||
return parsed
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(f"[JSON Extract] ✗ Parsing direct échoué")
|
||||
|
||||
# Aucune stratégie n'a fonctionné
|
||||
logger.error(f"[JSON Extract] ❌ Impossible d'extraire JSON valide")
|
||||
logger.error(f"[JSON Extract] Réponse brute complète:\n{response_text}")
|
||||
raise ValueError(f"Impossible d'extraire un JSON valide de la réponse du LLM.\nRéponse: {response_text}")
|
||||
|
||||
|
||||
def _validate_and_sanitize(spec: ProjectSpec, user_input: str) -> ProjectSpec:
|
||||
"""
|
||||
Fait confiance au LLM pour la logique métier.
|
||||
|
||||
Reference in New Issue
Block a user