Etape 1 OK, sans optionnel
This commit is contained in:
@@ -1,15 +1,179 @@
|
|||||||
|
#pm_agent.py
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
from app.schemas.spec import ProjectSpec
|
from app.schemas.spec import ProjectSpec
|
||||||
|
from app.llm.prompts import PM_AGENT_PROMPT
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
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",
|
||||||
|
temperature=0.3,
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "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.
|
||||||
|
|
||||||
|
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 = _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)
|
||||||
|
|
||||||
|
|
||||||
async def run_pm_agent(user_input: str) -> ProjectSpec:
|
def _extract_json_robust(response_text: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Agent PM minimal :
|
Extrait et parse le JSON depuis la réponse du LLM.
|
||||||
- transforme l'entrée utilisateur en cahier des charges structuré
|
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.
|
||||||
|
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(
|
return ProjectSpec(
|
||||||
title="Projet généré depuis demande utilisateur",
|
title="Automation Script",
|
||||||
description=user_input,
|
description=f"Automatisation du besoin utilisateur: {user_input}",
|
||||||
requirements=["MVP minimal", "Architecture modulaire"],
|
requirements=[
|
||||||
constraints=["Python", "LangGraph", "Pydantic"],
|
f"Exécuter l'action demandée: {user_input}",
|
||||||
target_stack="Python",
|
"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 ?"
|
||||||
)
|
)
|
||||||
@@ -7,5 +7,6 @@ router = APIRouter(tags=["workflow"])
|
|||||||
|
|
||||||
@router.post("/workflow/run", response_model=WorkflowResponse)
|
@router.post("/workflow/run", response_model=WorkflowResponse)
|
||||||
async def run_workflow(payload: WorkflowRequest):
|
async def run_workflow(payload: WorkflowRequest):
|
||||||
result = await run_arc_workflow(payload.user_input)
|
result = await run_arc_workflow(payload.model_dump())
|
||||||
|
|
||||||
return WorkflowResponse(**result)
|
return WorkflowResponse(**result)
|
||||||
@@ -1,18 +1,34 @@
|
|||||||
|
#nodes.py
|
||||||
from app.agents.pm_agent import run_pm_agent
|
from app.agents.pm_agent import run_pm_agent
|
||||||
from app.agents.dev_agent import run_dev_agent
|
from app.agents.dev_agent import run_dev_agent
|
||||||
from app.agents.qa_agent import run_qa_agent
|
from app.agents.qa_agent import run_qa_agent
|
||||||
from app.services.retrieval_service import find_existing_project
|
from app.services.retrieval_service import find_existing_project
|
||||||
from app.graph.state import WorkflowState
|
from app.graph.state import WorkflowState
|
||||||
|
|
||||||
async def pm_node(state: WorkflowState):
|
async def pm_node(state: WorkflowState):
|
||||||
prompt = state["user_input"]
|
history = state.get("chat_history", []) or []
|
||||||
if state.get("user_feedback"):
|
|
||||||
prompt += f"\nRetour utilisateur pour correction : {state['user_feedback']}"
|
if state.get("status") == "spec_incomplete" and state.get("user_feedback"):
|
||||||
|
current_input = state["user_feedback"]
|
||||||
|
full_user_input = f"{state['user_input']}\n{current_input}"
|
||||||
|
else:
|
||||||
|
current_input = state["user_input"]
|
||||||
|
full_user_input = current_input
|
||||||
|
|
||||||
|
spec = await run_pm_agent(user_input=current_input, history=history)
|
||||||
|
|
||||||
|
updated_history = list(history)
|
||||||
|
updated_history.append({"role": "user", "content": current_input})
|
||||||
|
|
||||||
|
if not spec.is_complete and spec.clarifying_question:
|
||||||
|
updated_history.append({"role": "assistant", "content": spec.clarifying_question})
|
||||||
|
|
||||||
spec = await run_pm_agent(prompt)
|
|
||||||
return {
|
return {
|
||||||
"spec": spec.model_dump(),
|
"spec": spec.model_dump(),
|
||||||
"status": "spec_ready",
|
"status": "spec_ready" if spec.is_complete else "spec_incomplete",
|
||||||
|
"chat_history": updated_history,
|
||||||
|
"user_input": full_user_input,
|
||||||
|
"user_feedback": None,
|
||||||
"loop_count": 0,
|
"loop_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,4 +69,45 @@ async def human_review_node(state: WorkflowState):
|
|||||||
"existing_project_approved": True,
|
"existing_project_approved": True,
|
||||||
"is_completed": True,
|
"is_completed": True,
|
||||||
"status": "approved_by_human"
|
"status": "approved_by_human"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# def inspect_specifications_gaps(spec_dict: dict) -> list[str]:
|
||||||
|
# missing_fields = []
|
||||||
|
|
||||||
|
# # 1. Vérifications globales (clés alignées sur ProjectSpec)
|
||||||
|
# if not spec_dict.get("title"): missing_fields.append("title")
|
||||||
|
# if not spec_dict.get("description"): missing_fields.append("description")
|
||||||
|
# if not spec_dict.get("requirements"): missing_fields.append("requirements")
|
||||||
|
|
||||||
|
# # 2. Entrées/Sorties
|
||||||
|
# io = spec_dict.get("io_config", {})
|
||||||
|
# if io.get("has_inputs") is True and not io.get("input_type"):
|
||||||
|
# missing_fields.append("io_config.input_type")
|
||||||
|
# if io.get("has_outputs") is True and not io.get("output_type"):
|
||||||
|
# missing_fields.append("io_config.output_type")
|
||||||
|
|
||||||
|
# # 3. Authentification
|
||||||
|
# auth = spec_dict.get("auth_config", {})
|
||||||
|
# if auth.get("requires_auth") is True and not auth.get("auth_method"):
|
||||||
|
# missing_fields.append("auth_config.auth_method")
|
||||||
|
|
||||||
|
# return missing_fields
|
||||||
|
|
||||||
|
# def generate_clarifying_questions_prompt(missing_fields: list[str]) -> str:
|
||||||
|
# # Le mapping utilise désormais les EXACTES mêmes clés
|
||||||
|
# mapping_instructions = {
|
||||||
|
# "title": "- Préciser un titre pour le projet.",
|
||||||
|
# "description": "- Expliquer le but global du script.",
|
||||||
|
# "requirements": "- Lister les fonctionnalités attendues étape par étape.",
|
||||||
|
# "io_config.input_type": "- Préciser le type et le format des fichiers/données d'entrée (CSV, dossier, etc.).",
|
||||||
|
# "io_config.output_type": "- Préciser le type et le format attendus en sortie (Excel, PDF, log, etc.).",
|
||||||
|
# "auth_config.auth_method": "- Clarifier la méthode d'accès ou d'authentification exigée pour l'outil tiers."
|
||||||
|
# }
|
||||||
|
|
||||||
|
# bullet_points = "\n".join([mapping_instructions[field] for field in missing_fields if field in mapping_instructions])
|
||||||
|
|
||||||
|
# return f"""
|
||||||
|
# ATTENTION : Les données suivantes sont obligatoires mais absentes.
|
||||||
|
# Vous devez formuler une question naturelle et polie pour demander à l'utilisateur de préciser :
|
||||||
|
# {bullet_points}
|
||||||
|
# """
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TypedDict, Optional, Any, Dict
|
from typing import TypedDict, Optional, Any, Dict, List
|
||||||
|
|
||||||
class WorkflowState(TypedDict, total=False):
|
class WorkflowState(TypedDict, total=False):
|
||||||
user_input: str
|
user_input: str
|
||||||
@@ -10,4 +10,5 @@ class WorkflowState(TypedDict, total=False):
|
|||||||
loop_count: int # Compteur pour la Loop 1 (Dev <-> QA)
|
loop_count: int # Compteur pour la Loop 1 (Dev <-> QA)
|
||||||
user_feedback: Optional[str] # Retours si l'utilisateur refuse le code final
|
user_feedback: Optional[str] # Retours si l'utilisateur refuse le code final
|
||||||
is_completed: bool # Statut de livraison finale
|
is_completed: bool # Statut de livraison finale
|
||||||
status: str
|
status: str
|
||||||
|
chat_history: List[Dict[str, str]]
|
||||||
@@ -12,7 +12,17 @@ from app.graph.nodes import (
|
|||||||
human_review_node,
|
human_review_node,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def route_entry_point(state: WorkflowState):
|
||||||
|
if state.get("status") == "spec_approved":
|
||||||
|
return "retrieval"
|
||||||
|
return "pm"
|
||||||
|
|
||||||
# --- Fonctions de Routage (Conditional Edges) ---
|
# --- Fonctions de Routage (Conditional Edges) ---
|
||||||
|
def route_after_pm(state: WorkflowState):
|
||||||
|
current_status = state.get("status")
|
||||||
|
if current_status in ["spec_incomplete", "spec_ready"]:
|
||||||
|
return END
|
||||||
|
return "retrieval"
|
||||||
|
|
||||||
def route_after_retrieval(state: WorkflowState):
|
def route_after_retrieval(state: WorkflowState):
|
||||||
# Si un projet existe, on demande d'abord à l'humain (via le nœud de review)
|
# Si un projet existe, on demande d'abord à l'humain (via le nœud de review)
|
||||||
@@ -55,8 +65,22 @@ graph.add_node("dev", dev_node)
|
|||||||
graph.add_node("qa", qa_node)
|
graph.add_node("qa", qa_node)
|
||||||
graph.add_node("human_review", human_review_node)
|
graph.add_node("human_review", human_review_node)
|
||||||
|
|
||||||
graph.set_entry_point("pm")
|
graph.set_conditional_entry_point(
|
||||||
graph.add_edge("pm", "retrieval")
|
route_entry_point,
|
||||||
|
{
|
||||||
|
"pm": "pm",
|
||||||
|
"retrieval": "retrieval",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
graph.add_conditional_edges(
|
||||||
|
"pm",
|
||||||
|
route_after_pm,
|
||||||
|
{
|
||||||
|
END: END,
|
||||||
|
"retrieval": "retrieval",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Étape 1 : Choix après recherche vectorielle
|
# Étape 1 : Choix après recherche vectorielle
|
||||||
graph.add_conditional_edges(
|
graph.add_conditional_edges(
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
PM_AGENT_PROMPT = """Tu es un Product Manager senior spécialisé en structuration de cahiers des charges.
|
||||||
|
|
||||||
|
=== TÂCHE UNIQUE ET EXCLUSIVE ===
|
||||||
|
Tu reçois une demande utilisateur brute.
|
||||||
|
Tu DOIS la transformer en JSON valide suivant EXACTEMENT cette structure (ProjectSpec).
|
||||||
|
|
||||||
|
⚠️ RÈGLE ABSOLUE : Tu génères UNIQUEMENT du JSON valide. Pas de texte avant, pas de texte après.
|
||||||
|
Si tu ne comprends pas un champ, tu le remplis avec une valeur par défaut INTELLIGENTE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
=== STRUCTURE JSON À RETOURNER ===
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "...",
|
||||||
|
"description": "...",
|
||||||
|
"requirements": ["...", "..."],
|
||||||
|
"constraints": ["..."],
|
||||||
|
"language": "...",
|
||||||
|
"target_stack": "...",
|
||||||
|
"io_config": {
|
||||||
|
"has_inputs": true/false,
|
||||||
|
"input_type": "file|directory|api|database|none",
|
||||||
|
"input_paths_or_sources": ["..."],
|
||||||
|
"has_outputs": true/false,
|
||||||
|
"output_type": "file|directory|database|api_response|log_only",
|
||||||
|
"output_formats": ["..."]
|
||||||
|
},
|
||||||
|
"auth_config": {
|
||||||
|
"requires_auth": true/false,
|
||||||
|
"auth_method": "env_variables|service_account_json|oauth2|api_key|none",
|
||||||
|
"target_tools_and_apis": ["..."]
|
||||||
|
},
|
||||||
|
"env_config": {
|
||||||
|
"target_os": "linux|windows|macos|cross_platform",
|
||||||
|
"python_version": "^3.11",
|
||||||
|
"critical_dependencies": ["..."]
|
||||||
|
},
|
||||||
|
"error_handling_strategy": "abort_on_error|log_and_continue|retry_policy",
|
||||||
|
"is_complete": true,
|
||||||
|
"clarifying_question": null
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
=== RÈGLES DE REMPLISSAGE STRICTES (NON-NÉGOCIABLES) ===
|
||||||
|
|
||||||
|
**1. title** :
|
||||||
|
- JAMAIS vide. Jamais null.
|
||||||
|
- Si vague: génère un titre court et clair dérivé du besoin
|
||||||
|
- Format: "Verb + Object" (ex: "CSV Data Cleaner", "API Report Generator")
|
||||||
|
- Si l'utilisateur dit "teste mon code" → "Code Test Runner"
|
||||||
|
- Si l'utilisateur dit juste "script" → "Automation Script"
|
||||||
|
|
||||||
|
**2. description** :
|
||||||
|
- JAMAIS vide. Jamais null.
|
||||||
|
- 1-2 phrases qui expliquent QUOI et POURQUOI.
|
||||||
|
- Réutilise les mots clés de la demande pour que ce soit cohérent.
|
||||||
|
- Ex: "Traite les fichiers CSV mensuels. Filtre les anomalies et génère un rapport Excel."
|
||||||
|
|
||||||
|
**3. requirements** :
|
||||||
|
- JAMAIS vide. JAMAIS une liste vide [].
|
||||||
|
- MINIMUM 3 items (même générés intelligemment).
|
||||||
|
- Format: "Action: Détail" ou "Étape N: Décrire l'action"
|
||||||
|
- Ex:
|
||||||
|
[
|
||||||
|
"Lire les données CSV depuis le dossier /input",
|
||||||
|
"Valider l'intégrité des colonnes requises",
|
||||||
|
"Générer un rapport Excel avec statistiques"
|
||||||
|
]
|
||||||
|
|
||||||
|
**4. constraints** :
|
||||||
|
- Peux être vide si pas mentionné.
|
||||||
|
- Sinon: normes de code, limitations, formats (ex: ["PEP 8 strict", "Logs rotatifs"])
|
||||||
|
|
||||||
|
**5. language** :
|
||||||
|
- JAMAIS vide.
|
||||||
|
- Par défaut: "Python"
|
||||||
|
- Sauf si l'utilisateur dit "Node.js", "Go", etc.
|
||||||
|
|
||||||
|
**6. target_stack** :
|
||||||
|
- Peux être vide
|
||||||
|
- Sauf si l'utilisateur dit "FastAPI", "Django", etc.
|
||||||
|
|
||||||
|
**7. io_config.has_inputs** :
|
||||||
|
- TRUE si l'utilisateur mentionne : "fichier", "dossier", "données", "lire", "importer", "télécharger", "API", "base de données", "table"
|
||||||
|
- FALSE sinon.
|
||||||
|
- ⚠️ Si TRUE → input_type DOIT être rempli (pas null)
|
||||||
|
|
||||||
|
**8. io_config.input_type** :
|
||||||
|
- DÉDUIS du contexte :
|
||||||
|
- "fichier CSV/Excel/JSON/PDF/TXT" → "file"
|
||||||
|
- "dossier" → "directory"
|
||||||
|
- "API REST/GraphQL/Service Web" → "api"
|
||||||
|
- "base de données/table SQL/MongoDB" → "database"
|
||||||
|
- Aucun input → "none"
|
||||||
|
- ❌ JAMAIS null si has_inputs=true
|
||||||
|
|
||||||
|
**9. io_config.has_outputs** :
|
||||||
|
- TRUE si l'utilisateur mentionne : "exporter", "générer", "créer", "sauvegarder", "envoyer", "résultat", "fichier", "rapport"
|
||||||
|
- FALSE sinon.
|
||||||
|
|
||||||
|
**10. io_config.output_type** :
|
||||||
|
- DÉDUIS du contexte :
|
||||||
|
- "fichier Excel/CSV/PDF/JSON" → "file"
|
||||||
|
- "dossier de résultats" → "directory"
|
||||||
|
- "base de données" → "database"
|
||||||
|
- "appel API/réponse HTTP" → "api_response"
|
||||||
|
- "juste des logs/console" → "log_only"
|
||||||
|
- ❌ JAMAIS null si has_outputs=true
|
||||||
|
|
||||||
|
**11. io_config.output_formats** :
|
||||||
|
- Formats de sortie mentionnés (ex: ["CSV", "JSON", "XLSX", "PDF"])
|
||||||
|
- Peux être vide si has_outputs=false
|
||||||
|
|
||||||
|
**12. auth_config.requires_auth** :
|
||||||
|
- TRUE si l'utilisateur mentionne : "API", "token", "key", "clé", "authentification", "compte", "service account", "OAuth", "login", "credential", "Jira", "SharePoint", "GCP", "AWS", "Azure", "base de données"
|
||||||
|
- FALSE sinon.
|
||||||
|
|
||||||
|
**13. auth_config.auth_method** :
|
||||||
|
- DÉDUIS du contexte :
|
||||||
|
- "variables d'environnement/.env" → "env_variables"
|
||||||
|
- "fichier .json avec credentials" → "service_account_json"
|
||||||
|
- "OAuth/OpenID" → "oauth2"
|
||||||
|
- "API key/token" → "api_key"
|
||||||
|
- Pas d'auth → "none"
|
||||||
|
- ⚠️ Si requires_auth=true → DÉDUIS intelligemment, jamais null
|
||||||
|
|
||||||
|
**14. auth_config.target_tools_and_apis** :
|
||||||
|
- Liste les services externes (ex: ["Jira API", "Google Drive", "AWS S3"])
|
||||||
|
- Peux être vide si requires_auth=false
|
||||||
|
|
||||||
|
**15. env_config.target_os** :
|
||||||
|
- "cross_platform" par défaut
|
||||||
|
- Sauf si mentionné : "linux", "windows", "macos"
|
||||||
|
|
||||||
|
**16. env_config.language_version** :
|
||||||
|
- "^3.11" par défaut si Python
|
||||||
|
- Sauf si la version exacte est mentionnée par l'utilisateur (ex: "Python 3.12", "Node.js 18", "Go 1.20")
|
||||||
|
- Sinon vide
|
||||||
|
|
||||||
|
**17. env_config.critical_dependencies** :
|
||||||
|
- DÉDUIS du contexte :
|
||||||
|
- Données tabulaires → "pandas"
|
||||||
|
- API REST → "requests"
|
||||||
|
- Configuration → "pydantic"
|
||||||
|
- Base de données → "sqlalchemy", "pymongo"
|
||||||
|
- Fichiers → "openpyxl", "python-docx", "PyPDF2"
|
||||||
|
- Parallelisation → "asyncio", "concurrent.futures"
|
||||||
|
- Toujours inclure: ["logging"] (natif mais important)
|
||||||
|
|
||||||
|
**18. error_handling_strategy** :
|
||||||
|
- JAMAIS vide. JAMAIS null.
|
||||||
|
- "log_and_continue" par défaut (robuste)
|
||||||
|
- "abort_on_error" si critique
|
||||||
|
- "retry_policy" si mentionné "retry", "robustesse", "résilience"
|
||||||
|
|
||||||
|
**19. is_complete** :
|
||||||
|
- TRUE si tu as pu remplir tous les champs principaux (title, description, requirements) sans ambiguïté
|
||||||
|
- FALSE UNIQUEMENT si quelque chose est vraiment manquant et ambigu
|
||||||
|
|
||||||
|
**20. clarifying_question** :
|
||||||
|
- null si is_complete=true
|
||||||
|
- Sinon: une question naturelle et polie pour l'utilisateur
|
||||||
|
- Ex: "Quel format de fichier en entrée (CSV, Excel, JSON) et où souhaitez-vous la sortie ?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
=== EXEMPLES CONCRETS ===
|
||||||
|
|
||||||
|
### Input 1: "Je veux un script en Ruby qui lit un CSV et l'exporte en Excel"
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "CSV to Excel Converter",
|
||||||
|
"description": "Lit un fichier CSV, valide les données et exporte le résultat en format Excel (.xlsx).",
|
||||||
|
"requirements": [
|
||||||
|
"Lire le fichier CSV depuis le dossier source",
|
||||||
|
"Valider les colonnes requises",
|
||||||
|
"Exporter en fichier Excel avec formatage"
|
||||||
|
],
|
||||||
|
"constraints": ["PEP 8 strict"],
|
||||||
|
"language": "Ruby",
|
||||||
|
"target_stack": "",
|
||||||
|
"io_config": {
|
||||||
|
"has_inputs": true,
|
||||||
|
"input_type": "file",
|
||||||
|
"input_paths_or_sources": ["input.csv"],
|
||||||
|
"has_outputs": true,
|
||||||
|
"output_type": "file",
|
||||||
|
"output_formats": ["XLSX"]
|
||||||
|
},
|
||||||
|
"auth_config": {
|
||||||
|
"requires_auth": false,
|
||||||
|
"auth_method": "none",
|
||||||
|
"target_tools_and_apis": []
|
||||||
|
},
|
||||||
|
"env_config": {
|
||||||
|
"target_os": "cross_platform",
|
||||||
|
"language_version": "3.2.1O",
|
||||||
|
"critical_dependencies": ["pandas", "openpyxl"]
|
||||||
|
},
|
||||||
|
"error_handling_strategy": "log_and_continue",
|
||||||
|
"is_complete": true,
|
||||||
|
"clarifying_question": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Input 2: "Je dois récupérer des données sur une API Zabbix et les stocker en Python 3.12"
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "API Data Fetcher",
|
||||||
|
"description": "Récupère les données depuis une API, les valide et les stocke en base de données.",
|
||||||
|
"requirements": [
|
||||||
|
"Appeler l'API avec authentification",
|
||||||
|
"Parser les données JSON",
|
||||||
|
"Insérer ou mettre à jour les enregistrements en base de données"
|
||||||
|
],
|
||||||
|
"constraints": [],
|
||||||
|
"language": "Python",
|
||||||
|
"target_stack": "Zabbix API",
|
||||||
|
"io_config": {
|
||||||
|
"has_inputs": true,
|
||||||
|
"input_type": "api",
|
||||||
|
"input_paths_or_sources": ["https://api.example.com/data"],
|
||||||
|
"has_outputs": true,
|
||||||
|
"output_type": "database",
|
||||||
|
"output_formats": ["SQL", "JSON"]
|
||||||
|
},
|
||||||
|
"auth_config": {
|
||||||
|
"requires_auth": true,
|
||||||
|
"auth_method": "api_key",
|
||||||
|
"target_tools_and_apis": ["REST API"]
|
||||||
|
},
|
||||||
|
"env_config": {
|
||||||
|
"target_os": "cross_platform",
|
||||||
|
"language_version": "3.12",
|
||||||
|
"critical_dependencies": ["requests", "sqlalchemy", "pydantic"]
|
||||||
|
},
|
||||||
|
"error_handling_strategy": "retry_policy",
|
||||||
|
"is_complete": true,
|
||||||
|
"clarifying_question": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
=== DERNIER CHECKPOINT ===
|
||||||
|
|
||||||
|
Avant de retourner le JSON :
|
||||||
|
1. ✅ title rempli ? (pas vide, pas null)
|
||||||
|
2. ✅ description rempli ? (pas vide, pas null)
|
||||||
|
3. ✅ requirements rempli ? (pas vide, 3+ items)
|
||||||
|
4. ✅ target_stack rempli ? (pas vide, pas null)
|
||||||
|
5. ✅ error_handling_strategy rempli ? (pas vide, pas null)
|
||||||
|
6. ✅ Si has_inputs=true → input_type rempli ? (pas null)
|
||||||
|
7. ✅ Si has_outputs=true → output_type rempli ? (pas null)
|
||||||
|
8. ✅ Si requires_auth=true → auth_method rempli ? (pas null, pas "none")
|
||||||
|
|
||||||
|
Si tout est ✅, tu retournes le JSON.
|
||||||
|
Si un champ fail, tu génères une VALEUR PAR DÉFAUT INTELLIGENTE et marque is_complete=false.
|
||||||
|
|
||||||
|
CRITICAL: Si la demande de l'utilisateur est trop courte, trop vague (ex: 'Je veux un script Ruby'), ou manque d'objectifs clairs, tu DOIS impérativement définir is_complete à false et formuler une question précise dans clarifying_question pour lui demander ce que le script doit faire concrètement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
=== MAINTENANT, ANALYSE ET GÉNÈRE LE JSON ===
|
||||||
|
|
||||||
|
Ci-dessous la demande utilisateur brute. Tu dois transformer cela en JSON ProjectSpec valide, rempli et cohérent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# DEV_AGENT_PROMPT = """Vous êtes un Product Manager & Architecte Logiciel Senior spécialisé dans la conception d'outils d'automatisation et de scripts robustes en Python.
|
||||||
|
|
||||||
|
# Votre objectif est double :
|
||||||
|
# 1. Agir comme garde-fou technique en imposant des standards de développement d'entreprise drastiques.
|
||||||
|
# 2. Fournir un code Python prêt à l'emploi, structuré et commenté, accompagné d'un fichier 'requirements.txt' et d'un 'README.md' de qualité professionnelle.
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# ### DIRECTIVES DE CONCEPTION ET STANDARDS IMPOSÉS :
|
||||||
|
# Vous devez concevoir la solution technique en respectant les règles suivantes :
|
||||||
|
|
||||||
|
# 1. ARCHITECTURE ET FORMAT DES LIVRABLES :
|
||||||
|
# Le projet devra obligatoirement être constitué de 3 briques :
|
||||||
|
# - Le code source applicatif structuré et modulaire respectant les besoins indiqué par le PM Agent.
|
||||||
|
# - Un fichier 'requirements.txt' listant l'intégralité des dépendances avec leurs versions fixées.
|
||||||
|
# - Un fichier 'README.md' d'une qualité professionnelle exemplaire.
|
||||||
|
|
||||||
|
# 2. STRUCTURE ET NORMES DU CODE PYTHON :
|
||||||
|
# - Convention de nommage : Respect strict de la PEP 8. Fonctions, variables et scripts en snake_case (ex: 'file_processor.py'). Classes en PascalCase.
|
||||||
|
# - Modularité : Pas de script monolithique géant. Séparation claire entre la configuration (chargée via variables d'environnement), la logique métier (fonctions principales) et les connecteurs d'I/O ou d'API.
|
||||||
|
# - Sécurité : Interdiction formelle d'écrire des clés d'API, des tokens ou des identifiants en dur. Utilisation obligatoire du module 'os' ou de 'pydantic-settings' pour lire l'environnement.
|
||||||
|
# - Robustesse : Utilisation systématique de blocs 'try/except' ciblés avec un module de 'logging' Python natif (pas de simples 'print').
|
||||||
|
|
||||||
|
# 3. STRUCTURE ATTENDUE DU README.md :
|
||||||
|
# Le fichier d'accompagnement devra obligatoirement comporter les sections suivantes :
|
||||||
|
# - # [Titre du Projet] (Rappel de l'objectif macro).
|
||||||
|
# - ## Prérequis (Version de Python, outils tiers requis).
|
||||||
|
# - ## Installation (Création de l'environnement virtuel, installation des requirements).
|
||||||
|
# - ## Configuration (Exemple de fichier .env avec les variables nécessaires à créer).
|
||||||
|
# - ## Utilisation (Explication textuelle et exemples de commandes CLI pour lancer le script).
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# ### PROCESSUS DE TRAVAIL ET SÉCURITÉ DES DONNÉES :
|
||||||
|
# - Développer à partir des informations fournies par l'Agent PM.
|
||||||
|
# """
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any, List, Dict
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRequest(BaseModel):
|
class WorkflowRequest(BaseModel):
|
||||||
user_input: str
|
user_input: str
|
||||||
|
user_feedback: Optional[str] = None
|
||||||
|
chat_history: Optional[List[Dict[str, str]]] = []
|
||||||
|
spec: Optional[dict] = {}
|
||||||
|
status: Optional[str] = "start"
|
||||||
|
loop_count: Optional[int] = 0
|
||||||
|
existing_project: Optional[Any] = None
|
||||||
|
generated_code: Optional[Any] = None
|
||||||
|
qa_result: Optional[Any] = None
|
||||||
|
is_completed: Optional[bool] = False
|
||||||
|
|
||||||
class WorkflowResponse(BaseModel):
|
class WorkflowResponse(BaseModel):
|
||||||
|
user_input: str
|
||||||
|
user_feedback: Optional[str] = None
|
||||||
|
chat_history: Optional[List[Dict[str, str]]] = []
|
||||||
|
spec: dict
|
||||||
status: str
|
status: str
|
||||||
spec: Optional[dict] = None
|
loop_count: int
|
||||||
existing_project: Optional[dict] = None
|
existing_project: Optional[Any] = None
|
||||||
generated_code: Optional[Any] = None
|
generated_code: Optional[Any] = None
|
||||||
qa_result: Optional[Any] = None
|
qa_result: Optional[Any] = None
|
||||||
|
is_completed: bool
|
||||||
@@ -1,10 +1,55 @@
|
|||||||
from pydantic import BaseModel, Field
|
#spec.py
|
||||||
from typing import List, Optional
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
from typing import List, Optional, Literal
|
||||||
|
|
||||||
|
class InputOutputConfig(BaseModel):
|
||||||
|
has_inputs: bool = Field(default=False, description="Le script prend-il des éléments en entrée ?")
|
||||||
|
input_type: Optional[Literal["file", "directory", "api", "database", "none"]] = Field(None, description="Type d'entrée principale")
|
||||||
|
input_paths_or_sources: List[str] = Field(default_factory=list, description="Chemins, tables ou endpoints sources")
|
||||||
|
|
||||||
|
has_outputs: bool = Field(default=False, description="Le script génère-t-style des éléments en sortie ?")
|
||||||
|
output_type: Optional[Literal["file", "directory", "database", "api_response", "log_only"]] = Field(None, description="Type de sortie principale")
|
||||||
|
output_formats: List[str] = Field(default_factory=list, description="Formats attendus (ex: CSV, JSON, XLSX)")
|
||||||
|
|
||||||
|
class AuthenticationConfig(BaseModel):
|
||||||
|
requires_auth: bool = Field(default=False, description="Le script nécessite-t-il des accès sécurisés ?")
|
||||||
|
auth_method: Optional[Literal["env_variables", "service_account_json", "oauth2", "api_key", "none"]] = Field(None, description="Méthode d'authentification")
|
||||||
|
target_tools_and_apis: List[str] = Field(default_factory=list, description="Logiciels ou API avec lesquels interagir (ex: Sharepoint, JIRA)")
|
||||||
|
|
||||||
|
class EnvironmentConfig(BaseModel):
|
||||||
|
target_os: Literal["linux", "windows", "macos", "cross_platform"] = Field(default="cross_platform", description="Système d'exploitation cible")
|
||||||
|
language_version: str = Field(default="^3.11", description="Contrainte de version du langage")
|
||||||
|
critical_dependencies: List[str] = Field(default_factory=list, description="Librairies tierces indispensables")
|
||||||
|
|
||||||
class ProjectSpec(BaseModel):
|
class ProjectSpec(BaseModel):
|
||||||
title: str = Field(default="Projet ARC")
|
title: Optional[str] = Field(None, description="Nom clair et concis du script")
|
||||||
description: str
|
description: Optional[str] = Field(None, description="Description macro de l'objectif du script")
|
||||||
requirements: List[str] = Field(default_factory=list)
|
requirements: List[str] = Field(default_factory=list, description="Liste des fonctionnalités pas-à-pas attendues")
|
||||||
constraints: List[str] = Field(default_factory=list)
|
constraints: List[str] = Field(default_factory=list, description="Normes, formats de code et conventions de nommage imposés")
|
||||||
target_stack: Optional[str] = "Python"
|
language: str = Field(default="Python", description="Langage de programmation principal")
|
||||||
|
target_stack: str = Field(None, description="Stack technique ou framework attendu (ex: Pandas, FastAPI, Flask, Django, etc.)")
|
||||||
|
|
||||||
|
# Sous-configurations détaillées (permettent la détection des champs manquants)
|
||||||
|
io_config: InputOutputConfig = Field(default_factory=InputOutputConfig)
|
||||||
|
auth_config: AuthenticationConfig = Field(default_factory=AuthenticationConfig)
|
||||||
|
env_config: EnvironmentConfig = Field(default_factory=EnvironmentConfig)
|
||||||
|
|
||||||
|
error_handling_strategy: Literal["abort_on_error", "log_and_continue", "retry_policy"] = Field(
|
||||||
|
default="log_and_continue",
|
||||||
|
description="Comportement du script face à une anomalie"
|
||||||
|
)
|
||||||
|
is_complete: bool = Field(default=False, description="Passez à True UNIQUEMENT si vous avez TOUTES les infos pour coder (titre, desc, reqs, io_config, auth_config si besoin).")
|
||||||
|
clarifying_question: Optional[str] = Field(None, description="Si is_complete est False, écrivez ici une question claire et polie pour Chainlit pour demander les détails manquants.")
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_conditional_fields(self) -> 'ProjectSpec':
|
||||||
|
"""Validation des dépendances logiques pour marquer le cahier des charges comme valide."""
|
||||||
|
# Si les champs majeurs sont remplis, on valide la cohérence interne
|
||||||
|
if self.title and self.description and len(self.requirements) > 0:
|
||||||
|
if self.io_config.has_inputs and not self.io_config.input_type:
|
||||||
|
raise ValueError("input_type manquant alors que has_inputs est True")
|
||||||
|
if self.io_config.has_outputs and not self.io_config.output_type:
|
||||||
|
raise ValueError("output_type manquant alors que has_outputs est True")
|
||||||
|
if self.auth_config.requires_auth and (not self.auth_config.auth_method or self.auth_config.auth_method == "none"):
|
||||||
|
raise ValueError("auth_method manquante alors que requires_auth est True")
|
||||||
|
return self
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
from app.graph.workflow import compiled_graph
|
from app.graph.workflow import compiled_graph
|
||||||
|
|
||||||
|
async def run_arc_workflow(state_data: dict) -> dict:
|
||||||
async def run_arc_workflow(user_input: str) -> dict:
|
"""
|
||||||
result = await compiled_graph.ainvoke({"user_input": user_input})
|
Prend le state actuel (provenant de l'API/Chainlit),
|
||||||
return result
|
exécute le graphe jusqu'au prochain point d'arrêt (END),
|
||||||
|
et retourne le state mis à jour.
|
||||||
|
"""
|
||||||
|
final_state = await compiled_graph.ainvoke(state_data)
|
||||||
|
|
||||||
|
return dict(final_state)
|
||||||
@@ -5,6 +5,20 @@ import json
|
|||||||
|
|
||||||
@cl.on_chat_start
|
@cl.on_chat_start
|
||||||
async def 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(
|
await cl.Message(
|
||||||
content="Bonjour 👋 Je suis ARC. Décris-moi ton besoin logiciel."
|
content="Bonjour 👋 Je suis ARC. Décris-moi ton besoin logiciel."
|
||||||
).send()
|
).send()
|
||||||
@@ -12,14 +26,98 @@ async def on_chat_start():
|
|||||||
|
|
||||||
@cl.on_message
|
@cl.on_message
|
||||||
async def on_message(message: cl.Message):
|
async def on_message(message: cl.Message):
|
||||||
|
# 1. Récupérer le state actuel de la session
|
||||||
|
state = cl.user_session.get("graph_state")
|
||||||
|
|
||||||
|
if "chat_history" not in state:
|
||||||
|
state["chat_history"] = []
|
||||||
|
if "status" not in state:
|
||||||
|
state["status"] = "start"
|
||||||
|
|
||||||
|
# 2. Déterminer si le message est une réponse à une question ou un nouveau projet
|
||||||
|
if state.get("status") == "spec_incomplete":
|
||||||
|
state["user_feedback"] = message.content
|
||||||
|
else:
|
||||||
|
state["user_input"] = message.content
|
||||||
|
state["user_feedback"] = None
|
||||||
|
|
||||||
|
# 3. Appel de l'API en envoyant le state COMPLET
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"http://127.0.0.1:8000/api/workflow/run",
|
"http://127.0.0.1:8000/api/workflow/run",
|
||||||
json={"user_input": message.content},
|
json=state,
|
||||||
|
timeout=600.0
|
||||||
)
|
)
|
||||||
|
|
||||||
result = response.json()
|
# 4. Enregistrer le nouvel état retourné par le serveur
|
||||||
|
new_state = response.json()
|
||||||
|
cl.user_session.set("graph_state", new_state)
|
||||||
|
|
||||||
await cl.Message(
|
# 5. Rendu UI intelligent dans Chainlit
|
||||||
content=f"Résultat workflow :\n```json\n{json.dumps(result, indent=2, ensure_ascii=False)}\n```"
|
if new_state.get("status") == "spec_incomplete":
|
||||||
).send()
|
spec = new_state.get("spec", {})
|
||||||
|
question = spec.get("clarifying_question")
|
||||||
|
await cl.Message(content=f"**Spécifications incomplètes**\n\n{question}").send()
|
||||||
|
elif new_state.get("status") == "spec_ready":
|
||||||
|
spec = new_state.get("spec", {})
|
||||||
|
|
||||||
|
summary = "### Éléments importants à retenir de ton projet :\n\n"
|
||||||
|
|
||||||
|
summary+= f"- **Nom du projet** : {spec.get('title')}\n"
|
||||||
|
summary+= f"- **Description** : {spec.get('description')}\n"
|
||||||
|
summary+= f"- **Actions** : {', '.join(spec.get('requirements', []))}\n"
|
||||||
|
summary+= f"- **Contraintes** : {', '.join(spec.get('constraints', []))}\n"
|
||||||
|
summary+= f"- **Langage** : {spec.get('language')}\n"
|
||||||
|
|
||||||
|
summary += "\n**Est-ce que cela vous convient ?**"
|
||||||
|
|
||||||
|
# Ajout des boutons de décision
|
||||||
|
res = await cl.AskActionMessage(
|
||||||
|
content=summary,
|
||||||
|
actions=[
|
||||||
|
cl.Action(name="oui", payload={"value": "oui"}, label="Oui, c'est parfait 👍"),
|
||||||
|
cl.Action(name="non", payload={"value": "non"}, label="Non, modifier ❌")
|
||||||
|
],
|
||||||
|
timeout=3600
|
||||||
|
).send()
|
||||||
|
|
||||||
|
if res is None:
|
||||||
|
await cl.Message(
|
||||||
|
content="⏰ **Session expirée.** Si tu es toujours là, envoie un message pour relancer l'analyse."
|
||||||
|
).send()
|
||||||
|
return
|
||||||
|
|
||||||
|
if res and res.get("name") == "oui":
|
||||||
|
await cl.Message(content="🚀 **Spécifications validées !** Lancement de la génération du code...").send()
|
||||||
|
|
||||||
|
# On passe le statut attendu par ton graphe
|
||||||
|
new_state["status"] = "spec_approved"
|
||||||
|
cl.user_session.set("graph_state", new_state)
|
||||||
|
|
||||||
|
# On relance immédiatement le workflow pour exécuter la suite (dev, qa...)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
"http://127.0.0.1:8000/api/workflow/run",
|
||||||
|
json=new_state,
|
||||||
|
timeout=600.0
|
||||||
|
)
|
||||||
|
final_state = response.json()
|
||||||
|
cl.user_session.set("graph_state", final_state)
|
||||||
|
|
||||||
|
await cl.Message(
|
||||||
|
content=f"Résultat workflow :\n```json\n{json.dumps(final_state, indent=2, ensure_ascii=False)}\n```"
|
||||||
|
).send()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Si "non" (ou si le choix a expiré)
|
||||||
|
new_state["status"] = "spec_incomplete"
|
||||||
|
cl.user_session.set("graph_state", new_state)
|
||||||
|
|
||||||
|
await cl.Message(
|
||||||
|
content="🔄 **Compris.** Qu'est-ce qui ne convient pas ? S'il te plaît, précise les éléments manquants ou à corriger :"
|
||||||
|
).send()
|
||||||
|
|
||||||
|
else:
|
||||||
|
await cl.Message(
|
||||||
|
content=f"Résultat workflow :\n```json\n{json.dumps(new_state, indent=2, ensure_ascii=False)}\n```"
|
||||||
|
).send()
|
||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
- model_storage:/models
|
- model_storage:/models
|
||||||
ports:
|
ports:
|
||||||
- "8003:8080"
|
- "8003:8080"
|
||||||
command: "-m /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf --host 0.0.0.0 --port 8080 -c 4096"
|
command: "-m /models/gemma-4-E4B-it-UD-Q4_K_XL.gguf --host 0.0.0.0 --port 8080 -c 8192"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- arc-network
|
- arc-network
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ python-dotenv==1.0.1
|
|||||||
pytest==8.3.3
|
pytest==8.3.3
|
||||||
ruff==0.6.8
|
ruff==0.6.8
|
||||||
bandit==1.7.10
|
bandit==1.7.10
|
||||||
requests
|
requests
|
||||||
|
langchain-openai>=0.1.0
|
||||||
@@ -29,21 +29,21 @@ gantt
|
|||||||
Affichage CDC :e1, after d3, 0.5d
|
Affichage CDC :e1, after d3, 0.5d
|
||||||
Boutons validation/refus :e2, after d3, 0.5d
|
Boutons validation/refus :e2, after d3, 0.5d
|
||||||
|
|
||||||
section Qdrant (BDD vectorielle)
|
section Qdrant (BDD vectorielle) (optionnel)
|
||||||
Installer Qdrant :f1, after e2, 0.5d
|
Installer Qdrant :f1, after e2, 0.5d
|
||||||
Créer collection :f2, after e2, 0.5d
|
Créer collection :f2, after e2, 0.5d
|
||||||
Structure payload :f3, after f2, 0.5d
|
Structure payload :f3, after f2, 0.5d
|
||||||
|
|
||||||
section Embedding
|
section Embedding (optionnel)
|
||||||
Intégrer Snowflake Arctic :g1, after f2, 0.5d
|
Intégrer Snowflake Arctic :g1, after f2, 0.5d
|
||||||
Fonction embedding :g2, after f2, 0.5d
|
Fonction embedding :g2, after f2, 0.5d
|
||||||
|
|
||||||
section Recherche d'existant
|
section Recherche d'existant (optionnel)
|
||||||
Recherche projets similaires :h1, after g2, 0.5d
|
Recherche projets similaires :h1, after g2, 0.5d
|
||||||
Filtres payload :h2, after g2, 0.5d
|
Filtres payload :h2, after g2, 0.5d
|
||||||
Formatage résultats :h3, after h2, 0.5d
|
Formatage résultats :h3, after h2, 0.5d
|
||||||
|
|
||||||
section Proposition utilisateur
|
section Proposition utilisateur (optionnel)
|
||||||
Affichage résultats Chainlit :i1, after h2, 0.5d
|
Affichage résultats Chainlit :i1, after h2, 0.5d
|
||||||
Bouton "utiliser projet" :i2, after h2, 0.5d
|
Bouton "utiliser projet" :i2, after h2, 0.5d
|
||||||
Bouton "continuer génération" :i3, after h2, 0.5d
|
Bouton "continuer génération" :i3, after h2, 0.5d
|
||||||
|
|||||||
@@ -89,21 +89,21 @@ gantt
|
|||||||
Affichage CDC :e1, after d3, 0.5d
|
Affichage CDC :e1, after d3, 0.5d
|
||||||
Boutons validation/refus :e2, after d3, 0.5d
|
Boutons validation/refus :e2, after d3, 0.5d
|
||||||
|
|
||||||
section Qdrant (BDD vectorielle)
|
section Qdrant (BDD vectorielle) (optionnel)
|
||||||
Installer Qdrant :f1, after e2, 0.5d
|
Installer Qdrant :f1, after e2, 0.5d
|
||||||
Créer collection :f2, after e2, 0.5d
|
Créer collection :f2, after e2, 0.5d
|
||||||
Structure payload :f3, after f2, 0.5d
|
Structure payload :f3, after f2, 0.5d
|
||||||
|
|
||||||
section Embedding
|
section Embedding (optionnel)
|
||||||
Intégrer Snowflake Arctic :g1, after f2, 0.5d
|
Intégrer Snowflake Arctic :g1, after f2, 0.5d
|
||||||
Fonction embedding :g2, after f2, 0.5d
|
Fonction embedding :g2, after f2, 0.5d
|
||||||
|
|
||||||
section Recherche d'existant
|
section Recherche d'existant (optionnel)
|
||||||
Recherche projets similaires :h1, after g2, 0.5d
|
Recherche projets similaires :h1, after g2, 0.5d
|
||||||
Filtres payload :h2, after g2, 0.5d
|
Filtres payload :h2, after g2, 0.5d
|
||||||
Formatage résultats :h3, after h2, 0.5d
|
Formatage résultats :h3, after h2, 0.5d
|
||||||
|
|
||||||
section Proposition utilisateur
|
section Proposition utilisateur (optionnel)
|
||||||
Affichage résultats Chainlit :i1, after h2, 0.5d
|
Affichage résultats Chainlit :i1, after h2, 0.5d
|
||||||
Bouton "utiliser projet" :i2, after h2, 0.5d
|
Bouton "utiliser projet" :i2, after h2, 0.5d
|
||||||
Bouton "continuer génération" :i3, after h2, 0.5d
|
Bouton "continuer génération" :i3, after h2, 0.5d
|
||||||
|
|||||||
Reference in New Issue
Block a user