| """Pipeline multi-agente em LangGraph. |
| |
| Quatro nós em sequência condicional: |
| |
| [planner] → [researcher] → [writer] → [critic] |
| ↓ |
| (revisar) ← ← ← (refazer) |
| |
| - planner: decompõe a pergunta em sub-tarefas |
| - researcher: usa tools para coletar contexto factual |
| - writer: redige resposta final usando o contexto |
| - critic: avalia se a resposta atende, decide se refaz ou aprova |
| |
| Este é o padrão "plan-and-execute com self-critique" que aparece em |
| sistemas de produção como Devin, Cursor agents, Salesforce Agentforce. |
| """ |
| import json |
| from typing import Annotated, Literal, TypedDict |
|
|
| from langgraph.graph import END, START, StateGraph |
| from langgraph.graph.message import add_messages |
|
|
| from src.config import get_product_config, get_settings |
| from src.llm import chat_completion |
| from src.tools import execute_tool, get_tools_for_product |
|
|
|
|
| |
| |
| |
| class GraphState(TypedDict): |
| """Estado que flui entre todos os nós do grafo.""" |
|
|
| user_query: str |
| plan: str |
| context: str |
| draft_answer: str |
| critique: str |
| final_answer: str |
| iteration: int |
| max_iterations: int |
| messages: Annotated[list[dict], add_messages] |
|
|
|
|
| |
| |
| |
| def planner_node(state: GraphState) -> dict: |
| """Decompõe a pergunta em 2-4 sub-tarefas concretas.""" |
| config = get_product_config() |
| system = ( |
| f"Você é o PLANNER do agente {config['name']}. " |
| "Sua função: receber uma pergunta do usuário e decompor em " |
| "2 a 4 sub-tarefas concretas para o agente RESEARCHER executar. " |
| "Saída: lista numerada, ATÔMICA, em português. " |
| "NÃO responda a pergunta. Apenas planeje." |
| ) |
| msg = chat_completion( |
| messages=[ |
| {"role": "system", "content": system}, |
| {"role": "user", "content": state["user_query"]}, |
| ], |
| temperature=0.3, |
| ) |
| plan = msg.get("content") or "1. Responder diretamente." |
| return {"plan": plan} |
|
|
|
|
| |
| |
| |
| def researcher_node(state: GraphState) -> dict: |
| """Executa o plano usando as tools disponíveis para coletar contexto.""" |
| config = get_product_config() |
| tools = get_tools_for_product(config) |
|
|
| system = ( |
| f"Você é o RESEARCHER do agente {config['name']}. " |
| "Você recebeu um PLANO de sub-tarefas. Para cada sub-tarefa, " |
| "decida se precisa chamar uma tool. Chame tools quando precisar " |
| "de fato verificável. NÃO redija a resposta final — apenas colete " |
| "o CONTEXTO factual relevante para o WRITER usar depois. " |
| "Quando terminar de pesquisar, responda em texto livre resumindo " |
| "tudo que você descobriu." |
| ) |
|
|
| messages: list[dict] = [ |
| {"role": "system", "content": system}, |
| { |
| "role": "user", |
| "content": ( |
| f"PERGUNTA ORIGINAL DO USUÁRIO:\n{state['user_query']}\n\n" |
| f"PLANO:\n{state['plan']}\n\n" |
| "Execute o plano usando tools quando necessário." |
| ), |
| }, |
| ] |
|
|
| |
| max_tool_iterations = 5 |
| for _ in range(max_tool_iterations): |
| msg = chat_completion( |
| messages=messages, |
| tools=tools if tools else None, |
| temperature=0.3, |
| ) |
| messages.append(msg) |
| tool_calls = msg.get("tool_calls") or [] |
| if not tool_calls: |
| break |
| for tool_call in tool_calls: |
| fn = tool_call["function"] |
| tool_name = fn["name"] |
| try: |
| tool_args = json.loads(fn.get("arguments") or "{}") |
| except json.JSONDecodeError: |
| tool_args = {} |
| result = execute_tool(tool_name, tool_args) |
| messages.append( |
| {"role": "tool", "tool_call_id": tool_call["id"], "content": result} |
| ) |
|
|
| |
| context = (messages[-1].get("content") if messages else "") or "" |
| return {"context": context} |
|
|
|
|
| |
| |
| |
| def writer_node(state: GraphState) -> dict: |
| """Redige a resposta final ao usuário usando o contexto coletado.""" |
| config = get_product_config() |
| system = ( |
| f"{config['persona']}\n\n" |
| "Você está na função WRITER deste sistema multi-agente. " |
| "Use o CONTEXTO fornecido para redigir a resposta final ao usuário. " |
| "Tom: conforme a persona acima. Não invente fatos que não estejam " |
| "no contexto. Se algo está faltando, diga claramente." |
| ) |
| user = ( |
| f"PERGUNTA DO USUÁRIO:\n{state['user_query']}\n\n" |
| f"CONTEXTO COLETADO PELO RESEARCHER:\n{state['context']}\n\n" |
| ) |
| if state.get("critique") and state.get("iteration", 0) > 0: |
| user += ( |
| f"\nCRÍTICA DA VERSÃO ANTERIOR:\n{state['critique']}\n\n" |
| "Refaça incorporando os pontos da crítica." |
| ) |
|
|
| msg = chat_completion( |
| messages=[ |
| {"role": "system", "content": system}, |
| {"role": "user", "content": user}, |
| ], |
| temperature=0.6, |
| ) |
| draft = msg.get("content") or "" |
| return {"draft_answer": draft} |
|
|
|
|
| |
| |
| |
| def critic_node(state: GraphState) -> dict: |
| """Avalia a draft. Decide aprovar ou pedir refação.""" |
| config = get_product_config() |
| system = ( |
| f"Você é o CRITIC do agente {config['name']}. " |
| "Sua função: avaliar se a DRAFT atende à pergunta do usuário de " |
| "forma completa, factualmente correta (à luz do CONTEXTO), e " |
| "no tom da persona. Devolva JSON com duas chaves: " |
| '"verdict" (uma string: "APROVADO" ou "REFAZER") e "feedback" ' |
| "(uma string em PT-BR explicando o motivo). " |
| "Seja RIGOROSO mas justo: aprove quando estiver bom. NÃO peça " |
| "refação para detalhes cosméticos." |
| ) |
| user = ( |
| f"PERGUNTA:\n{state['user_query']}\n\n" |
| f"CONTEXTO:\n{state['context']}\n\n" |
| f"DRAFT:\n{state['draft_answer']}\n" |
| ) |
| msg = chat_completion( |
| messages=[ |
| {"role": "system", "content": system}, |
| {"role": "user", "content": user}, |
| ], |
| temperature=0.2, |
| response_format={"type": "json_object"}, |
| ) |
| raw = msg.get("content") or '{"verdict":"APROVADO","feedback":"ok"}' |
| try: |
| parsed = json.loads(raw) |
| critique = parsed.get("feedback", "") |
| verdict = parsed.get("verdict", "APROVADO").upper() |
| except json.JSONDecodeError: |
| critique = raw |
| verdict = "APROVADO" |
|
|
| new_iteration = state.get("iteration", 0) + 1 |
|
|
| |
| if verdict == "APROVADO" or new_iteration >= state.get("max_iterations", 2): |
| return { |
| "critique": critique, |
| "iteration": new_iteration, |
| "final_answer": state["draft_answer"], |
| } |
|
|
| return {"critique": critique, "iteration": new_iteration} |
|
|
|
|
| |
| |
| |
| def should_continue(state: GraphState) -> Literal["writer", "end"]: |
| """Decide se volta ao writer (refazer) ou termina.""" |
| if state.get("final_answer"): |
| return "end" |
| return "writer" |
|
|
|
|
| |
| |
| |
| def build_graph(): |
| """Constrói o StateGraph LangGraph com os 4 nós e o ciclo de revisão.""" |
| graph = StateGraph(GraphState) |
|
|
| graph.add_node("planner", planner_node) |
| graph.add_node("researcher", researcher_node) |
| graph.add_node("writer", writer_node) |
| graph.add_node("critic", critic_node) |
|
|
| graph.add_edge(START, "planner") |
| graph.add_edge("planner", "researcher") |
| graph.add_edge("researcher", "writer") |
| graph.add_edge("writer", "critic") |
| graph.add_conditional_edges( |
| "critic", |
| should_continue, |
| {"writer": "writer", "end": END}, |
| ) |
|
|
| return graph.compile() |
|
|
|
|
| |
| |
| |
| def run_multi_agent(user_query: str, max_iterations: int = 2) -> dict: |
| """Roda o pipeline e retorna estado final.""" |
| app = build_graph() |
| initial_state: GraphState = { |
| "user_query": user_query, |
| "plan": "", |
| "context": "", |
| "draft_answer": "", |
| "critique": "", |
| "final_answer": "", |
| "iteration": 0, |
| "max_iterations": max_iterations, |
| "messages": [], |
| } |
| final_state = app.invoke(initial_state) |
| return final_state |
|
|