2026-06-16 11:27:41 +02:00
import logging
2026-06-17 10:18:55 +02:00
import httpx
2026-06-16 11:27:41 +02:00
from langchain_openai import ChatOpenAI
2026-06-17 10:18:55 +02:00
from langchain_core . messages import SystemMessage , HumanMessage
2026-06-12 18:16:58 +02:00
from app . schemas . spec import ProjectSpec
2026-06-16 11:27:41 +02:00
from app . llm . prompts import PM_AGENT_PROMPT
from app . core . config import settings
2026-06-12 18:16:58 +02:00
2026-06-16 11:27:41 +02:00
logger = logging . getLogger ( __name__ )
2026-06-17 10:18:55 +02:00
sync_client = httpx . Client ( verify = False )
async_client = httpx . AsyncClient ( verify = False )
2026-06-12 18:16:58 +02:00
2026-06-16 11:27:41 +02:00
async def run_pm_agent ( user_input : str , history : list | None = None ) - > ProjectSpec :
""" Agent PM qui remplit ProjectSpec en analysant le besoin utilisateur. """
2026-06-17 10:18:55 +02:00
2026-06-16 11:27:41 +02:00
llm = ChatOpenAI (
base_url = settings . llm_base_url ,
api_key = settings . llm_api_key ,
2026-06-17 10:18:55 +02:00
model = settings . llm_model ,
2026-06-16 11:27:41 +02:00
temperature = 0.3 ,
2026-06-17 10:18:55 +02:00
max_retries = 2 ,
http_client = sync_client ,
http_async_client = async_client ,
2026-06-16 11:27:41 +02:00
)
2026-06-17 10:18:55 +02:00
structured_llm = llm . with_structured_output ( ProjectSpec , strict = True )
2026-06-16 11:27:41 +02:00
messages = [
2026-06-17 10:18:55 +02:00
SystemMessage ( content = PM_AGENT_PROMPT ) ,
2026-06-16 11:27:41 +02:00
]
if history :
messages . extend ( history )
2026-06-17 10:18:55 +02:00
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.
2026-06-16 11:27:41 +02:00
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 . """
2026-06-17 10:18:55 +02:00
) )
2026-06-16 11:27:41 +02:00
try :
2026-06-17 10:18:55 +02:00
project_spec = await structured_llm . ainvoke ( messages )
logger . info ( f " [PM Agent] ✅ Tentative réussie (Structured Output) " )
2026-06-16 11:27:41 +02:00
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 } " )
2026-06-12 18:16:58 +02:00
return ProjectSpec (
2026-06-16 11:27:41 +02:00
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 ? "
2026-06-12 18:16:58 +02:00
)