Plano de Implementação · v2 · CREWAI_TO_LANGGRAPH 2026-05-08 · launch/v2 · audit-validated

Migração CrewAI → LangGraph, Fases 2–5

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.

Fase 1 · shipped 2026-05-07 Fase 2 · ~1–2 d Fase 3 · ~2–3 d Fase 4 · ~3–4 d + spike 2 h Fase 5 · ~2–3 d (re-estimado)
Fase 2 — AUTH
~1–2 d
Determinístico · zero LLM
Fase 3 — 4 estados
~2–3 d
ONB · SALES · PLANS · CTRT
Fase 4 — Tools
~3–4 d
+ spike 2 h prévio
Fase 5 — Cleanup
~2–3 d
Re-estimado (era 1 d)
Pré-cond. Fase 2
2026-05-12
≥5 d estável após F1

Pipeline imperativo virou plataforma declarativa — mas só em um estado

Hoje — Pós-Fase 1
Após Fase 5

Um estado declarativo, sete imperativos.

  1. Mensagem chega ao handle_negotiation_flow via webhook Twilio.
  2. Se state == NEGOTIATION e flag MIGRATE_NEGOTIATION_TO_LANGGRAPH=true → LangGraph (9 nós).
  3. Para AUTH, ONBOARDING, SALES, PLANS, CONTRACT, CHECKOUT, INGESTION → CrewAI process_turn imperativo.
  4. requirements.txt ainda pinned em crewai>=1.14; superfície CVE + tempo de boot maior.
  5. Telemetria mistura dois modelos (spans manuais + traces CrewAI), tornando dashboards inconsistentes.

Oito estados num grafo único, CrewAI removido.

  1. Roteador determinístico no entry-point escolhe sub-grafo por state.
  2. Cada estado tem grafo próprio reusando funções existentes (pattern Fase 1).
  3. pip uninstall crewai langchain-crewai — libera ~24 deps transitivas.
  4. Spans Langfuse uniformes langgraph.node.* em todos os fluxos.
  5. Refactor sem risco em process_turn: ele desaparece, side-effects ficam fora do grafo (ADR-2).

Sub-grafos por estado, roteados na entrada

Ctrl/Cmd + scroll para zoom · arraste para mover · duplo clique para fit · ⛶ para abrir em nova aba.

Loading…

Cada sub-grafo herda o boundary da Fase 1: decisão dentro, side-effects fora. O roteador é puramente determinístico — zero LLM, zero overhead.

Quatro fases · pattern provado em todas

Fase 2
AUTH
~1–2 dias

Estado determinístico, zero LLM. Ideal para solidificar o pattern do roteador state → sub-graph sem riscos de side-effect ou de prompt drift.

  • Sub-grafo auth/graph.py
  • Reuso direto dos handlers de auth_handler.py
  • Smoke prod: 1 fluxo de OTP completo
Fase 3
ONB · SALES · PLANS · CTRT
~2–3 dias

Quatro estados conversacionais. Domain agents continuam respondendo (wrapper-fino), Sonnet continua sendo o cérebro do follow-up; só a orquestração migra.

  • 4 sub-grafos coexistindo em services/conversation/langgraph/
  • Reuso de onboarding/handler.py e cia.
  • Smoke prod: cascade contractor → debtor
Spike 2 h
Fase 4
CHECKOUT + INGESTION
~3–4 dias

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.

  • Pre-Phase audit: grep -rn "from crewai" utils/
  • Tool invocation strategy → documentar ADR-7
  • Smoke prod: 1 PIX gerado + 1 import CSV
Re-estimado
Fase 5
Cleanup CrewAI
~2–3 dias

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 é.

  • Remover agents/, crew/, crewai_* imports
  • pip uninstall crewai + ~24 transitivas
  • Suite full + 24 h soak prod antes de archive

TypedDict por sub-grafo, herdando o pattern da Fase 1

Fase 1 — NegotiationState (shipped)
# 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]
Fase 2 — AuthState (proposto)
# 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

Pontos de extensão por fase

services/conversation/state_manager.py modified Fase 2 entry-point — branch único por estado
# 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.

services/conversation/langgraph/auth/nodes.py new — Fase 2 Reutilização direta de 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"
services/conversation/langgraph/ops/nodes.py new — Fase 4 Spike: tool invocation strategy
# 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.

Operacional — deploy, rollback, monitoração

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ários que precisam de teste explícito

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_otpescalate_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).

Categorias por fase

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%.

Cronograma proposto

Avisos críticos · contratos · riscos

Backward compatibility

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.

Crítico — Fase 5 não é trivial

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.

Performance · pin hygiene

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.

Pattern shipped — reusar

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.

Pre-Phase 4 audit obrigatório

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.

Deepeval gate — novo

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.

Mapa de mudanças por fase

Fase 2 — AUTH · ~7 arquivos novos · ~20 LOC modificadas
ArquivoTipoDescrição
services/conversation/langgraph/auth/__init__.pynewRe-export de build_auth_graph.
services/conversation/langgraph/auth/state.pynewAuthState TypedDict (~30 LOC).
services/conversation/langgraph/auth/nodes.pynew3-4 nós (node_check_otp, node_request_otp, route_after_otp) (~80 LOC).
services/conversation/langgraph/auth/graph.pynewFactory build_auth_graph() (~40 LOC).
services/conversation/langgraph/auth/runtime.pynewrun_auth_graph() orchestra grafo + side effects (~50 LOC).
services/conversation/state_manager.pymod+ branch state == AUTH em _dispatch_message_to_graph (+8 LOC).
tests/unit/test_langgraph_auth.pynew≥6 testes parametrizados (~150 LOC).
CLAUDE.mdmodDocumentar MIGRATE_AUTH_TO_LANGGRAPH em "Variáveis de Ambiente" (+3 LOC).
Fase 3 — ONBOARDING + SALES + PLANS + CONTRACT · ~10 arquivos novos
ArquivoTipoDescrição
services/conversation/langgraph/contractor/{state,nodes,graph,runtime}.pynew4 sub-grafos coexistindo via shared ContractorState + dispatch interno por state.current_stage.
services/conversation/langgraph/contractor/onboarding_nodes.pynewWrappers sobre onboarding/handler.py.
services/conversation/langgraph/contractor/sales_nodes.pynewSales agent + Sonnet planner reuso.
services/conversation/langgraph/contractor/plans_nodes.pynewPlans state + Sonnet.
services/conversation/langgraph/contractor/contract_nodes.pynewAsaas subaccount provisioning wrapper.
tests/unit/test_langgraph_contractor.pynew≥12 cases (3 por estado).
tests/integration/test_contractor_cascade.pynewCascade ONB → SALES → PLANS → CTRT.
Fase 4 — CHECKOUT + INGESTION · spike + ~8 arquivos
ArquivoTipoDescrição
.claude/sdd/active/CREWAI_TO_LANGGRAPH/SPIKE_TOOL_STRATEGY.mdnewOutput do spike de 2 h. Documenta ADR-7.
services/conversation/langgraph/ops/{state,nodes,graph,runtime}.pynewSub-grafo CHECKOUT + INGESTION compartilhado.
services/conversation/langgraph/ops/tools.pynewWrappers sobre services/payment/asaas.py + services/data/provider.py.
tests/unit/test_langgraph_ops.pynewMock tool success/timeout/malformed.
requirements.txtmodPossível: langchain-anthropic SE Estratégia B do spike for escolhida.
Fase 5 — Cleanup · remoção massiva
Arquivo / PathAçãoNotas
agents/deleteToda a pasta CrewAI agents.
crew/deletePasta de inicialização CrewAI.
services/conversation/state_manager.pymodRemover _legacy_dispatch + branch fallback.
services/conversation/negotiation_flow.pymodRemover early-branch e o caminho imperativo legado (~200 LOC).
requirements.txtmodRemove crewai>=1.14.4 + dependências exclusivas.
CLAUDE.mdmodAtualizar diagrama "Arquitetura do Sistema" → LangGraph único.
.claude/sdd/archive/CREWAI_TO_LANGGRAPH/SHIPPED_F5.mdnewClosing report da migração completa.