first push
This commit is contained in:
56
backend/app/graph/nodes.py
Normal file
56
backend/app/graph/nodes.py
Normal file
@@ -0,0 +1,56 @@
|
||||
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
|
||||
from app.services.retrieval_service import find_existing_project
|
||||
from app.graph.state import WorkflowState
|
||||
|
||||
async def pm_node(state: WorkflowState):
|
||||
prompt = state["user_input"]
|
||||
if state.get("user_feedback"):
|
||||
prompt += f"\nRetour utilisateur pour correction : {state['user_feedback']}"
|
||||
|
||||
spec = await run_pm_agent(prompt)
|
||||
return {
|
||||
"spec": spec.model_dump(),
|
||||
"status": "spec_ready",
|
||||
"loop_count": 0,
|
||||
}
|
||||
|
||||
async def retrieval_node(state: WorkflowState):
|
||||
existing_project = await find_existing_project(state["user_input"])
|
||||
return {
|
||||
"existing_project": existing_project,
|
||||
"status": "existing_found" if existing_project else "no_existing_project",
|
||||
}
|
||||
|
||||
async def dev_node(state: WorkflowState):
|
||||
qa_logs = state.get("qa_result", {}).get("logs", "") if state.get("qa_result") else None
|
||||
|
||||
generated_code = await run_dev_agent(state["spec"], qa_feedback=qa_logs)
|
||||
return {
|
||||
"generated_code": generated_code,
|
||||
"status": "code_generated",
|
||||
}
|
||||
|
||||
async def qa_node(state: WorkflowState):
|
||||
qa_result = await run_qa_agent(state["generated_code"])
|
||||
current_loops = state.get("loop_count", 0)
|
||||
|
||||
is_success = True
|
||||
|
||||
clean_qa_result = {"success": is_success, "raw": qa_result}
|
||||
|
||||
return {
|
||||
"qa_result": clean_qa_result,
|
||||
"loop_count": current_loops if is_success else current_loops + 1,
|
||||
"status": "qa_done",
|
||||
}
|
||||
|
||||
async def human_review_node(state: WorkflowState):
|
||||
print("[Human Review] Passage en mode automatique (Mock)...")
|
||||
|
||||
return {
|
||||
"existing_project_approved": True,
|
||||
"is_completed": True,
|
||||
"status": "approved_by_human"
|
||||
}
|
||||
13
backend/app/graph/state.py
Normal file
13
backend/app/graph/state.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import TypedDict, Optional, Any, Dict
|
||||
|
||||
class WorkflowState(TypedDict, total=False):
|
||||
user_input: str
|
||||
spec: dict
|
||||
existing_project: Optional[dict]
|
||||
existing_project_approved: Optional[bool] # Choix utilisateur si projet similaire trouvé
|
||||
generated_code: Optional[Dict[str, str]] # Arborescence et code
|
||||
qa_result: Optional[dict] # Contient les clés 'success' et 'logs'
|
||||
loop_count: int # Compteur pour la Loop 1 (Dev <-> QA)
|
||||
user_feedback: Optional[str] # Retours si l'utilisateur refuse le code final
|
||||
is_completed: bool # Statut de livraison finale
|
||||
status: str
|
||||
92
backend/app/graph/workflow.py
Normal file
92
backend/app/graph/workflow.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import warnings
|
||||
from langchain_core._api.deprecation import LangChainPendingDeprecationWarning
|
||||
warnings.filterwarnings("ignore", category=LangChainPendingDeprecationWarning)
|
||||
|
||||
from langgraph.graph import StateGraph, END
|
||||
from app.graph.state import WorkflowState
|
||||
from app.graph.nodes import (
|
||||
pm_node,
|
||||
retrieval_node,
|
||||
dev_node,
|
||||
qa_node,
|
||||
human_review_node,
|
||||
)
|
||||
|
||||
# --- Fonctions de Routage (Conditional Edges) ---
|
||||
|
||||
def route_after_retrieval(state: WorkflowState):
|
||||
# Si un projet existe, on demande d'abord à l'humain (via le nœud de review)
|
||||
if state.get("existing_project"):
|
||||
return "human_review"
|
||||
return "dev"
|
||||
|
||||
def route_after_qa(state: WorkflowState):
|
||||
qa_res = state.get("qa_result", {})
|
||||
|
||||
# Loop 1 : Si échec des tests ET qu'on a pas dépassé 3 essais -> On renvoie chez le Dev
|
||||
if not qa_res.get("success") and state.get("loop_count", 0) < 3:
|
||||
return "dev"
|
||||
|
||||
# Si c'est vert (ou trop d'échecs), on présente le résultat à l'utilisateur
|
||||
# EXTENSION FUTURE : si trop d'échecs, on pourrait envoyer à une IA plus puissante
|
||||
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
|
||||
|
||||
# Cas du code généré
|
||||
if state.get("is_completed") == True:
|
||||
return END
|
||||
|
||||
# Si l'utilisateur a refusé le code -> Retour à la case PM avec ses commentaires
|
||||
return "pm"
|
||||
|
||||
# --- Assemblage du Graphe ---
|
||||
|
||||
graph = StateGraph(WorkflowState)
|
||||
|
||||
graph.add_node("pm", pm_node)
|
||||
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.set_entry_point("pm")
|
||||
graph.add_edge("pm", "retrieval")
|
||||
|
||||
# Étape 1 : Choix après recherche vectorielle
|
||||
graph.add_conditional_edges(
|
||||
"retrieval",
|
||||
route_after_retrieval,
|
||||
{
|
||||
"dev": "dev",
|
||||
"human_review": "human_review",
|
||||
},
|
||||
)
|
||||
|
||||
# Étape 2 & 3 : Boucle Dev <-> QA (Loop 1)
|
||||
graph.add_edge("dev", "qa")
|
||||
graph.add_conditional_edges(
|
||||
"qa",
|
||||
route_after_qa,
|
||||
{
|
||||
"dev": "dev",
|
||||
"human_review": "human_review",
|
||||
},
|
||||
)
|
||||
|
||||
# Étape 4 : Boucle de Feedback Humain (Loop 2) ou Clôture
|
||||
graph.add_conditional_edges(
|
||||
"human_review",
|
||||
route_after_human,
|
||||
{
|
||||
"pm": "pm",
|
||||
"dev": "dev",
|
||||
END: END,
|
||||
},
|
||||
)
|
||||
compiled_graph = graph.compile()
|
||||
Reference in New Issue
Block a user