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 from app.services.retrieval_service import find_existing_project from app.graph.state import WorkflowState from langchain_core.runnables import RunnableConfig async def pm_node(state: WorkflowState): history = state.get("chat_history", []) or [] 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}) return { "spec": spec.model_dump(), "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, } async def retrieval_node(state: WorkflowState, config: RunnableConfig): qdrant_repo = config.get("configurable", {}).get("qdrant_repo") if not qdrant_repo: raise ValueError("❌ Erreur : Le repository Qdrant n'a pas été transmis au graphe.") existing_project = await find_existing_project(qdrant_repo, 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 = [] qa_result = state.get("qa_result") if qa_result: global_summary = qa_result.get("global_summary") technical_feedback = qa_result.get("technical_feedback", []) if global_summary: qa_logs.append(f"Résumé Global : {global_summary}") if isinstance(technical_feedback, list): qa_logs.extend(technical_feedback) elif technical_feedback: qa_logs.append(technical_feedback) generated_code_state = state.get("generated_code") or {} existing_repo_url = generated_code_state.get("repo_url") existing_files = generated_code_state.get("files") generated_code = await run_dev_agent( spec=state["spec"], qa_feedback=qa_logs if qa_logs else None, repo_url=existing_repo_url, files=existing_files ) return { "generated_code": generated_code, "status": "code_generated", } async def qa_node(state: WorkflowState): dev_data = state.get("generated_code", {}) project_title = dev_data.get("spec_title", "default_project") current_loops = state.get("loop_count", 0) qa_eval = await run_qa_agent( project_title=project_title, dev_output=dev_data ) if hasattr(qa_eval, "model_dump"): clean_qa_result = qa_eval.model_dump() elif isinstance(qa_eval, dict): clean_qa_result = qa_eval else: clean_qa_result = { "is_complete_and_safe": getattr(qa_eval, "is_complete_and_safe", False), "global_summary": getattr(qa_eval, "global_summary", "Erreur d'analyse"), "technical_feedback": getattr(qa_eval, "technical_feedback", []) } is_success = clean_qa_result.get("is_complete_and_safe", False) 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): """ 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 { "is_completed": True, "status": "delivered", "user_input": f"{state.get('user_input')}\n\n[System] Projet {project_title} validé et package ZIP disponible." }