Etape 4 finie, TODO : correction QA, gestion retour au PM après refus utilisateur
This commit is contained in:
@@ -88,7 +88,8 @@ async def run_qa_agent(project_title: str, dev_output: dict) -> QAEvaluation:
|
||||
if qa_evaluation.is_complete_and_safe:
|
||||
logger.info(f"[QA Agent] ✅ Projet '{project_title}' VALIDÉ (Analyse statique propre).")
|
||||
else:
|
||||
logger.warning(f"[QA Agent] ❌ Projet '{project_title}' REJETÉ (Failles de sécurité détectées).")
|
||||
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
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from app.agents.pm_agent import run_pm_agent
|
||||
from app.agents.dev_agent import run_dev_agent
|
||||
from app.agents.qa_agent import run_qa_agent
|
||||
@@ -106,10 +110,59 @@ async def qa_node(state: WorkflowState):
|
||||
}
|
||||
|
||||
async def human_review_node(state: WorkflowState):
|
||||
print("[Human Review] Passage en mode automatique (Mock)...")
|
||||
"""
|
||||
Nœud pivot. Si le statut vient du QA, il bascule en attente de validation humaine.
|
||||
Si Chainlit a déjà collecté la décision, il laisse passer le flux vers le routage.
|
||||
"""
|
||||
current_status = state.get("status")
|
||||
|
||||
if current_status in ["qa_done", "existing_found"]:
|
||||
return {
|
||||
"status": "wait_human_review"
|
||||
}
|
||||
|
||||
return {"status": current_status}
|
||||
|
||||
async def delivery_node(state: WorkflowState, config: RunnableConfig):
|
||||
"""
|
||||
Nœud final de livraison : Archivage ZIP en mémoire et réindexation Qdrant.
|
||||
"""
|
||||
dev_data = state.get("generated_code", {})
|
||||
project_title = dev_data.get("spec_title", f"project_{int(time.time())}")
|
||||
|
||||
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
|
||||
|
||||
# 3. Réindexation dans Qdrant (en utilisant le payload mémoire)
|
||||
# qdrant_repo = config.get("configurable", {}).get("qdrant_repo")
|
||||
# if qdrant_repo:
|
||||
# payload_text = f"Title: {project_title}\nDescription: {state.get('spec', {}).get('description')}"
|
||||
|
||||
# # /!\ À remplacer par ton vrai modèle d'embedding (ex: await qdrant_repo.embed(payload_text))
|
||||
# dummy_vector = [0.1] * 1536
|
||||
|
||||
# qdrant_repo.client.upsert(
|
||||
# collection_name="arc_projects",
|
||||
# points=[
|
||||
# PointStruct(
|
||||
# id=str(Path(project_title).name),
|
||||
# vector=dummy_vector,
|
||||
# payload=metadata
|
||||
# )
|
||||
# ]
|
||||
# )
|
||||
|
||||
return {
|
||||
"existing_project_approved": True,
|
||||
"is_completed": True,
|
||||
"status": "approved_by_human"
|
||||
"status": "delivered",
|
||||
"user_input": f"{state.get('user_input')}\n\n[System] Projet {project_title} validé et package ZIP disponible."
|
||||
}
|
||||
@@ -10,10 +10,14 @@ from app.graph.nodes import (
|
||||
dev_node,
|
||||
qa_node,
|
||||
human_review_node,
|
||||
delivery_node
|
||||
)
|
||||
|
||||
def route_entry_point(state: WorkflowState):
|
||||
if state.get("status") == "spec_approved":
|
||||
current_status = state.get("status")
|
||||
if current_status in ["human_approved", "human_refused"]:
|
||||
return "human_review"
|
||||
if current_status == "spec_approved":
|
||||
return "retrieval"
|
||||
return "pm"
|
||||
|
||||
@@ -40,18 +44,23 @@ def route_after_qa(state: WorkflowState):
|
||||
return "human_review"
|
||||
|
||||
def route_after_human(state: WorkflowState):
|
||||
# Cas d'un projet existant proposé
|
||||
if state.get("existing_project") and not state.get("generated_code"):
|
||||
if state.get("existing_project_approved") == True:
|
||||
return END # L'utilisateur est satisfait du projet existant
|
||||
return "dev" # L'utilisateur refuse l'existant, on génère du neuf
|
||||
current_status = state.get("status")
|
||||
|
||||
# Cas du code généré
|
||||
if state.get("is_completed") == True:
|
||||
if current_status == "wait_human_review":
|
||||
return END
|
||||
|
||||
# Si l'utilisateur a refusé le code -> Retour à la case PM avec ses commentaires
|
||||
return "pm"
|
||||
if state.get("existing_project") and not state.get("generated_code"):
|
||||
if state.get("existing_project_approved") is True:
|
||||
return END
|
||||
return "dev"
|
||||
|
||||
if current_status == "human_approved":
|
||||
return "delivery"
|
||||
|
||||
if current_status == "human_refused":
|
||||
return "pm"
|
||||
|
||||
return END
|
||||
|
||||
# --- Assemblage du Graphe ---
|
||||
|
||||
@@ -62,12 +71,14 @@ graph.add_node("retrieval", retrieval_node)
|
||||
graph.add_node("dev", dev_node)
|
||||
graph.add_node("qa", qa_node)
|
||||
graph.add_node("human_review", human_review_node)
|
||||
graph.add_node("delivery", delivery_node)
|
||||
|
||||
graph.set_conditional_entry_point(
|
||||
route_entry_point,
|
||||
{
|
||||
"pm": "pm",
|
||||
"retrieval": "retrieval",
|
||||
"human_review": "human_review"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -108,7 +119,12 @@ graph.add_conditional_edges(
|
||||
{
|
||||
"pm": "pm",
|
||||
"dev": "dev",
|
||||
"delivery": "delivery",
|
||||
END: END,
|
||||
},
|
||||
)
|
||||
|
||||
# Maillon final : Après Delivery, c'est la fin du cycle
|
||||
graph.add_edge("delivery", END)
|
||||
|
||||
compiled_graph = graph.compile()
|
||||
@@ -243,6 +243,35 @@ Si tu ne comprends pas un champ, tu le remplis avec une valeur par défaut INTEL
|
||||
```
|
||||
---
|
||||
|
||||
=== VALIDATION DE COMPLÉTUDE DE LA SPÉCIFICATION (OBLIGATOIRE) ===
|
||||
|
||||
Avant de considérer la ProjectSpec comme complète, tu dois te demander :
|
||||
|
||||
"Un développeur senior peut-il implémenter l'intégralité du projet sans devoir faire la moindre hypothèse importante ?"
|
||||
|
||||
Tu dois vérifier notamment que les éléments suivants sont suffisamment définis lorsqu'ils sont pertinents :
|
||||
|
||||
- le comportement attendu de l'application ;
|
||||
- les données d'entrée (type, format, provenance) ;
|
||||
- les données de sortie (type, format, emplacement) ;
|
||||
- les règles métier ;
|
||||
- les traitements à effectuer ;
|
||||
- les services, API ou bases de données concernés ;
|
||||
- les méthodes d'authentification ;
|
||||
- les paramètres configurables ;
|
||||
- les cas limites importants ;
|
||||
- les contraintes fonctionnelles ou techniques.
|
||||
|
||||
Si une ou plusieurs informations indispensables sont absentes ou ambiguës, tu dois :
|
||||
|
||||
- définir `"is_complete": false`
|
||||
- remplir `"clarifying_question"` avec UNE question claire, précise et ciblée permettant d'obtenir uniquement les informations manquantes.
|
||||
- ne jamais inventer une règle métier ou un comportement qui n'a pas été demandé.
|
||||
|
||||
Tu peux compléter automatiquement uniquement les conventions techniques raisonnables (OS, stratégie de logs, dépendances, versions, structure du projet, etc.), mais jamais les besoins fonctionnels.
|
||||
|
||||
---
|
||||
|
||||
=== DERNIER CHECKPOINT ===
|
||||
|
||||
Avant de retourner le JSON :
|
||||
@@ -283,6 +312,7 @@ Votre objectif est de générer l'intégralité du code source d'un projet basé
|
||||
Tu dois impérativement identifier le langage cible défini dans la `ProjectSpec` (champ `"language"`). Tu as l'interdiction stricte d'utiliser un autre langage.
|
||||
- **Extensions :** Adapte l'extension du point d'entrée et des tests (.rb pour Ruby, .py pour Python, .js pour JS, .go pour Go, etc.).
|
||||
- **Gestionnaire de dépendances :** Utilise le fichier standard du langage (ex: `Gemfile` pour Ruby, `requirements.txt` ou `pyproject.toml` pour Python, `package.json` pour Node.js).
|
||||
- **Dépendances :** N'ajoute dans le gestionnaire de dépendances (ex: `requirements.txt`, `Gemfile`, `package.json`, etc.) que les bibliothèques externes réellement nécessaires. Il est strictement interdit d'ajouter des modules ou bibliothèques faisant déjà partie de la bibliothèque standard (standard library) du langage ciblé (ex: `logging`, `json`, `os`, `pathlib`, `datetime`, `typing` en Python, etc.).
|
||||
- **Commandes du README :** Les instructions d'installation et d'exécution doivent correspondre EXACTEMENT au langage (ex: `bundle install` et `ruby main.rb` pour Ruby).
|
||||
|
||||
### DIRECTIVES D'ARCHITECTURE (ADAPTATIVE) :
|
||||
@@ -316,6 +346,16 @@ Tu dois appliquer STRICTEMENT l'une des deux structures suivantes selon des crit
|
||||
- **Découplage :** Évite les dépendances trop fortes entre les modules pour permettre une évolution facile du code.
|
||||
- Utilise des **Design Patterns** reconnus (Factory, Singleton, Strategy, etc.) si la complexité du projet le justifie.
|
||||
|
||||
4. DOCUMENTATION DU CODE (OBLIGATOIRE) :
|
||||
- Chaque fichier source doit commencer par un commentaire décrivant clairement son rôle.
|
||||
- Chaque classe doit posséder un commentaire ou une documentation expliquant sa responsabilité.
|
||||
- Chaque fonction ou méthode doit être documentée avec un commentaire ou une documentation décrivant :
|
||||
- son objectif ;
|
||||
- les paramètres attendus ;
|
||||
- la valeur de retour (si applicable) ;
|
||||
- les éventuelles exceptions levées lorsque le langage le permet.
|
||||
- Les commentaires doivent utiliser le format idiomatique du langage ciblé (ex: docstrings Python, JSDoc pour JavaScript/TypeScript, YARD pour Ruby, GoDoc pour Go, JavaDoc pour Java, XML Documentation pour C#, etc.).
|
||||
|
||||
---
|
||||
|
||||
### STANDARDS DE CONCEPTION IMPOSÉS :
|
||||
@@ -341,7 +381,7 @@ Tu dois appliquer STRICTEMENT l'une des deux structures suivantes selon des crit
|
||||
|
||||
---
|
||||
|
||||
## CONTRÂINTE DE MODERNITÉ ET SÉCURITÉ DU CODE (UNIVERSEL)
|
||||
## CONTRAINTE DE MODERNITÉ ET SÉCURITÉ DU CODE (UNIVERSEL)
|
||||
|
||||
Peu importe le langage de programmation choisi pour répondre à la spécification, tu dois appliquer les règles strictes suivantes :
|
||||
|
||||
|
||||
@@ -1,81 +1,36 @@
|
||||
import chainlit as cl
|
||||
import asyncio
|
||||
import zipfile
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
import io
|
||||
|
||||
# --- FONCTION DE RENDU DE L'UI SELON LE STATUT ---
|
||||
async def render_workflow_state(new_state: dict):
|
||||
"""
|
||||
Fonction centrale pour aiguiller l'affichage Chainlit
|
||||
en fonction du statut renvoyé par LangGraph.
|
||||
"""
|
||||
current_status = new_state.get("status")
|
||||
|
||||
@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()
|
||||
|
||||
|
||||
@cl.on_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:
|
||||
response = await client.post(
|
||||
"http://127.0.0.1:8000/api/workflow/run",
|
||||
json=state,
|
||||
timeout=600.0
|
||||
)
|
||||
|
||||
# 4. Enregistrer le nouvel état retourné par le serveur
|
||||
new_state = response.json()
|
||||
cl.user_session.set("graph_state", new_state)
|
||||
|
||||
# 5. Rendu UI intelligent dans Chainlit
|
||||
if new_state.get("status") == "spec_incomplete":
|
||||
if current_status == "spec_incomplete":
|
||||
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":
|
||||
|
||||
elif current_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"- **Nom du projet** : {spec.get('title')}\n"
|
||||
summary += f"- **Description** : {spec.get('description')}\n"
|
||||
summary += "- **Actions** :\n"
|
||||
summary += "\n".join(
|
||||
f" - {req}" for req in spec.get("requirements", [])
|
||||
) + "\n"
|
||||
summary += "\n".join(f" - {req}" for req in spec.get("requirements", [])) + "\n"
|
||||
summary += "- **Contraintes** :\n"
|
||||
summary += "\n".join(
|
||||
f" - {constraint}" for constraint in spec.get("constraints", [])
|
||||
) + "\n"
|
||||
summary+= f"- **Langage** : {spec.get('language')}\n"
|
||||
|
||||
summary += "\n**Est-ce que cela vous convient ?**"
|
||||
summary += "\n".join(f" - {constraint}" for constraint in spec.get("constraints", [])) + "\n"
|
||||
summary += f"- **Langage** : {spec.get('language')}\n\n"
|
||||
summary += "**Est-ce que cela vous convient ?**"
|
||||
|
||||
res = await cl.AskActionMessage(
|
||||
content=summary,
|
||||
@@ -107,9 +62,8 @@ async def on_message(message: cl.Message):
|
||||
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()
|
||||
# Rappel de la fonction pour traiter le nouvel état de review
|
||||
await render_workflow_state(final_state)
|
||||
|
||||
else:
|
||||
new_state["status"] = "spec_incomplete"
|
||||
@@ -119,7 +73,197 @@ async def on_message(message: 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()
|
||||
|
||||
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"
|
||||
|
||||
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()
|
||||
|
||||
# 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}"
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# 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"
|
||||
|
||||
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)
|
||||
|
||||
final_state = resp.json()
|
||||
cl.user_session.set("graph_state", final_state)
|
||||
|
||||
elif res and res.get("name") == "refuser_projet":
|
||||
await cl.Message(content="❌ **Projet refusé.**").send()
|
||||
feedback_user = await cl.AskUserMessage(
|
||||
content="📝 Veuillez décrire les corrections ou les modifications à apporter au projet :",
|
||||
timeout=600
|
||||
).send()
|
||||
|
||||
if feedback_user:
|
||||
new_state["status"] = "human_refused"
|
||||
new_state["user_feedback"] = feedback_user["output"]
|
||||
new_state["status"] = "spec_incomplete"
|
||||
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)
|
||||
|
||||
if loop_state.get("status") == "spec_incomplete":
|
||||
question = loop_state.get("spec", {}).get("clarifying_question")
|
||||
await cl.Message(content=f"**ARC a analysé vos retours mais a besoin d'une précision :**\n\n{question}").send()
|
||||
|
||||
elif current_status == "delivered":
|
||||
await cl.Message(content="✅ Ce projet a déjà été traité et livré.").send()
|
||||
else:
|
||||
await cl.Message(
|
||||
content=f"Résultat workflow :\n```json\n{json.dumps(new_state, indent=2, ensure_ascii=False)}\n```"
|
||||
content=f"Résultat du traitement :\n```json\n{json.dumps(new_state, indent=2, ensure_ascii=False)}\n```"
|
||||
).send()
|
||||
|
||||
|
||||
# --- ÉVÉNEMENTS CHAINLIT STANDARDS ---
|
||||
@cl.action_callback("open_file")
|
||||
async def on_open_file(action: cl.Action):
|
||||
filename = action.payload.get("filename")
|
||||
files = cl.user_session.get("current_files", {})
|
||||
content = files.get(filename, "")
|
||||
|
||||
ext = filename.split(".")[-1] if "." in filename else "text"
|
||||
is_md = ext in ("md", "markdown")
|
||||
|
||||
files_message = cl.user_session.get("files_message")
|
||||
message_id = files_message.id if files_message else None
|
||||
|
||||
old_viewer = cl.user_session.get("side_code_viewer")
|
||||
if old_viewer:
|
||||
await old_viewer.remove()
|
||||
|
||||
new_viewer = cl.Text(
|
||||
name=filename,
|
||||
content=content,
|
||||
language=None if is_md else ext,
|
||||
display="side",
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@cl.on_message
|
||||
async def on_message(message: cl.Message):
|
||||
state = cl.user_session.get("graph_state")
|
||||
|
||||
if "chat_history" not in state:
|
||||
state["chat_history"] = []
|
||||
if "status" not in state:
|
||||
state["status"] = "start"
|
||||
|
||||
if state.get("status") == "spec_incomplete":
|
||||
state["user_feedback"] = message.content
|
||||
else:
|
||||
state["user_input"] = message.content
|
||||
state["user_feedback"] = None
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
"http://127.0.0.1:8000/api/workflow/run",
|
||||
json=state,
|
||||
timeout=600.0
|
||||
)
|
||||
|
||||
new_state = response.json()
|
||||
cl.user_session.set("graph_state", new_state)
|
||||
|
||||
# Appel du rendu
|
||||
await render_workflow_state(new_state)
|
||||
Reference in New Issue
Block a user