Fase 1 (NEGOTIATION) shipped 2026-05-07 estabeleceu o pattern declarativo. As próximas quatro fases trazem AUTH, ONBOARDING+SALES+PLANS+CONTRACT, CHECKOUT+INGESTION e o cleanup definitivo de crewai==1.14.4 — com re-estimativas pós-audit.
A Fase 1 provou que o grafo equivale ao caminho imperativo em NEGOTIATION. Os outros sete estados continuam vivendo no domain_agent.process_turn do CrewAI legacy. Migrar é o que destrava a remoção da dependência crewai==1.14.4.
Um estado declarativo, sete imperativos.
handle_negotiation_flow via webhook Twilio.state == NEGOTIATION e flag MIGRATE_NEGOTIATION_TO_LANGGRAPH=true → LangGraph (9 nós).process_turn imperativo.requirements.txt ainda pinned em crewai>=1.14; superfície CVE + tempo de boot maior.Oito estados num grafo único, CrewAI removido.
state.pip uninstall crewai langchain-crewai — libera ~24 deps transitivas.langgraph.node.* em todos os fluxos.process_turn: ele desaparece, side-effects ficam fora do grafo (ADR-2).Cada fase encapsula um cluster de estados em seu próprio sub-grafo, mantendo o pattern Fase 1: nós são wrappers finos sobre funções existentes; side-effects ficam fora do grafo (ADR-2).
Ctrl/Cmd + scroll para zoom · arraste para mover · duplo clique para fit · ⛶ para abrir em nova aba.
Cada sub-grafo herda o boundary da Fase 1: decisão dentro, side-effects fora. O roteador é puramente determinístico — zero LLM, zero overhead.
Re-estimativas pós-AUDIT_2026-05-08 destacadas em laranja. Fase 4 ganha um spike prévio; Fase 5 dobrou de 1 dia para 2–3 dias após reconhecer que substituir domain_agent.process_turn é refactor honesto, não pip uninstall.
Estado determinístico, zero LLM. Ideal para solidificar o pattern do roteador state → sub-graph sem riscos de side-effect ou de prompt drift.
auth/graph.pyauth_handler.pyQuatro estados conversacionais. Domain agents continuam respondendo (wrapper-fino), Sonnet continua sendo o cérebro do follow-up; só a orquestração migra.
services/conversation/langgraph/onboarding/handler.py e cia.
Primeiros nós com tools reais (Asaas, Bling/Omie/Sankhya). Spike de 2 h decide a estratégia: chamada direta ou re-decoração via @langchain_core.tools.tool.
grep -rn "from crewai" utils/
Substituir process_turn é trabalho real: ele produz um outcome dict completo (intention + next_stage + side_effects + payment_plan + handoff_flags). Apagar a dep é trivial; o refactor não é.
agents/, crew/, crewai_* importspip uninstall crewai + ~24 transitivasADR-3 da Fase 1 (TypedDict, não Pydantic) foi validada pelo audit. Cada nova fase replica a estratégia — estados pequenos, opcionais, sem overhead de validação.
# services/conversation/langgraph/state.py class NegotiationState(TypedDict, total=False): user_input: str redacted_input: str relevant_memories: List[Dict] context: Dict[str, Any] state_manager_ref: Any # perception sentiment: str risk_signal: str pii_detected: bool # cascade path_taken: str planner_message: Optional[str] outcome: Dict[str, Any] ai_telemetry: Dict[str, Any]
# services/conversation/langgraph/auth/state.py class AuthState(TypedDict, total=False): user_input: str context: Dict[str, Any] state_manager_ref: Any # otp flow expected_otp: Optional[str] submitted_otp: Optional[str] otp_verified: bool attempts: int # outcome (mesmo shape Fase 1) outcome: Dict[str, Any] next_state: str # NEGOTIATION | DONE
Cada fase abre um sub-grafo novo e adiciona um branch no roteador central. As funções abaixo são os únicos pontos cruzados entre fases — o resto vive isolado em sub-pacotes.
# Roteador determinístico no entry-point. # Hoje (pós-Fase 1) só NEGOTIATION tem branch; F2-F4 expandem. def _dispatch_message_to_graph(self, user_input: str) -> Dict: from services.conversation.langgraph.runtime import is_langgraph_enabled if not is_langgraph_enabled(): return self._legacy_dispatch(user_input) state = self.state if state == "NEGOTIATION": # Fase 1 (shipped) from .langgraph.runtime import run_graph return run_graph(self, user_input, ...) elif state == "AUTH": # Fase 2 ✦ NEW from .langgraph.auth.runtime import run_auth_graph return run_auth_graph(self, user_input) elif state in {"ONBOARDING", "SALES", "PLANS", "CONTRACT"}: # F3 from .langgraph.contractor.runtime import run_contractor_graph return run_contractor_graph(self, user_input) elif state in {"CHECKOUT", "INGESTION"}: # Fase 4 from .langgraph.ops.runtime import run_ops_graph return run_ops_graph(self, user_input) return self._legacy_dispatch(user_input) # bypass safety net
Arquitetura aditiva: cada fase adiciona um branch sem tocar nos anteriores. _legacy_dispatch permanece como rede de segurança até Fase 5.
auth_handler
# Pattern Fase 1: nó como wrapper-fino sobre função existente. def node_check_otp(state: AuthState) -> Dict: from services.conversation.auth_handler import verify_otp submitted = state.get("user_input", "").strip() expected = state.get("expected_otp") verified = verify_otp(submitted, expected) # lógica intocada return { "submitted_otp": submitted, "otp_verified": verified, "attempts": state.get("attempts", 0) + 1, } def route_after_otp(state: AuthState) -> str: if state["otp_verified"]: return "transition_to_negotiation" if state.get("attempts", 0) >= 3: return "escalate_to_handoff" return "prompt_retry"
# Decisão a fechar no spike de 2 h pré-Fase 4: # Estratégia A (call-direct): def node_create_pix_charge(state): from services.payment.asaas import create_pix payment = create_pix(amount=state["amount"], debtor_id=state["debtor_id"]) return {"payment_id": payment.id, "qr_code": payment.qr_code} # Estratégia B (re-decoração @tool): from langchain_core.tools import tool @tool def create_pix_tool(amount: int, debtor_id: str) -> Dict: from services.payment.asaas import create_pix return create_pix(amount, debtor_id).to_dict() # O spike decide qual estratégia + adiciona ADR-7 ao DESIGN_F4.
| Comando / Flag | Fase | Comportamento |
|---|---|---|
MIGRATE_NEGOTIATION_TO_LANGGRAPH |
Fase 1 · shipped | Default false. Quando true, NEGOTIATION roda no LangGraph (verificado em prod 2026-05-07). Rollback validado em ~50 s via sed + force-recreate. |
MIGRATE_AUTH_TO_LANGGRAPH |
Fase 2 · proposto | Default false. Ativa sub-grafo AUTH no entry-point. Mesma convenção da Fase 1 (ADR-5). |
MIGRATE_CONTRACTOR_TO_LANGGRAPH |
Fase 3 · proposto | Cobre ONBOARDING + SALES + PLANS + CONTRACT em um único toggle. |
MIGRATE_OPS_TO_LANGGRAPH |
Fase 4 · proposto | CHECKOUT + INGESTION. Bloqueia até spike-2h definir tool strategy. |
git rm -r agents/ crew/ |
Fase 5 · gated | Após 24 h soak com todas flags true em prod sem regressão. Procedimento documentado em SHIPPED_F5.md (a criar). |
pip uninstall crewai |
Fase 5 · gated | Remove ~24 deps transitivas. Rebuild Docker libera ~80–120 MB de imagem. |
| Cenário | Fase | Comportamento esperado |
|---|---|---|
state == DONE com flag F2 true |
F2 | Roteador cai em _legacy_dispatch. Não há sub-grafo para DONE — comportamento idêntico ao baseline. |
| OTP errado 3× | F2 | route_after_otp → escalate_to_handoff + state = DONE. Reuso de handoff_service. |
| Transição AUTH → NEGOTIATION mid-graph | F2 · F3 | Sub-grafo AUTH retorna; roteador é re-invocado no próximo turn (next_state já é NEGOTIATION). Sem re-entrada. |
| Tool externa (Asaas) timeout | F4 | Best-effort herdado da Fase 1. Tool wrapper em try/except → outcome com payment_plan.action=null + handoff_flags. |
| Import falha pós-cleanup F5 | F5 | CI deve pegar via grep -rn "from crewai" antes do merge. Run extra: uv pip check. |
| Smoke prod F4 sem aprovar spike | F4 | Bloqueado: spike de 2 h é gate obrigatório. Sem ADR-7, não há merge. |
Múltiplas flags true simultâneas |
F2-F4 | Roteador trata cada estado independente; flags são ortogonais por design (não há flag-master). |
| Fase | Categoria | Arquivo | Cobertura mínima |
|---|---|---|---|
| F2 | Unit · graph | tests/unit/test_langgraph_auth.py |
OTP correct · OTP errado · 3 tentativas · transição p/ NEGOTIATION · flag off mantém legacy |
| F2 | Equivalência funcional | Mesmo arquivo · parametrize |
Cada caso testado com flag off e on — outcomes idênticos. |
| F3 | Unit · 4 sub-grafos | tests/unit/test_langgraph_contractor.py |
≥3 tests por estado (ONB, SALES, PLANS, CTRT) → mínimo 12 cases. |
| F3 | Integration · cascade | tests/integration/test_contractor_cascade.py |
Onboarding completo → primeira cobrança gerada (cascade real entre estados). |
| F4 | Unit · tools | tests/unit/test_langgraph_ops.py |
Tool success · tool timeout · tool malformed response. 3 strategies x 3 cenários. |
| F4 | Smoke · sandbox real | Manual via Twilio Sandbox | 1 PIX gerado + 1 import CSV via WhatsApp — observar trace Langfuse + audit MongoDB. |
| F5 | Suite full | uv run pytest |
403/403 ou superior (baseline shipped 2026-05-07). Zero ImportError, zero crewai residual. |
| F5 | Soak prod | 24 h em prod sem regressão | Métrica: zero handoff_active a mais que baseline; latência p95 ≤ +15%. |
Pré-condição comum: ≥5 dias estável em prod entre fases. As datas abaixo assumem início imediato após o gate liberar; ajustar conforme ramo paralelo de V2.1.
services/conversation/langgraph/ em prod. Flag MIGRATE_NEGOTIATION_TO_LANGGRAPH=false após smoke (rollback drill 50 s validado).MIGRATE_AUTH_TO_LANGGRAPH=true em smoke prod.grep -rn "process_turn" em ONBOARDING/SALES/PLANS/CONTRACT.@tool. Output: ADR-7 anexado a DESIGN_F4. Bloqueia merge da Fase 4.process_turn. pip uninstall crewai + git rm -r agents/ crew/. requirements.txt sem crewai>=1.14.Cada fase mantém _legacy_dispatch como fallback. Flag false → comportamento idêntico ao baseline pré-fase. Nenhuma fase remove código legacy — só Fase 5 faz cleanup, e mesmo assim só após soak prod.
Audit 2026-05-08 re-estimou de 1 d para 2–3 d. Substituir domain_agent.process_turn exige refactor real: ele produz {intention, next_stage, side_effects, payment_plan, handoff_flags}. Ignorar essa nuance → bug latente em side-effects.
Considerar pin exato langgraph==1.1.10 (em vez de >=1.1.0,<2.0.0) durante toda a migração F2-F5. Bumps minor não-revisados durante refactor crítico = footgun.
Da Fase 1: (1) TypedDict não Pydantic, (2) side-effects fora do grafo, (3) spans Langfuse manuais (não LangfuseCallbackHandler), (4) wrappers finos sobre funções existentes, (5) flag default false. Audit confirmou todas contra docs LangGraph 1.x.
Antes de começar Fase 4, rodar grep -rn "from crewai" utils/ + grep -rn "import crewai". Se aparecer em paths não esperados → mapear no spike de 2 h. 5 minutos que evitam CI fail.
Adicionar gate "deepeval golden suite verde" entre fases ($0.04/run, ~50 s). Complementa o smoke manual. Suite skipa silenciosamente sem ANTHROPIC_API_KEY; em CI use o secret.
| Arquivo | Tipo | Descrição |
|---|---|---|
services/conversation/langgraph/auth/__init__.py | new | Re-export de build_auth_graph. |
services/conversation/langgraph/auth/state.py | new | AuthState TypedDict (~30 LOC). |
services/conversation/langgraph/auth/nodes.py | new | 3-4 nós (node_check_otp, node_request_otp, route_after_otp) (~80 LOC). |
services/conversation/langgraph/auth/graph.py | new | Factory build_auth_graph() (~40 LOC). |
services/conversation/langgraph/auth/runtime.py | new | run_auth_graph() orchestra grafo + side effects (~50 LOC). |
services/conversation/state_manager.py | mod | + branch state == AUTH em _dispatch_message_to_graph (+8 LOC). |
tests/unit/test_langgraph_auth.py | new | ≥6 testes parametrizados (~150 LOC). |
CLAUDE.md | mod | Documentar MIGRATE_AUTH_TO_LANGGRAPH em "Variáveis de Ambiente" (+3 LOC). |
| Arquivo | Tipo | Descrição |
|---|---|---|
services/conversation/langgraph/contractor/{state,nodes,graph,runtime}.py | new | 4 sub-grafos coexistindo via shared ContractorState + dispatch interno por state.current_stage. |
services/conversation/langgraph/contractor/onboarding_nodes.py | new | Wrappers sobre onboarding/handler.py. |
services/conversation/langgraph/contractor/sales_nodes.py | new | Sales agent + Sonnet planner reuso. |
services/conversation/langgraph/contractor/plans_nodes.py | new | Plans state + Sonnet. |
services/conversation/langgraph/contractor/contract_nodes.py | new | Asaas subaccount provisioning wrapper. |
tests/unit/test_langgraph_contractor.py | new | ≥12 cases (3 por estado). |
tests/integration/test_contractor_cascade.py | new | Cascade ONB → SALES → PLANS → CTRT. |
| Arquivo | Tipo | Descrição |
|---|---|---|
.claude/sdd/active/CREWAI_TO_LANGGRAPH/SPIKE_TOOL_STRATEGY.md | new | Output do spike de 2 h. Documenta ADR-7. |
services/conversation/langgraph/ops/{state,nodes,graph,runtime}.py | new | Sub-grafo CHECKOUT + INGESTION compartilhado. |
services/conversation/langgraph/ops/tools.py | new | Wrappers sobre services/payment/asaas.py + services/data/provider.py. |
tests/unit/test_langgraph_ops.py | new | Mock tool success/timeout/malformed. |
requirements.txt | mod | Possível: langchain-anthropic SE Estratégia B do spike for escolhida. |
| Arquivo / Path | Ação | Notas |
|---|---|---|
agents/ | delete | Toda a pasta CrewAI agents. |
crew/ | delete | Pasta de inicialização CrewAI. |
services/conversation/state_manager.py | mod | Remover _legacy_dispatch + branch fallback. |
services/conversation/negotiation_flow.py | mod | Remover early-branch e o caminho imperativo legado (~200 LOC). |
requirements.txt | mod | Remove crewai>=1.14.4 + dependências exclusivas. |
CLAUDE.md | mod | Atualizar diagrama "Arquitetura do Sistema" → LangGraph único. |
.claude/sdd/archive/CREWAI_TO_LANGGRAPH/SHIPPED_F5.md | new | Closing report da migração completa. |