Menuabrir
EstávelAtualizado em 14 de mai. de 2026, 00:06

Este módulo depende de

8
  • whatsappLê contexto de grupos (últimos 30 dias); envia respostas via service.send_message
  • agentsBridge dispatch_to_agent via council_action; agents executam ações propostas
  • crmcross_module.search_contacts, read_contact_360 para contexto
  • billingcross_module.read_revenue_summary (MRR, overdue, custo retenção)
  • businesscross_module.read_products, read_knowledge, read_brain_config, read_objections
  • intelligencecross_module.read_intelligence_insights L1/L3/L5 para decisões com evidência
  • settingscouncil_enabled, ai_auto_reply_enabled kill switches
  • memberscross_module.business_snapshot (membros ativos, plano)

Módulos que dependem deste

2
  • whatsappPersiste mensagens do conselho em grupos; Conselho responde via whatsapp.service
  • agentsBridge: recebe council_action.dispatched; reporta outcome

Conselho

Status: 🟢 Estável Code: backend/app/modules/council UI: frontend/src/app/(admin)/[slug]/admin/council Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: whatsapp (presença em grupos), agents (despacho de ações), crm + billing + members + intelligence + business (leitura de contexto), settings (kill switches), tenant_router (multi-tenant DB)


1. Identidade

O que faz (uma frase)

Orquestra 8 conselheiros de IA (CEO + 7 especialistas) que conversam com o dono do negócio via web ou grupos WhatsApp, consultam o sistema inteiro como contexto, propõem ações e despacham para Agentes externos quando o dono aprova.

Por que existe (negócio)

O Mandir tem duas IAs com naturezas diferentes (ver memória [[camadas_ia]]):

  • Agentes externos (Hari, Clara, Aurora, Chandra, Dunning) — falam com cliente final. Operação.
  • Conselho — fala com o dono. Gerência.

Antes do Conselho, Felipe tinha que abrir 20 telas pra entender o estado do negócio: dashboard de email, painel de CRM, métricas de Meta Ads, histórico de WhatsApp, etc. O Conselho consolida isso em conversa: "@conselho como está minha taxa de conversão?" e o CEO responde com números reais lidos do GA4 + CRM + revenue.

Mais importante: o Conselho propõe ações concretas ("dispara campanha X pro segmento Y") que o dono aprova com um botão. Aprovado, o despacho vai pro Agente certo executar.

Por que existe (técnico)

A IA generativa precisa de três camadas separadas que se acoplaram historicamente:

  1. Conhecimento (system prompt + tools que leem o DB) — Conselheiros têm scope diferente por persona.
  2. Decisão (ações propostas, aprovação humana, despacho) — separa "sugerir" de "executar". Audit + reversibilidade.
  3. Memória (memórias de longo prazo cross-conversa + mirrors periódicos) — IA não esquece o contexto do negócio entre sessões.

Sem o módulo Conselho, isso vira código espalhado em crm/ + whatsapp/ + agents/. Como módulo dedicado:

  • Multi-tenant nativo (cada tenant ganha seus conselheiros provisionados automaticamente).
  • Persona/prompt overrride por tenant sem alterar o código.
  • Kill switch independente (desligar Conselho não desliga Agentes e vice-versa).
  • Trilha de auditoria completa: cada council_message tem tool_calls, cada council_mirror_section tem evidence.

Status atual

  • 2026-05-11 — Marcos 1-13 deployados (alembic 0084). Primeiros 7 conselheiros + UI básica + brain com tool-use.
  • 2026-05-12 — Fase async definitiva (commit f7f0925, migs 0089-0093). Pipeline assíncrono via Celery, contexto de grupo, roles + min_caller_role, agents em grupos, digests hierárquicos, import de chat.txt.
  • 2026-05-13 — Personas aplicadas em prod (8 conselheiros: CEO + 7), UX leigo-friendly, scope split marketing → 4 namespaces (paid + analytics + instagram + youtube), kill switch separado (commit 8b7107e, mig 0111). Loop-mismatch corrigido (commits cd4c60b + a1356e5 + ba46496).

Próxima mudança planejada: F1-F6 — validação end-to-end com Felipe interagindo nos grupos WhatsApp reais. Eventos canônicos emitidos via mandir-events (hoje só log estruturado).


2. Cases de uso reais

Case 1: "Como está o GA4 do site?"

Situação: Felipe pergunta no grupo Mais Consciente: @conselho o que você poderia me dar um panorama do meu GA4 e trazer insights para mim?

Fluxo:

  1. Webhook Evolution chega em /api/whatsapp/webhooks/evolution/<instance>/<token>.
  2. Persiste em whatsapp_message (RLS por tenant).
  3. Enfileira task Celery council.process_inbound_message.
  4. Worker abre worker_session(tenant_id) (NullPool isolada — fix loop-mismatch).
  5. routing.route_inbound_whatsapp_message resolve presence (Conselho @conselho ativo no grupo), classifica caller (council_member_role.role=owner), aplica handler ROUTE_COUNCIL.
  6. brain.respond monta system prompt (CORE + ADVISOR[ceo] + VERTICAL[other][ceo]), contexto (últimos 30 dias de mensagens do grupo, truncado em 8000 tokens), histórico de conversation.
  7. LLM chama 5 tools em paralelo: read_ga4_traffic_summary, read_ga4_conversion_funnel, read_ga4_top_sources (2x), read_ga4_event_breakdown.
  8. Tools lêem das tabelas ga4_* (persistidas pelo plugin Hub.3, atualizadas via observe).
  9. LLM segunda iteração: gera resposta narrativa (1010 tokens out).
  10. whatsapp_send_callback envia via Evolution + persiste outbound em whatsapp_message.

Impacto: Felipe entende seu funil sem abrir o painel do GA4. Tempo de resposta: ~25 segundos.

Case 2: Mirror semanal

Situação: Conforme cronograma futuro (beat schedule planejado), toda segunda-feira o sistema gera um Mirror tipo weekly para cada tenant ativo.

Fluxo:

  1. Task Celery cria council_mirror em status generating.
  2. Blueprint define 8 seções: business_health, real_customer, conversion_patterns, leaks, identity_dissonance, next_lever, wellbeing_check, content_audit.
  3. Para cada seção, brain._run_turn dispara o conselheiro autor (ex: business_health → Strategy + Operations) com instruções específicas.
  4. Cada seção é persistida em council_mirror_section com evidence (lista de tool_calls feitos pelo conselheiro).
  5. Quando todas concluem, brain sintetiza summary (3-5 frases) e marca status=ready.
  6. Felipe abre /admin/council/mirrors/<id> e lê o dossiê.

Impacto: Diagnóstico semanal de maturidade sem Felipe ter que coletar/analisar. Comparações /mirrors/compare/latest mostram evolução.

Case 3: Conselheiro propõe ação que o dono aprova

Situação: Conselheiro Commercial detecta lead MQL com score alto sem follow-up em 7 dias.

Fluxo:

  1. Brain (via tool propose_action) cria council_action em status proposed:
    • intent=send_message, target_agent_slug=clara, payload={target_phone, content}, requires_approval=true.
  2. UI mostra ação proposta em /admin/council/actions com botão Aprovar/Rejeitar.
  3. Felipe aprova → POST /api/council/actions/{id}/decision {approve:true} → status approved.
  4. UI mostra botão Despachar → POST /api/council/actions/{id}/dispatchbridge.dispatch_to_agent cria agents_run.
  5. Agente Clara executa, retorna outcome, council_action.outcome é preenchido + status completed.

Impacto: Atua sem Felipe ter que escrever a mensagem nem trocar de tela. Audit trail completo (proposed_at, decided_at, dispatched_at, completed_at, outcome).


3. Oportunidades de negócio

  • Venda externa para coaches/consultores solo: o Conselho é especialmente útil pra empreendedor solo que não tem CEO/CFO/CMO. Cada conselheiro é um "papel" que ele não pode contratar fisicamente. Pricing por tenant + número de conselheiros ativos.

  • Tier "Sociedade" (auto-despacho): hoje toda ação exige aprovação. Tier premium poderia permitir requires_approval=false para intents de baixo risco (ex: send_message para lead frio com template pré-aprovado), com kill switch global.

  • Conselheiros customizados por vertical: wellness, e-commerce, SaaS, consultoria — cada vertical tem necessidades específicas. Hoje há overlay vertical (prompts/verticals.py); poderia virar marketplace de personas.

  • Mirrors como entregável de consultoria: o Mirror weekly/monthly é literalmente um diagnóstico de negócio assinado por "Conselho IA do Mandir". Pode virar produto vendido separado (assinatura "Diagnóstico Mensal").

  • API pra terceiros: Hoje o Conselho fala via UI + WhatsApp. Poderia expor /api/council/ask?question=... autenticada como webhook, permitindo bots externos (Slack, Discord, etc.) consultar o Conselho.

  • Risco comercial: dependência forte de Anthropic (Claude Opus). Migração pra outro LLM exige reescrita do llm_gateway e dos prompts (que foram afinados pra Claude). Custos crescem linearmente com uso — cada mensagem custa ~$0.02-0.10 dependendo de tool calls.


4. Arquitetura interna

Diagrama de fluxo principal — Conselho em grupo WhatsApp

Usuário no grupo WA escreve "@conselho ..."
   ↓
Evolution webhook → /api/whatsapp/webhooks/evolution/<instance>/<token>
   ↓
record_inbound() persiste em whatsapp_message (RLS)
   ↓
process_inbound_message_task.apply_async()  [Celery]
   ↓
_process_inbound_worker() [worker_session — NullPool isolada]
   ↓
route_inbound_whatsapp_message()
   ├─ select_presence_for_message → @conselho ativa? min_caller_role?
   ├─ classify_caller → council_member_role + CRM + billing
   ├─ resolve_route → conditions match? → ROUTE_COUNCIL
   └─ apply_route
        ↓
   brain.respond(conversation_id)
        ├─ build_system_prompt(advisor=ceo, vertical=other)
        ├─ build_context_preamble + build_group_context_block(30 days, 8k tokens)
        ├─ load_conversation_history(40 msgs)
        ├─ _run_turn() — loop tool-use (até 6 iterações)
        │     └─ tool calls: read_ga4_*, read_crm_*, read_business_snapshot, etc.
        └─ append_message(role=assistant, body, tool_calls, tokens)
        ↓
   whatsapp_send_callback() [worker_session NullPool]
        ├─ set_config('whatsapp.tenant_id', uuid) — RLS
        ├─ resolve provider_account_id por phone_number_e164
        └─ wa_send → POST evolution/message/sendText

Arquivos do módulo

ArquivoPropósito
models.pyORM das 12 tabelas
routes.py41 endpoints REST /api/council/*
service.pyCRUD + transições de estado (sem LLM)
brain.pyOrquestrador LLM (loop tool-use, consult_advisor)
context.pyBuilder de contexto (prompts + histórico + group block)
routing.pyPABX router (classify caller → resolve rule → apply handler)
whatsapp_presence.pyConselho em grupos (mention, intervenção, cooldown)
tasks.py5 tasks Celery (mirror, inbound, digests weekly/monthly/quarterly)
bridge.pyInterface Conselho ↔ Agentes (read_agent_state, dispatch_to_agent, observe_outcome)
cross_module.py~25 funções read-only contra outros módulos (CRM, billing, intelligence, GA4, etc.)
mirror.pyPipeline Mirror (blueprint, seções, síntese, compare)
digests.pySíntese hierárquica de grupos (week → month → quarter)
tools.pyVocabulário de tools (JSON Schema)
scope_defaults.pyDefaults de escopo por conselheiro
constants.pySlugs canônicos
persona.pySchema AdvisorConfig (Pydantic)
policy.pyGates (_council_enabled, _advisor_is_active, _ai_auto_reply_enabled)
prompts/builder.pybuild_system_prompt(slug, vertical, overrides)
prompts/core.pyCORE_SYSTEM_PROMPT (universal)
prompts/advisors.pyADVISOR_CORE_PROMPTS[slug]
prompts/verticals.pyVERTICAL_OVERLAYS[vertical][slug]

Tasks Celery

TaskQuando disparaIdempotente?Retry
council.process_inbound_messageWebhook WA inbound enfileira; transcrição de áudio tambémSem dedup formal (persistência prévia em whatsapp_message resolve)max_retries=2, countdown 30s, skip erros lógicos
council.generate_mirrorPOST /mirrors com run_inline=falseSim (checa status=generating)max_retries=2, countdown 120s
council.weekly_group_digestsBeat schedule (planejado: cron semanal segunda 04:00)Upsert por UNIQUE(tenant, group, period_kind, period_start)Sem retry (best-effort)
council.monthly_group_digestsBeat schedule (planejado: cron primeiro dia do mês)Upsert idemSem retry
council.quarterly_group_digestsBeat schedule (planejado: cron primeiro dia do trimestre)Upsert idemSem retry

Sessão: todas as tasks usam tenant_router.worker_session(tenant_id) (NullPool isolada por task) para evitar loop-mismatch — ver memória [[celery-asyncpg-loop-mismatch]].

Adapters externos

ProviderComo integra
Anthropic ClaudeVia llm_gateway (não local ao módulo — vive em app/core/llm_gateway.py); modelo default claude-opus-4-7, com cache_control no system
Evolution API (WhatsApp)Via whatsapp.service.send_message; chamado de whatsapp_send_callback
ElevenLabs (voz)Planejado (Tier S, voice_enabled na config) — ainda não implementado

Os 8 Conselheiros

SlugNome amigávelFocoPersonalidade
ceoConselheiro CEOVisão integrada do negócioOrquestrador; resposta default em grupos com @conselho
strategyConselheiro de EstratégiaVisão de longo prazo, OKRs, alocaçãoReflexivo, foco em 90d-quarter
operationsConselheiro de OperaçõesExecução, rotina, gargalosDireto, prioriza ação
commercialConselheiro ComercialVendas, conversão, pipelineAnalítico, métricas-first
content_brandConselheiro de Conteúdo & MarcaBrand voice, presença pública, audiênciaSensível a tom; lê IG + YT + analytics
intelligenceConselheiro de InteligênciaDados, padrões, anomalias, alertasPattern-finder; flag_risk frequente
relationshipConselheiro de RelacionamentoComunicação 1:1, retenção, customer successEmpático; aciona Hari/Chandra
wellbeingConselheiro de Bem-EstarSustentabilidade do donoÚnico blindado de receita/CAC (não vê billing/marketing)

5. Tabelas + relacionamentos

12 tabelas, todas prefixadas council_*. Sem FK física cross-módulo (convenção Mandir).

council_advisor

Definição de um Conselheiro para um tenant — permite override de persona.

ColunaTipoNotas
idUUID PKuuid_generate_v4()
tenant_idUUIDidx
slugVARCHAR(32)ceo/strategy/operations/commercial/content_brand/intelligence/relationship/wellbeing
display_nameVARCHAR(120)"Conselheiro CEO" (personalizável)
headlineVARCHAR(255)"visão integrada do negócio"
core_prompt_overrideTEXTNULL = usa prompts.ADVISOR_CORE_PROMPTS[slug]
vertical_overlay_overrideTEXTNULL = deriva de tenant.vertical
configJSONB{tone, proactivity, model_tier, voice_enabled, custom_instructions, max_tokens_per_turn, auto_dispatch, avatar_url, accent_color}
is_activeBOOLdefault true — kill switch por conselheiro
created_at / updated_atTIMESTAMPnow()

Constraint: UNIQUE(tenant_id, slug). Lido por: council_conversation.primary_advisor_slug (string, sem FK física).

council_advisor_scope

Escopo de um Conselheiro — quais módulos observa, agentes pode acionar, intents permitidos.

ColunaTipoNotas
tenant_idUUIDidx
advisor_slugVARCHAR(32)idx
observed_modulesJSONBNULL = tudo; [] = nada; ["crm","whatsapp",...]
observed_channelsJSONBex: ["whatsapp","instagram","email"]
allowed_agent_slugsJSONBex: ["hari","clara"]
allowed_intentsJSONBex: ["send_message","create_task"]
extraJSONBlivre

Constraint: UNIQUE(tenant_id, advisor_slug). Auto-seed: primeira leitura via service.get_advisor_scope() aplica defaults de scope_defaults.py.

council_conversation

Fio de conversa dono ↔ Conselheiro(s) — origina em UI ou grupo WA.

ColunaTipoNotas
tenant_idUUIDidx
owner_user_idUUIDIdentity.app_user.id (cross-DB, sem FK física)
primary_advisor_slugVARCHAR(32)Conselheiro que "abriu" o fio
titleVARCHAR(255)Auto-gerado na 1ª mensagem; editável
statusVARCHAR(16)active / archived
sourceVARCHAR(32)ui / whatsapp_group / whatsapp_dm / email
source_external_idVARCHAR(255)Group JID se source=whatsapp_group
last_message_atTIMESTAMP(tz)Atualizado a cada nova msg

Relacionamento: 1:N com council_message (ondelete=CASCADE). Índices: (tenant_id, owner_user_id), (tenant_id, last_message_at).

council_message

Mensagem individual da conversa (user/assistant/system).

ColunaTipoNotas
conversation_idUUIDFK council_conversation.id CASCADE
roleVARCHAR(16)user / assistant / system
advisor_slugVARCHAR(32)Preenchido quando role=assistant
bodyTEXTConteúdo
tool_callsJSONB{calls: [{name, input, result}, ...]} — audit trail
tokens_in / tokens_outINTPara custo
modelVARCHAR(64)ex: claude-opus-4-7

Índices: (conversation_id, created_at).

council_action

Ação recomendada por Conselheiro — workflow proposed → approved → dispatched → completed.

ColunaTipoNotas
advisor_slugVARCHAR(32)Quem propôs
conversation_idUUIDFK opcional (SET NULL)
titleVARCHAR(255)Título humano
bodyTEXTRationale completa
intentVARCHAR(64)send_message / create_task / schedule_followup / flag_risk / adjust_segment / ...
target_agent_slugVARCHAR(64)Agente executor (hari/clara/aurora/...)
payloadJSONBParâmetros pro Agente: {content, target_email, target_phone, ...}
statusVARCHAR(16)proposed / approved / dispatched / completed / failed / rejected / expired
requires_approvalBOOLdefault true (Tier Sociedade pode setar false)
bridge_run_idUUIDagents_run.id (cross-módulo, sem FK)
outcomeJSONBResultado reportado pelo Agente
proposed_at / decided_at / dispatched_at / completed_at / expires_atTIMESTAMP(tz)Audit trail

Índices: (tenant_id, status), (tenant_id, advisor_slug).

council_memory

Memória de longo prazo cross-conversa (fact/decision/pattern/relationship/dissonance).

ColunaTipoNotas
kindVARCHAR(32)fact / decision / pattern / relationship / dissonance
advisor_slugVARCHAR(32)Quem registrou (NULL = compartilhado)
subject_typeVARCHAR(32)contact / deal / program / tenant / channel / global
subject_idUUIDID do objeto referenciado
titleVARCHAR(255)Sumário
bodyTEXTDescrição completa
confidenceFLOAT0.0-1.0
sourceJSONB{channel, msg_ids, observed_at}
expires_atTIMESTAMP(tz)NULL = permanente
last_reinforced_atTIMESTAMP(tz)Atualizado quando outro sinal reforça

Índices: (tenant_id, kind), (tenant_id, subject_type, subject_id).

council_mirror

Dossiê estruturado (Maturity Diagnostics — Tese v3.4 §11).

ColunaTipoNotas
kindVARCHAR(16)onboarding / weekly / monthly / quarterly / ad_hoc
statusVARCHAR(16)generating / ready / failed / archived
title / summaryVARCHAR / TEXTSíntese 3-5 frases quando ready
requested_byUUIDapp_user.id (NULL = automático)
error_messageTEXTSe failed
started_at / completed_atTIMESTAMP(tz)

Relacionamento: 1:N com council_mirror_section (CASCADE).

council_mirror_section

Seção individual do Mirror autorada por um conselheiro.

ColunaTipoNotas
mirror_idUUIDFK CASCADE
advisor_slugVARCHAR(32)Autor
section_kindVARCHAR(32)business_health / real_customer / conversion_patterns / leaks / identity_dissonance / next_lever / wellbeing_check / content_audit / custom
titleVARCHAR(255)
bodyTEXTMarkdown
severityVARCHAR(16)info / warning / critical
evidenceJSONB{tool_calls, key_metrics} — auditoria
statusVARCHAR(16)generating / ready / failed
tokens_in / tokens_out / model
sort_orderINTOrdem de exibição

Índices: (mirror_id, sort_order), (tenant_id, advisor_slug).

council_whatsapp_presence

Presença do Conselho em um grupo WA — quem responde, com que token, em que cadência.

ColunaTipoNotas
group_external_idVARCHAR(255)JID do grupo (...@g.us)
group_labelVARCHAR(255)Snapshot do nome (pode divergir do real)
primary_advisor_slugVARCHAR(32)Padrão que responde
phone_number_e164VARCHAR(32)Chave canônica (mig 0087) — número do WA
provider_account_idUUIDSnapshot — não é chave funcional
mention_tokenVARCHAR(64)@conselho (default). Multi-presence: @estrategia, @comercial, etc.
proactivityVARCHAR(16)silent / moderate / active
is_activeBOOLKill switch
settingsJSONB{auto_summarize_on: ["pricing"], quiet_hours: [22, 8]}
last_intervention_atTIMESTAMP(tz)Cooldown
context_window_daysINT1-90; padrão 30
max_context_tokensINT500-32000; padrão 8000
min_caller_roleVARCHAR(16)anyone / staff / admin / owner (mig 0090)

Chave funcional: (tenant_id, phone_number_e164, group_external_id, lower(mention_token)) — gerenciada via op.execute() em migration.

council_routing_rule

PABX-style routing — classify caller → match conditions → handler.

ColunaTipoNotas
priorityINTASC — menor = mais específico
nameVARCHAR(255)"Owner em DM"
conditionsJSONB{is_dm: true, is_owner: true, lifecycle_stage: ["mql"], has_tag: ["vip"], crm_score_min: 70}
routeJSONB{handler: "council", advisor_slug: "commercial"} ou {handler: "agent", agent_slug: "hari"} ou {handler: "reply_template", template: "..."} ou {handler: "ignore"}
is_activeBOOL
last_matched_at / match_countDebug

Defaults seeded: 7 regras (routing.seed_default_rules): Owner DM → Strategy; Admin → Strategy; Staff → Commercial; Overdue → Dunning; Active customer → Hari; Lead MQL/SQL → Clara; Unknown → reply_template.

council_member_role

Mapping (tenant, phone) → role — substitui heurística cross-DB Identity (mig 0090).

ColunaTipoNotas
phone_e164VARCHAR(32)
roleVARCHAR(16)owner / admin / staff / student / external
labelVARCHAR(255)"Felipe — CEO"
notesTEXT

Constraint: UNIQUE(tenant_id, phone_e164). Hierarquia (rank): owner=4, admin=3, staff=2, student=1, external=0, anyone=0.

council_group_digest

Síntese hierárquica de grupo WA (week → month → quarter — mig 0092).

ColunaTipoNotas
group_external_idVARCHAR(255)
period_kindVARCHAR(16)week / month / quarter
period_start / period_endTIMESTAMP(tz)Inclusivos
message_countINT
summaryTEXTProsa pt-BR (2-4 parágrafos)
key_decisions / key_facts / key_people / themes / open_questionsJSONB array
sentimentFLOAT-1.0 a 1.0
generated_by_advisorVARCHAR(32)Mig 0092
statusVARCHAR(16)ready / generating / failed

Constraint: UNIQUE(tenant_id, group_external_id, period_kind, period_start) — upsert idempotente.

Relacionamentos cross-módulo

DireçãoOutro móduloComoPor quê
↗ Lêcrmcross_module.search_contacts, read_contact_360Contexto de lead/cliente
↗ Lêbillingcross_module.read_revenue_summaryMRR, overdue, custo de retenção
↗ Lêmemberscross_module.business_snapshotMembros ativos, plano
↗ Lêintelligencecross_module.read_intelligence_insights, read_patterns, read_observationsL1/L3/L5 do Brain Observatory
↗ Lêbusinesscross_module.read_knowledge, read_products, read_objectionsCatálogo + cotações + objeções
↗ Lêmarketing, analytics (GA4), instagram, youtubecross_module.read_meta_*, read_ga4_*, etc. (~25 tools)Hubs 2-6
↗ Lêwhatsappcontext.build_group_context_blockHistórico do grupo
↘ Escreveagentsbridge.dispatch_to_agent → cria agents_runDespacho de ação aprovada
↘ Escrevewhatsappwhatsapp.service.send_messageResposta no grupo/DM
↗ Lêsettingspolicy._council_enabled, _ai_auto_reply_enabledKill switches
↗ Lêtenant_routerworker_sessionDB-per-tenant nas tasks Celery

6. API / Endpoints

41 endpoints sob /api/council/*. Auth: require_session_user (qualquer usuário autenticado) ou require_tenant_admin (admin/owner do tenant).

Configuração global

MétodoRotaAuthO que faz
GET/api/council/settingssessionsettings_identity.council_enabled
PATCH/api/council/settingsadminAtiva/desativa Conselho global do tenant

Conselheiros

MétodoRotaAuthO que faz
GET/api/council/advisorssessionLista os 8 conselheiros (auto-seed se ausentes)
GET/api/council/advisors/{slug}sessionLê um conselheiro
PATCH/api/council/advisors/{slug}adminRefina persona (display_name, headline, is_active, prompts, config)
GET/api/council/advisors/{slug}/scopesessionLê escopo
PUT/api/council/advisors/{slug}/scopeadminSubstitui escopo completo
GET/api/council/advisors/{slug}/preview-promptsessionRenderiza system prompt final (debug)

Conversas

MétodoRotaAuthO que faz
POST/api/council/conversationssessionCria fio novo (primary_advisor_slug, title?) → 201
GET/api/council/conversationssessionLista conversas do user (filtros: status, limit)
GET/api/council/conversations/{id}/messagessessionHistórico (limit default 200)
POST/api/council/conversations/{id}/messagessessionTurno atômico: persiste user msg → brain.respond → persiste assistant. Retorna {user_message, assistant_message}. 503 se Anthropic falha, rollback completo.

Ações

MétodoRotaAuthO que faz
POST/api/council/actionssessionPropõe ação (advisor_slug, intent, target_agent_slug?, payload?, body?, conversation_id?, requires_approval?, expires_at?) → 201
GET/api/council/actionssessionLista (filtros: status, advisor_slug, limit)
POST/api/council/actions/{id}/decisionsession{approve: bool} → transição proposed → approved/rejected
POST/api/council/actions/{id}/dispatchsessionapproved → dispatched via bridge.dispatch_to_agent

Memória

MétodoRotaAuthO que faz
POST/api/council/memorysessionRegistra (kind, title, body?, advisor_slug?, subject_type?, subject_id?, confidence?, source?, expires_at?) → 201
GET/api/council/memorysessionLê (filtros: kind, subject_type, subject_id, limit)

Mirror

MétodoRotaAuthO que faz
POST/api/council/mirrorsadminDispara pipeline (kind, run_inline?) → 201. Se run_inline=true, executa síncrono (dev); senão Celery
GET/api/council/mirrorssessionLista (filtros: kind, status, limit)
GET/api/council/mirrors/{id}sessionLê completo com seções
GET/api/council/mirrors/compare/latest?kind=XsessionCompara 2 mais recentes ready. 404 se < 2
GET/api/council/mirrors/compare?older_id&newer_idsessionCompara 2 específicos
POST/api/council/mirrors/{id}/runadminDispara síncrono mirror em generating (CLI/jobs)

WhatsApp Presence

MétodoRotaAuthO que faz
GET/api/council/whatsapp/presencessessionLista (filtros: is_active, phone_number_e164) com current_account_label decorado
POST/api/council/whatsapp/presencesadminAdiciona Conselho a grupo. Valida instância Evolution ativa + conflito (phone, group, token) → 201
PATCH/api/council/whatsapp/presences/{id}adminAtualiza qualquer campo
DELETE/api/council/whatsapp/presences/{id}adminRemove → 204
POST/api/council/whatsapp/triggeradminDebug: simula inbound sem enviar real (send_callback=None)

Routing PABX

MétodoRotaAuthO que faz
GET/api/council/routing/rulessessionLista (filtro: is_active)
POST/api/council/routing/rulesadminCria regra → 201
PATCH/api/council/routing/rules/{id}adminAtualiza
DELETE/api/council/routing/rules/{id}adminRemove → 204
POST/api/council/routing/seed-defaults?overwrite=falseadminCria 7 regras padrão (idempotente)
POST/api/council/routing/simulateadminDry-run completo (classify → resolve → apply sem enviar)

Inbound hook

MétodoRotaAuthO que faz
POST/api/council/inbound/whatsappsessionHook do módulo whatsapp entrega inbound. send_real=true envia resposta real

Member roles

MétodoRotaAuthO que faz
GET/api/council/member-rolesadminLista
POST/api/council/member-rolesadminCadastra (phone_e164, role, label?, notes?) → 201
PATCH/api/council/member-roles/{id}adminAtualiza
DELETE/api/council/member-roles/{id}adminRemove → 204

Import histórico

MétodoRotaAuthO que faz
POST/api/council/groups/{group_jid}/import-history?phone_number_e164=...adminUpload .txt do export WhatsApp (50MB limit, cooldown 1h). Dedup por (thread, content, ±60s). Dispara recompute weekly retroativo (best-effort)

7. Tools do Brain

O Brain expõe ~38 tools que o LLM pode invocar durante o loop tool-use.

Read-only (sensores)

ToolImplementaçãoO que retorna
read_agent_statebridge.read_agent_stateEstado de um Agente (is_active, channels, recent_runs)
read_council_memoryservice.read_memoriesMemórias filtradas (kind, subject)
read_business_snapshotcross_module.business_snapshotSnapshot CRM + WA + Email + Billing + Members + Programs
search_contactscross_module.search_contactsCRM (filtros: score, lifecycle, tag)
read_contact_360cross_module.contact_360Snapshot completo de 1 contato
read_intelligence_insightscross_module.list_intelligence_insightsInsights por severity/sub_agent
read_revenue_summarycross_module.revenue_summaryPaid, overdue, MRR
read_group_digestdigests.recent_digestsDigests do grupo (week/month/quarter)
list_eventscross_module.list_eventsEventos cross-módulo (tracking)
read_contact_attributes / read_contact_attribute_historycross_moduleL1 Brain
read_patterns / read_observationscross_moduleL3/L5 Brain
read_attribution / read_contact_timelinecross_moduleAtribuição + timeline
compare_cohorts / list_top_riskscross_moduleAnálises
read_knowledge / search_knowledgecross_moduleMódulo business
read_products / read_contact_price_inquiries / read_contact_objections / read_contact_appointmentscross_moduleComercial
read_brain_configcross_moduleBrand voice + forbidden topics + horário
read_paid_media_summary / read_meta_top_ads / read_meta_campaign_insightscross_moduleHub.2 Meta Ads
read_ga4_traffic_summary / read_ga4_conversion_funnel / read_ga4_top_sources / read_ga4_event_breakdowncross_moduleHub.3 GA4
read_google_ads_* (4 tools)cross_moduleHub.4
read_instagram_* (6 tools)cross_moduleHub.5
read_youtube_* (6 tools)cross_moduleHub.6

Write / propose (atuadores)

ToolImplementaçãoO que faz
write_council_memoryservice.write_memoryRegistra memória de longo prazo
consult_advisorbrain.consult_advisorSub-turno isolado com outro conselheiro (depth max 1)
propose_actionservice.propose_actionCria council_action em proposed

Filtros:

  • Todos os conselheiros têm acesso ao vocabulário completo de tools.
  • O scope (observed_modules, allowed_intents) é aplicado pelo Brain após o tool_result — tool retorna {omitted: true, module: "X", reason: "module_not_in_scope"} se módulo fora.
  • Limit duro: nenhuma query retorna > 100 rows (defesa contra runaway costs).

8. Scope defaults por conselheiro

Aplicados na primeira leitura de service.get_advisor_scope(). Quem quiser alterar usa PUT /api/council/advisors/{slug}/scope.

ConselheiroMódulos observadosAgentes acionáveisIntents permitidos
CEOtudo (17 módulos)— (orquestrador puro)create_task, request_review, flag_risk
Strategycrm, billing, members, programs, forms, marketing, analytics, tracking, intelligence, businesscreate_task, request_review, flag_risk
Operationscrm, whatsapp, email, meetings, community, forms, members, programs, businesscreate_task, schedule_followup, request_review
Commercialcrm, whatsapp, email, meetings, billing, members, programs, forms, marketing, analytics, tracking, intelligence, businessclara, dunning, harisend_message, schedule_followup, adjust_segment, flag_risk, create_task
Content & Brandinstagram, youtube, analytics, tracking, community, forms, members, programs, intelligence, business, designauroracreate_task, send_broadcast, request_review
Intelligencetudo (17 módulos)flag_risk, adjust_segment, request_review
Relationshipcrm, whatsapp, email, instagram, meetings, community, forms, members, programs, businesshari, chandrasend_message, schedule_followup, flag_risk, create_task
Wellbeingmembers, programs, community, forms, intelligence, diarycreate_task

Princípio: Wellbeing é blindado de receita/CAC/marketing — vê só sinais humanos do dono.


9. Configuração

Env vars

VarDefaultPropósito
ANTHROPIC_API_KEYChave Claude (obrigatória — sem ela 503)
Modelo defaultclaude-opus-4-7Sobrescrito por config.model_tier

settings_identity flags

ColunaDefaultPropósito
council_enabledtrueKill switch global do Conselho (mig 0111). Independente de ai_auto_reply_enabled
ai_auto_reply_enabledtrueKill switch dos Agentes — não afeta Conselho direto, mas bloqueia despacho de ações

Kill switches em cascata

  1. Global do Conselho: settings_identity.council_enabled = false → nenhum conselheiro responde.
  2. Por conselheiro: council_advisor.is_active = false → aquele slug não responde.
  3. Por presence (grupo WA): council_whatsapp_presence.is_active = false → Conselho não responde naquele grupo.
  4. Por advisor + grupo: combinação dos 3 acima.

Config por conselheiro (council_advisor.config JSONB)

{
    "tone": "direto" | "reflexivo" | "analítico",
    "proactivity": "silent" | "moderate" | "active",
    "voice_enabled": True | False,            # ElevenLabs (planejado, Tier S)
    "model_tier": "fast" | "default" | "deep",  # Haiku | Sonnet | Opus
    "max_tokens_per_turn": 2048,
    "auto_dispatch": "never" | "low_risk_only" | "all",  # Tier Sociedade (não impl)
    "custom_instructions": "...",   # apêndice ao system prompt (max 4k chars)
    "avatar_url": "...",
    "accent_color": "#RRGGBB",
}

Config por presence (council_whatsapp_presence)

  • mention_token@conselho (default) ou outros tokens (multi-presence)
  • proactivity — silent/moderate/active
  • context_window_days — 1-90 (default 30)
  • max_context_tokens — 500-32000 (default 8000)
  • min_caller_role — anyone/staff/admin/owner

10. Operações

Como ligar/desligar

Kill switch global (UI): /admin/council/advisors → toggle no header. Kill switch global (API): PATCH /api/council/settings {council_enabled: false}. Por conselheiro (UI): mesma página, toggle no card. Por grupo WA: PATCH /api/council/whatsapp/presences/{id} {is_active: false}.

Troubleshooting

Sintoma: Mensagem @conselho no grupo não recebe resposta

Causas possíveis (em ordem de frequência):

  1. Kill switch desligado — checar GET /api/council/settings e council_advisor[slug].is_active.
  2. Presence inativaSELECT * FROM council_whatsapp_presence WHERE tenant_id=? AND group_external_id=? AND is_active=true.
  3. min_caller_role bloqueando — checar council_member_role.role do telefone autor vs min_caller_role da presence.
  4. Loop-mismatch no worker — ver memória [[celery-asyncpg-loop-mismatch]]. Sinal: log RuntimeError: Future attached to a different loop no worker. Fix está em prod desde commit a1356e5.
  5. Webhook URL errada — Evolution apontando pra domínio legacy. Checar: evolution_panel → webhook → url deve ser https://suite.mandir.com.br/api/whatsapp/webhooks/evolution/<instance>/<token>.
  6. RLS WhatsApp — query ad-hoc em whatsapp_provider_account sem set_config('whatsapp.tenant_id', uuid) retorna 0 rows mesmo conectado como dono. Ver memória [[whatsapp-provider-account-rls]].

Diagnóstico:

-- worker_session já seta tenant. Ad-hoc precisa:
SELECT set_config('whatsapp.tenant_id', '<uuid>', false);

-- Última intervenção da presence
SELECT id, group_external_id, is_active, last_intervention_at, primary_advisor_slug
FROM council_whatsapp_presence
WHERE tenant_id = '<uuid>'
ORDER BY last_intervention_at DESC NULLS LAST;

-- Logs do worker
docker logs --since 5m mandir-suite-worker | grep -E "council|outbound_sent|outbound_error|loop"

Sintoma: Mirror ficou em generating indefinidamente

Causa: Celery worker reiniciou no meio. Sem heartbeat de progress. Fix: POST /api/council/mirrors/{id}/run redispara a geração inline.

Sintoma: Conselheiro responde mas omite módulo X

Causa: scope.observed_modules não inclui X. Fix: GET /api/council/advisors/{slug}/scope → checar lista → PUT com módulo adicionado.

Runbooks vinculados


11. Métricas e observabilidade

Logs estruturados-chave

Logger keyQuando emiteCampos
council.routing.presence_matchPresence resolvida para grupo+tokenadvisor, group, tenant_id, token
council.routing.completedPipeline de inbound terminouacted, handler, rule, tenant_id, group, phone, receiving_phone
council.routing.outbound_sentMensagem entregue ao WhatsAppmsg_id, status, target, tenant_id
council.routing.outbound_errorFalha em enviarerror, tenant_id
council.llm_gateway.completionLLM respondeubackend, model, input_tokens, output_tokens, cache_create, cache_read, latency_ms, stop_reason, tool_calls
council.brain.tool_callTool invocadoadvisor, iteration, tenant_id, tool
council.process_inbound.errorWorker explodiutenant_id, error
council.mirror.worker.*Mirror generating/skipped/failedmirror_id, tenant_id, status
council.digests.tenant_scan_failedScan beat falhou pra tenanttenant_id, error

Dashboards

  • Grafana (planejado): latência mediana de turno, % de turnos com erro, custo (tokens × $/token), distribuição de tool calls por conselheiro.

Alertas (planejados)

  • Loop-mismatch > 0 ocorrências em 5min → page Felipe (significa que workers quebraram).
  • outbound_error > 3 em 5min → page (Conselho não entregando).
  • Anthropic 5xx > 10% em 1h → notify (degradação).

12. Limitações e débitos técnicos conhecidos

#ItemImpactoPlano
1Memory sem embeddings — busca por keyword/filtro, não similaridade semânticaMid — busca por contexto similar exige hackFase 2: pgvector + embeddings via voyage-3
2Mirror blueprint hardcoded — seções em mirror.py constantesMid — não dá pra adicionar seção custom via UIFase 2: tabela council_mirror_blueprint
3Tool definitions estáticastools.TOOL_DEFINITIONS montado em importLow — redeploy pra adicionar/removerOK (estável)
4Context preamble não cacheado — embora estável, vai em messages, não systemLow — paga input tokens mesmo quando estávelFase 2: refatorar pra mover pro system
5Multi-presence por token não determinístico além do match de tokenLow — colisão improvável; se acontecer, ordem de SELECT decideOK
6Provider_account_id em presence é snapshot — pode ficar staleLow (mig 0087 cravou phone_number_e164 como chave)OK
7Auto-despacho (Tier Sociedade) não implementadoconfig.auto_dispatch ignoradoMid — promessa de roadmapFase 3: aplicar em service.dispatch_approved_action
8Eventos canônicos não emitidos — só log estruturado, sem mandir-eventsMid — outros módulos não podem reagirFase 2: emit_event em service.append_message, propose_action, dispatch_action, mirror.start/complete
9Conversation cross-grupo collision — sem unique constraint em (owner, primary_advisor)Low — source_external_id distingue na práticaOK
10Cooldown PROACTIVE_COOLDOWN=1h global — não por advisor nem por grupoLowFase 3: por presence
11Wellbeing sem tools próprias além de cross_module — vê módulos limitados, mas tools genéricasMid — pode soar genérico nas respostasFase 2: tools específicas (read_diary_entries com escopo restrito, read_calendar_load, etc.)
12consult_advisor MAX_DEPTH=1Low — defesa contra recursão; ok pro fluxo atualOK
13Digest semanal sem fallback heurístico — se LLM falha, status=failedMid — semana fica sem digestFase 3: fallback regex (key_people = top-3 senders)
14Forms scope mas sem toolsforms está no scope de vários conselheiros mas cross_module.py não tem read_forms_*Mid — IA "tem permissão mas não tem instrumento"Fase 2: implementar read_forms_list, read_form_responses, aggregate_form_question

13. Histórico relevante

  • 2026-05-13 (commit ba46496) — 4 outros workers do council migrados de get_sessionworker_session (preventivo).
  • 2026-05-13 (commit a1356e5) — whatsapp_send_callback migrado de get_sessionworker_session. Resolveu "Conselho gera resposta mas não chega no WA".
  • 2026-05-13 (commit cd4c60b) — Platform engine isolado em workers via ContextVar _IN_WORKER_CONTEXT. Loop-mismatch ressurgia após TTL de 5min do DSN cache. Ver memória [[celery-asyncpg-loop-mismatch]].
  • 2026-05-13 (commit 8b7107e, mig 0111_council_kill) — Kill switch do Conselho separado do dos Agentes. settings_identity.council_enabled é independente de ai_auto_reply_enabled. Ver memória [[conselho-e-agentes-kill-switches-separados]].
  • 2026-05-13 (commit d28b54d) — Scope marketing split em 4 namespaces: marketing (paid only) + analytics (GA4) + instagram (organic+DM) + youtube. Defaults dos 8 advisors aplicados em prod.
  • 2026-05-13 (commit 1b329762) — 8 conselheiros (CEO + 7) seeded em prod no Mais Consciente via service. UX leigo-friendly deployada.
  • 2026-05-13 (commit b536768) — Workers Celery migrados pra usar tenant_router.worker_session (NullPool ad-hoc). Pipeline async do Conselho persistia inbound mas nunca chamava o brain.
  • 2026-05-12 (commit f7f0925, migs 0089-0093) — Fase async definitiva. 6 fases entregues:
    • F1: pipeline async (workaround removido)
    • F2: janela de contexto (build_group_context_block com 30 dias / 8k tokens)
    • F3: roles + min_caller_role (mig 0090, council_member_role)
    • F4: agentes em grupos (compartilhamento de presence com agents_whatsapp_presence)
    • F5: digests hierárquicos (week/month/quarter — mig 0092)
    • F6: import de chat.txt (rota /import-history)
  • 2026-05-11 (alembic 0084) — Marcos 1-13 deployados. Primeira versão do Conselho em prod: 7 conselheiros, brain com tool-use, UI básica.

Apêndices

A. Pipeline de Brain (turn detalhado)

# brain.respond(db, tenant_id, conversation_id, max_iterations=6)

1. Carregar conversation (council_conversation) + última msg (deve ser role=user).
2. Carregar advisor (council_advisor) + scope (council_advisor_scope).
3. context.build_system(advisor, tenant) → system prompt:
   CORE_SYSTEM_PROMPT + ADVISOR_CORE[slug] + (VERTICAL_OVERLAY[vertical][slug] || "")
4. context.build_context_preamble(tenant, advisor) → primeira user msg:
   tenant.display_name, vertical, plan, locale, advisor headline.
5. context.load_conversation_history(conversation_id, limit=40) → últimas 40 msgs.
6. Se source=whatsapp_group: build_group_context_block(group_jid, 30d, 8k tokens) → bloco markdown.
7. Loop tool-use (iteration=0; max_iterations=6):
   a. Chamar llm_gateway.complete(messages, tools, model=advisor.config.model_tier).
   b. stop_reason="tool_use" → executar tool_calls em paralelo:
      - read_*: cross_module / bridge / digests / service.
      - write_council_memory: service.write_memory.
      - consult_advisor: brain.consult_advisor (depth=1, isolado).
      - propose_action: service.propose_action.
   c. Adicionar tool_results às messages.
   d. iteration += 1. Volta pro 7a.
8. stop_reason="end_turn" ou iteration==max → texto final.
9. service.append_message(role=assistant, body, tool_calls log, tokens, model).
10. Retorna CouncilMessage.

B. Vertical overlays

Cada tenant.vertical (other / wellness / consulting / ecommerce / saas / education / coaching) tem overlay opcional por slug em prompts/verticals.py:

VERTICAL_OVERLAYS = {
  "wellness": {
    "ceo": "Você atua num negócio de bem-estar — entende que conversão é mais lenta...",
    "wellbeing": "Você é especialmente sensível ao tema do esgotamento em facilitadores...",
  },
  "consulting": {...},
  ...
}

Se vertical=other ou slug não existe no overlay → vazia (sem adição).

C. Glossário do módulo

  • Advisor / Conselheiro: um dos 8 (CEO + 7) com persona/scope/prompt próprios.
  • Presence: ligação (tenant, grupo WA, telefone, mention_token) que define onde o Conselho escuta.
  • Mirror: dossiê estruturado periódico (weekly/monthly/quarterly) com seções autoradas por conselheiros distintos.
  • Memory: memória de longo prazo cross-conversa, indexada por (kind, subject_type, subject_id).
  • Mention token: palavra/marcador que ativa o Conselho num grupo (default @conselho; multi-presence permite @estrategia, @comercial, etc.).
  • PABX routing: padrão "central telefônica" — classifica chamador → resolve regra por prioridade → aplica handler (council/agent/template/ignore).
  • Bridge: interface contratual entre Conselho e Agentes (ADR 0015). Conselho propõe council_action; Agente cria agents_run.
  • Tier Sociedade: plano premium futuro onde Conselheiro pode despachar ação automaticamente (sem aprovação humana) para intents low-risk.
  • Caller role rank: owner=4, admin=3, staff=2, student=1, external=0, anyone=0. Comparado com presence.min_caller_role pra gate.