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=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 = [ SystemMessage(content=PM_AGENT_PROMPT), ] if history: messages.extend(history) 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. """ )) try: 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 except Exception as e2: logger.error(f"[PM Agent] ❌ Tentative échouée: {type(e2).__name__}: {str(e2)}") logger.info("[PM Agent] Fallback: Génération automatique du spec") return _generate_fallback_spec(user_input) def _validate_and_sanitize(spec: ProjectSpec, user_input: str) -> ProjectSpec: """ Fait confiance au LLM pour la logique métier. Se contente de s'assurer de la cohérence technique minimale. """ logger.debug(f"[Sanitize] Validation du spec généré pour : {spec.title}") # 1. Sécurité minimale sur les chaînes de texte vides if not spec.title or spec.title.lower() in ["null", "none", ""]: spec.title = "Automation Script" if not spec.description: spec.description = f"Script d'automatisation basé sur : {user_input[:50]}..." # 2. Gestion de la complétude : if not spec.is_complete or (spec.clarifying_question and spec.clarifying_question.strip()): spec.is_complete = False if not spec.clarifying_question: spec.clarifying_question = "Pourriez-vous apporter plus de précisions sur les actions que doit effectuer ce script ?" logger.info(f"[Sanitize] Le besoin est qualifié d'INCOMPLET par l'agent PM.") else: spec.is_complete = True spec.clarifying_question = None logger.info(f"[Sanitize] Le besoin est qualifié de COMPLET par l'agent PM.") return spec def _generate_fallback_spec(user_input: str) -> ProjectSpec: """Génère un ProjectSpec par défaut en cas d'erreur complète.""" logger.warning(f"[Fallback] Génération d'un spec par défaut pour: {user_input}") return ProjectSpec( title="Automation Script", description=f"Automatisation du besoin utilisateur: {user_input}", requirements=[ f"Exécuter l'action demandée: {user_input}", "Gérer les erreurs", "Générer des logs" ], constraints=["PEP 8 strict"], language="Python", target_stack="", io_config={ "has_inputs": False, "input_type": "none", "input_paths_or_sources": [], "has_outputs": False, "output_type": "log_only", "output_formats": [] }, auth_config={ "requires_auth": False, "auth_method": "none", "target_tools_and_apis": [] }, env_config={ "target_os": "cross_platform", "language_version": "3.11", "critical_dependencies": ["logging"] }, error_handling_strategy="log_and_continue", is_complete=False, clarifying_question="Pourriez-vous donner plus de détails sur votre demande ?" )