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:
- Conhecimento (system prompt + tools que leem o DB) — Conselheiros têm scope diferente por persona.
- Decisão (ações propostas, aprovação humana, despacho) — separa "sugerir" de "executar". Audit + reversibilidade.
- 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_messagetemtool_calls, cadacouncil_mirror_sectiontemevidence.
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 (commitscd4c60b+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:
- Webhook Evolution chega em
/api/whatsapp/webhooks/evolution/<instance>/<token>. - Persiste em
whatsapp_message(RLS por tenant). - Enfileira task Celery
council.process_inbound_message. - Worker abre
worker_session(tenant_id)(NullPool isolada — fix loop-mismatch). routing.route_inbound_whatsapp_messageresolve presence (Conselho @conselho ativo no grupo), classifica caller (council_member_role.role=owner), aplica handler ROUTE_COUNCIL.brain.respondmonta system prompt (CORE + ADVISOR[ceo] + VERTICAL[other][ceo]), contexto (últimos 30 dias de mensagens do grupo, truncado em 8000 tokens), histórico de conversation.- LLM chama 5 tools em paralelo:
read_ga4_traffic_summary,read_ga4_conversion_funnel,read_ga4_top_sources(2x),read_ga4_event_breakdown. - Tools lêem das tabelas
ga4_*(persistidas pelo plugin Hub.3, atualizadas via observe). - LLM segunda iteração: gera resposta narrativa (1010 tokens out).
whatsapp_send_callbackenvia via Evolution + persiste outbound emwhatsapp_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:
- Task Celery cria
council_mirrorem statusgenerating. - Blueprint define 8 seções: business_health, real_customer, conversion_patterns, leaks, identity_dissonance, next_lever, wellbeing_check, content_audit.
- Para cada seção, brain._run_turn dispara o conselheiro autor (ex: business_health → Strategy + Operations) com instruções específicas.
- Cada seção é persistida em
council_mirror_sectioncomevidence(lista de tool_calls feitos pelo conselheiro). - Quando todas concluem, brain sintetiza
summary(3-5 frases) e marcastatus=ready. - 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:
- Brain (via tool
propose_action) criacouncil_actionem statusproposed:intent=send_message,target_agent_slug=clara,payload={target_phone, content},requires_approval=true.
- UI mostra ação proposta em
/admin/council/actionscom botão Aprovar/Rejeitar. - Felipe aprova →
POST /api/council/actions/{id}/decision {approve:true}→ statusapproved. - UI mostra botão Despachar →
POST /api/council/actions/{id}/dispatch→bridge.dispatch_to_agentcriaagents_run. - Agente Clara executa, retorna outcome,
council_action.outcomeé preenchido + statuscompleted.
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=falsepara 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_gatewaye 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
| Arquivo | Propósito |
|---|---|
models.py | ORM das 12 tabelas |
routes.py | 41 endpoints REST /api/council/* |
service.py | CRUD + transições de estado (sem LLM) |
brain.py | Orquestrador LLM (loop tool-use, consult_advisor) |
context.py | Builder de contexto (prompts + histórico + group block) |
routing.py | PABX router (classify caller → resolve rule → apply handler) |
whatsapp_presence.py | Conselho em grupos (mention, intervenção, cooldown) |
tasks.py | 5 tasks Celery (mirror, inbound, digests weekly/monthly/quarterly) |
bridge.py | Interface 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.py | Pipeline Mirror (blueprint, seções, síntese, compare) |
digests.py | Síntese hierárquica de grupos (week → month → quarter) |
tools.py | Vocabulário de tools (JSON Schema) |
scope_defaults.py | Defaults de escopo por conselheiro |
constants.py | Slugs canônicos |
persona.py | Schema AdvisorConfig (Pydantic) |
policy.py | Gates (_council_enabled, _advisor_is_active, _ai_auto_reply_enabled) |
prompts/builder.py | build_system_prompt(slug, vertical, overrides) |
prompts/core.py | CORE_SYSTEM_PROMPT (universal) |
prompts/advisors.py | ADVISOR_CORE_PROMPTS[slug] |
prompts/verticals.py | VERTICAL_OVERLAYS[vertical][slug] |
Tasks Celery
| Task | Quando dispara | Idempotente? | Retry |
|---|---|---|---|
council.process_inbound_message | Webhook WA inbound enfileira; transcrição de áudio também | Sem dedup formal (persistência prévia em whatsapp_message resolve) | max_retries=2, countdown 30s, skip erros lógicos |
council.generate_mirror | POST /mirrors com run_inline=false | Sim (checa status=generating) | max_retries=2, countdown 120s |
council.weekly_group_digests | Beat schedule (planejado: cron semanal segunda 04:00) | Upsert por UNIQUE(tenant, group, period_kind, period_start) | Sem retry (best-effort) |
council.monthly_group_digests | Beat schedule (planejado: cron primeiro dia do mês) | Upsert idem | Sem retry |
council.quarterly_group_digests | Beat schedule (planejado: cron primeiro dia do trimestre) | Upsert idem | Sem 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
| Provider | Como integra |
|---|---|
| Anthropic Claude | Via 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
| Slug | Nome amigável | Foco | Personalidade |
|---|---|---|---|
ceo | Conselheiro CEO | Visão integrada do negócio | Orquestrador; resposta default em grupos com @conselho |
strategy | Conselheiro de Estratégia | Visão de longo prazo, OKRs, alocação | Reflexivo, foco em 90d-quarter |
operations | Conselheiro de Operações | Execução, rotina, gargalos | Direto, prioriza ação |
commercial | Conselheiro Comercial | Vendas, conversão, pipeline | Analítico, métricas-first |
content_brand | Conselheiro de Conteúdo & Marca | Brand voice, presença pública, audiência | Sensível a tom; lê IG + YT + analytics |
intelligence | Conselheiro de Inteligência | Dados, padrões, anomalias, alertas | Pattern-finder; flag_risk frequente |
relationship | Conselheiro de Relacionamento | Comunicação 1:1, retenção, customer success | Empático; aciona Hari/Chandra |
wellbeing | Conselheiro de Bem-Estar | Sustentabilidade 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.
| Coluna | Tipo | Notas |
|---|---|---|
id | UUID PK | uuid_generate_v4() |
tenant_id | UUID | idx |
slug | VARCHAR(32) | ceo/strategy/operations/commercial/content_brand/intelligence/relationship/wellbeing |
display_name | VARCHAR(120) | "Conselheiro CEO" (personalizável) |
headline | VARCHAR(255) | "visão integrada do negócio" |
core_prompt_override | TEXT | NULL = usa prompts.ADVISOR_CORE_PROMPTS[slug] |
vertical_overlay_override | TEXT | NULL = deriva de tenant.vertical |
config | JSONB | {tone, proactivity, model_tier, voice_enabled, custom_instructions, max_tokens_per_turn, auto_dispatch, avatar_url, accent_color} |
is_active | BOOL | default true — kill switch por conselheiro |
created_at / updated_at | TIMESTAMP | now() |
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.
| Coluna | Tipo | Notas |
|---|---|---|
tenant_id | UUID | idx |
advisor_slug | VARCHAR(32) | idx |
observed_modules | JSONB | NULL = tudo; [] = nada; ["crm","whatsapp",...] |
observed_channels | JSONB | ex: ["whatsapp","instagram","email"] |
allowed_agent_slugs | JSONB | ex: ["hari","clara"] |
allowed_intents | JSONB | ex: ["send_message","create_task"] |
extra | JSONB | livre |
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.
| Coluna | Tipo | Notas |
|---|---|---|
tenant_id | UUID | idx |
owner_user_id | UUID | Identity.app_user.id (cross-DB, sem FK física) |
primary_advisor_slug | VARCHAR(32) | Conselheiro que "abriu" o fio |
title | VARCHAR(255) | Auto-gerado na 1ª mensagem; editável |
status | VARCHAR(16) | active / archived |
source | VARCHAR(32) | ui / whatsapp_group / whatsapp_dm / email |
source_external_id | VARCHAR(255) | Group JID se source=whatsapp_group |
last_message_at | TIMESTAMP(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).
| Coluna | Tipo | Notas |
|---|---|---|
conversation_id | UUID | FK council_conversation.id CASCADE |
role | VARCHAR(16) | user / assistant / system |
advisor_slug | VARCHAR(32) | Preenchido quando role=assistant |
body | TEXT | Conteúdo |
tool_calls | JSONB | {calls: [{name, input, result}, ...]} — audit trail |
tokens_in / tokens_out | INT | Para custo |
model | VARCHAR(64) | ex: claude-opus-4-7 |
Índices: (conversation_id, created_at).
council_action
Ação recomendada por Conselheiro — workflow proposed → approved → dispatched → completed.
| Coluna | Tipo | Notas |
|---|---|---|
advisor_slug | VARCHAR(32) | Quem propôs |
conversation_id | UUID | FK opcional (SET NULL) |
title | VARCHAR(255) | Título humano |
body | TEXT | Rationale completa |
intent | VARCHAR(64) | send_message / create_task / schedule_followup / flag_risk / adjust_segment / ... |
target_agent_slug | VARCHAR(64) | Agente executor (hari/clara/aurora/...) |
payload | JSONB | Parâmetros pro Agente: {content, target_email, target_phone, ...} |
status | VARCHAR(16) | proposed / approved / dispatched / completed / failed / rejected / expired |
requires_approval | BOOL | default true (Tier Sociedade pode setar false) |
bridge_run_id | UUID | agents_run.id (cross-módulo, sem FK) |
outcome | JSONB | Resultado reportado pelo Agente |
proposed_at / decided_at / dispatched_at / completed_at / expires_at | TIMESTAMP(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).
| Coluna | Tipo | Notas |
|---|---|---|
kind | VARCHAR(32) | fact / decision / pattern / relationship / dissonance |
advisor_slug | VARCHAR(32) | Quem registrou (NULL = compartilhado) |
subject_type | VARCHAR(32) | contact / deal / program / tenant / channel / global |
subject_id | UUID | ID do objeto referenciado |
title | VARCHAR(255) | Sumário |
body | TEXT | Descrição completa |
confidence | FLOAT | 0.0-1.0 |
source | JSONB | {channel, msg_ids, observed_at} |
expires_at | TIMESTAMP(tz) | NULL = permanente |
last_reinforced_at | TIMESTAMP(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).
| Coluna | Tipo | Notas |
|---|---|---|
kind | VARCHAR(16) | onboarding / weekly / monthly / quarterly / ad_hoc |
status | VARCHAR(16) | generating / ready / failed / archived |
title / summary | VARCHAR / TEXT | Síntese 3-5 frases quando ready |
requested_by | UUID | app_user.id (NULL = automático) |
error_message | TEXT | Se failed |
started_at / completed_at | TIMESTAMP(tz) |
Relacionamento: 1:N com council_mirror_section (CASCADE).
council_mirror_section
Seção individual do Mirror autorada por um conselheiro.
| Coluna | Tipo | Notas |
|---|---|---|
mirror_id | UUID | FK CASCADE |
advisor_slug | VARCHAR(32) | Autor |
section_kind | VARCHAR(32) | business_health / real_customer / conversion_patterns / leaks / identity_dissonance / next_lever / wellbeing_check / content_audit / custom |
title | VARCHAR(255) | |
body | TEXT | Markdown |
severity | VARCHAR(16) | info / warning / critical |
evidence | JSONB | {tool_calls, key_metrics} — auditoria |
status | VARCHAR(16) | generating / ready / failed |
tokens_in / tokens_out / model | ||
sort_order | INT | Ordem 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.
| Coluna | Tipo | Notas |
|---|---|---|
group_external_id | VARCHAR(255) | JID do grupo (...@g.us) |
group_label | VARCHAR(255) | Snapshot do nome (pode divergir do real) |
primary_advisor_slug | VARCHAR(32) | Padrão que responde |
phone_number_e164 | VARCHAR(32) | Chave canônica (mig 0087) — número do WA |
provider_account_id | UUID | Snapshot — não é chave funcional |
mention_token | VARCHAR(64) | @conselho (default). Multi-presence: @estrategia, @comercial, etc. |
proactivity | VARCHAR(16) | silent / moderate / active |
is_active | BOOL | Kill switch |
settings | JSONB | {auto_summarize_on: ["pricing"], quiet_hours: [22, 8]} |
last_intervention_at | TIMESTAMP(tz) | Cooldown |
context_window_days | INT | 1-90; padrão 30 |
max_context_tokens | INT | 500-32000; padrão 8000 |
min_caller_role | VARCHAR(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.
| Coluna | Tipo | Notas |
|---|---|---|
priority | INT | ASC — menor = mais específico |
name | VARCHAR(255) | "Owner em DM" |
conditions | JSONB | {is_dm: true, is_owner: true, lifecycle_stage: ["mql"], has_tag: ["vip"], crm_score_min: 70} |
route | JSONB | {handler: "council", advisor_slug: "commercial"} ou {handler: "agent", agent_slug: "hari"} ou {handler: "reply_template", template: "..."} ou {handler: "ignore"} |
is_active | BOOL | |
last_matched_at / match_count | Debug |
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).
| Coluna | Tipo | Notas |
|---|---|---|
phone_e164 | VARCHAR(32) | |
role | VARCHAR(16) | owner / admin / staff / student / external |
label | VARCHAR(255) | "Felipe — CEO" |
notes | TEXT |
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).
| Coluna | Tipo | Notas |
|---|---|---|
group_external_id | VARCHAR(255) | |
period_kind | VARCHAR(16) | week / month / quarter |
period_start / period_end | TIMESTAMP(tz) | Inclusivos |
message_count | INT | |
summary | TEXT | Prosa pt-BR (2-4 parágrafos) |
key_decisions / key_facts / key_people / themes / open_questions | JSONB array | |
sentiment | FLOAT | -1.0 a 1.0 |
generated_by_advisor | VARCHAR(32) | Mig 0092 |
status | VARCHAR(16) | ready / generating / failed |
Constraint: UNIQUE(tenant_id, group_external_id, period_kind, period_start) — upsert idempotente.
Relacionamentos cross-módulo
| Direção | Outro módulo | Como | Por quê |
|---|---|---|---|
| ↗ Lê | crm | cross_module.search_contacts, read_contact_360 | Contexto de lead/cliente |
| ↗ Lê | billing | cross_module.read_revenue_summary | MRR, overdue, custo de retenção |
| ↗ Lê | members | cross_module.business_snapshot | Membros ativos, plano |
| ↗ Lê | intelligence | cross_module.read_intelligence_insights, read_patterns, read_observations | L1/L3/L5 do Brain Observatory |
| ↗ Lê | business | cross_module.read_knowledge, read_products, read_objections | Catálogo + cotações + objeções |
| ↗ Lê | marketing, analytics (GA4), instagram, youtube | cross_module.read_meta_*, read_ga4_*, etc. (~25 tools) | Hubs 2-6 |
| ↗ Lê | whatsapp | context.build_group_context_block | Histórico do grupo |
| ↘ Escreve | agents | bridge.dispatch_to_agent → cria agents_run | Despacho de ação aprovada |
| ↘ Escreve | whatsapp | whatsapp.service.send_message | Resposta no grupo/DM |
| ↗ Lê | settings | policy._council_enabled, _ai_auto_reply_enabled | Kill switches |
| ↗ Lê | tenant_router | worker_session | DB-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étodo | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /api/council/settings | session | Lê settings_identity.council_enabled |
| PATCH | /api/council/settings | admin | Ativa/desativa Conselho global do tenant |
Conselheiros
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /api/council/advisors | session | Lista os 8 conselheiros (auto-seed se ausentes) |
| GET | /api/council/advisors/{slug} | session | Lê um conselheiro |
| PATCH | /api/council/advisors/{slug} | admin | Refina persona (display_name, headline, is_active, prompts, config) |
| GET | /api/council/advisors/{slug}/scope | session | Lê escopo |
| PUT | /api/council/advisors/{slug}/scope | admin | Substitui escopo completo |
| GET | /api/council/advisors/{slug}/preview-prompt | session | Renderiza system prompt final (debug) |
Conversas
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| POST | /api/council/conversations | session | Cria fio novo (primary_advisor_slug, title?) → 201 |
| GET | /api/council/conversations | session | Lista conversas do user (filtros: status, limit) |
| GET | /api/council/conversations/{id}/messages | session | Histórico (limit default 200) |
| POST | /api/council/conversations/{id}/messages | session | Turno atômico: persiste user msg → brain.respond → persiste assistant. Retorna {user_message, assistant_message}. 503 se Anthropic falha, rollback completo. |
Ações
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| POST | /api/council/actions | session | Propõe ação (advisor_slug, intent, target_agent_slug?, payload?, body?, conversation_id?, requires_approval?, expires_at?) → 201 |
| GET | /api/council/actions | session | Lista (filtros: status, advisor_slug, limit) |
| POST | /api/council/actions/{id}/decision | session | {approve: bool} → transição proposed → approved/rejected |
| POST | /api/council/actions/{id}/dispatch | session | approved → dispatched via bridge.dispatch_to_agent |
Memória
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| POST | /api/council/memory | session | Registra (kind, title, body?, advisor_slug?, subject_type?, subject_id?, confidence?, source?, expires_at?) → 201 |
| GET | /api/council/memory | session | Lê (filtros: kind, subject_type, subject_id, limit) |
Mirror
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| POST | /api/council/mirrors | admin | Dispara pipeline (kind, run_inline?) → 201. Se run_inline=true, executa síncrono (dev); senão Celery |
| GET | /api/council/mirrors | session | Lista (filtros: kind, status, limit) |
| GET | /api/council/mirrors/{id} | session | Lê completo com seções |
| GET | /api/council/mirrors/compare/latest?kind=X | session | Compara 2 mais recentes ready. 404 se < 2 |
| GET | /api/council/mirrors/compare?older_id&newer_id | session | Compara 2 específicos |
| POST | /api/council/mirrors/{id}/run | admin | Dispara síncrono mirror em generating (CLI/jobs) |
WhatsApp Presence
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /api/council/whatsapp/presences | session | Lista (filtros: is_active, phone_number_e164) com current_account_label decorado |
| POST | /api/council/whatsapp/presences | admin | Adiciona Conselho a grupo. Valida instância Evolution ativa + conflito (phone, group, token) → 201 |
| PATCH | /api/council/whatsapp/presences/{id} | admin | Atualiza qualquer campo |
| DELETE | /api/council/whatsapp/presences/{id} | admin | Remove → 204 |
| POST | /api/council/whatsapp/trigger | admin | Debug: simula inbound sem enviar real (send_callback=None) |
Routing PABX
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /api/council/routing/rules | session | Lista (filtro: is_active) |
| POST | /api/council/routing/rules | admin | Cria regra → 201 |
| PATCH | /api/council/routing/rules/{id} | admin | Atualiza |
| DELETE | /api/council/routing/rules/{id} | admin | Remove → 204 |
| POST | /api/council/routing/seed-defaults?overwrite=false | admin | Cria 7 regras padrão (idempotente) |
| POST | /api/council/routing/simulate | admin | Dry-run completo (classify → resolve → apply sem enviar) |
Inbound hook
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| POST | /api/council/inbound/whatsapp | session | Hook do módulo whatsapp entrega inbound. send_real=true envia resposta real |
Member roles
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /api/council/member-roles | admin | Lista |
| POST | /api/council/member-roles | admin | Cadastra (phone_e164, role, label?, notes?) → 201 |
| PATCH | /api/council/member-roles/{id} | admin | Atualiza |
| DELETE | /api/council/member-roles/{id} | admin | Remove → 204 |
Import histórico
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| POST | /api/council/groups/{group_jid}/import-history?phone_number_e164=... | admin | Upload .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)
| Tool | Implementação | O que retorna |
|---|---|---|
read_agent_state | bridge.read_agent_state | Estado de um Agente (is_active, channels, recent_runs) |
read_council_memory | service.read_memories | Memórias filtradas (kind, subject) |
read_business_snapshot | cross_module.business_snapshot | Snapshot CRM + WA + Email + Billing + Members + Programs |
search_contacts | cross_module.search_contacts | CRM (filtros: score, lifecycle, tag) |
read_contact_360 | cross_module.contact_360 | Snapshot completo de 1 contato |
read_intelligence_insights | cross_module.list_intelligence_insights | Insights por severity/sub_agent |
read_revenue_summary | cross_module.revenue_summary | Paid, overdue, MRR |
read_group_digest | digests.recent_digests | Digests do grupo (week/month/quarter) |
list_events | cross_module.list_events | Eventos cross-módulo (tracking) |
read_contact_attributes / read_contact_attribute_history | cross_module | L1 Brain |
read_patterns / read_observations | cross_module | L3/L5 Brain |
read_attribution / read_contact_timeline | cross_module | Atribuição + timeline |
compare_cohorts / list_top_risks | cross_module | Análises |
read_knowledge / search_knowledge | cross_module | Módulo business |
read_products / read_contact_price_inquiries / read_contact_objections / read_contact_appointments | cross_module | Comercial |
read_brain_config | cross_module | Brand voice + forbidden topics + horário |
read_paid_media_summary / read_meta_top_ads / read_meta_campaign_insights | cross_module | Hub.2 Meta Ads |
read_ga4_traffic_summary / read_ga4_conversion_funnel / read_ga4_top_sources / read_ga4_event_breakdown | cross_module | Hub.3 GA4 |
read_google_ads_* (4 tools) | cross_module | Hub.4 |
read_instagram_* (6 tools) | cross_module | Hub.5 |
read_youtube_* (6 tools) | cross_module | Hub.6 |
Write / propose (atuadores)
| Tool | Implementação | O que faz |
|---|---|---|
write_council_memory | service.write_memory | Registra memória de longo prazo |
consult_advisor | brain.consult_advisor | Sub-turno isolado com outro conselheiro (depth max 1) |
propose_action | service.propose_action | Cria 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.
| Conselheiro | Módulos observados | Agentes acionáveis | Intents permitidos |
|---|---|---|---|
| CEO | tudo (17 módulos) | — (orquestrador puro) | create_task, request_review, flag_risk |
| Strategy | crm, billing, members, programs, forms, marketing, analytics, tracking, intelligence, business | — | create_task, request_review, flag_risk |
| Operations | crm, whatsapp, email, meetings, community, forms, members, programs, business | — | create_task, schedule_followup, request_review |
| Commercial | crm, whatsapp, email, meetings, billing, members, programs, forms, marketing, analytics, tracking, intelligence, business | clara, dunning, hari | send_message, schedule_followup, adjust_segment, flag_risk, create_task |
| Content & Brand | instagram, youtube, analytics, tracking, community, forms, members, programs, intelligence, business, design | aurora | create_task, send_broadcast, request_review |
| Intelligence | tudo (17 módulos) | — | flag_risk, adjust_segment, request_review |
| Relationship | crm, whatsapp, email, instagram, meetings, community, forms, members, programs, business | hari, chandra | send_message, schedule_followup, flag_risk, create_task |
| Wellbeing | members, programs, community, forms, intelligence, diary | — | create_task |
Princípio: Wellbeing é blindado de receita/CAC/marketing — vê só sinais humanos do dono.
9. Configuração
Env vars
| Var | Default | Propósito |
|---|---|---|
ANTHROPIC_API_KEY | — | Chave Claude (obrigatória — sem ela 503) |
| Modelo default | claude-opus-4-7 | Sobrescrito por config.model_tier |
settings_identity flags
| Coluna | Default | Propósito |
|---|---|---|
council_enabled | true | Kill switch global do Conselho (mig 0111). Independente de ai_auto_reply_enabled |
ai_auto_reply_enabled | true | Kill switch dos Agentes — não afeta Conselho direto, mas bloqueia despacho de ações |
Kill switches em cascata
- Global do Conselho:
settings_identity.council_enabled = false→ nenhum conselheiro responde. - Por conselheiro:
council_advisor.is_active = false→ aquele slug não responde. - Por presence (grupo WA):
council_whatsapp_presence.is_active = false→ Conselho não responde naquele grupo. - 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/activecontext_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):
- Kill switch desligado — checar
GET /api/council/settingsecouncil_advisor[slug].is_active. - Presence inativa —
SELECT * FROM council_whatsapp_presence WHERE tenant_id=? AND group_external_id=? AND is_active=true. - min_caller_role bloqueando — checar
council_member_role.roledo telefone autor vsmin_caller_roleda presence. - Loop-mismatch no worker — ver memória [[celery-asyncpg-loop-mismatch]]. Sinal: log
RuntimeError: Future attached to a different loopno worker. Fix está em prod desde commita1356e5. - Webhook URL errada — Evolution apontando pra domínio legacy. Checar:
evolution_panel → webhook → urldeve serhttps://suite.mandir.com.br/api/whatsapp/webhooks/evolution/<instance>/<token>. - RLS WhatsApp — query ad-hoc em
whatsapp_provider_accountsemset_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
- WhatsApp Cloud Recovery — quando Meta Cloud cai.
- Observabilidade e Alertas — alertas Conselho.
11. Métricas e observabilidade
Logs estruturados-chave
| Logger key | Quando emite | Campos |
|---|---|---|
council.routing.presence_match | Presence resolvida para grupo+token | advisor, group, tenant_id, token |
council.routing.completed | Pipeline de inbound terminou | acted, handler, rule, tenant_id, group, phone, receiving_phone |
council.routing.outbound_sent | Mensagem entregue ao WhatsApp | msg_id, status, target, tenant_id |
council.routing.outbound_error | Falha em enviar | error, tenant_id |
council.llm_gateway.completion | LLM respondeu | backend, model, input_tokens, output_tokens, cache_create, cache_read, latency_ms, stop_reason, tool_calls |
council.brain.tool_call | Tool invocado | advisor, iteration, tenant_id, tool |
council.process_inbound.error | Worker explodiu | tenant_id, error |
council.mirror.worker.* | Mirror generating/skipped/failed | mirror_id, tenant_id, status |
council.digests.tenant_scan_failed | Scan beat falhou pra tenant | tenant_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
| # | Item | Impacto | Plano |
|---|---|---|---|
| 1 | Memory sem embeddings — busca por keyword/filtro, não similaridade semântica | Mid — busca por contexto similar exige hack | Fase 2: pgvector + embeddings via voyage-3 |
| 2 | Mirror blueprint hardcoded — seções em mirror.py constantes | Mid — não dá pra adicionar seção custom via UI | Fase 2: tabela council_mirror_blueprint |
| 3 | Tool definitions estáticas — tools.TOOL_DEFINITIONS montado em import | Low — redeploy pra adicionar/remover | OK (estável) |
| 4 | Context preamble não cacheado — embora estável, vai em messages, não system | Low — paga input tokens mesmo quando estável | Fase 2: refatorar pra mover pro system |
| 5 | Multi-presence por token não determinístico além do match de token | Low — colisão improvável; se acontecer, ordem de SELECT decide | OK |
| 6 | Provider_account_id em presence é snapshot — pode ficar stale | Low (mig 0087 cravou phone_number_e164 como chave) | OK |
| 7 | Auto-despacho (Tier Sociedade) não implementado — config.auto_dispatch ignorado | Mid — promessa de roadmap | Fase 3: aplicar em service.dispatch_approved_action |
| 8 | Eventos canônicos não emitidos — só log estruturado, sem mandir-events | Mid — outros módulos não podem reagir | Fase 2: emit_event em service.append_message, propose_action, dispatch_action, mirror.start/complete |
| 9 | Conversation cross-grupo collision — sem unique constraint em (owner, primary_advisor) | Low — source_external_id distingue na prática | OK |
| 10 | Cooldown PROACTIVE_COOLDOWN=1h global — não por advisor nem por grupo | Low | Fase 3: por presence |
| 11 | Wellbeing sem tools próprias além de cross_module — vê módulos limitados, mas tools genéricas | Mid — pode soar genérico nas respostas | Fase 2: tools específicas (read_diary_entries com escopo restrito, read_calendar_load, etc.) |
| 12 | consult_advisor MAX_DEPTH=1 | Low — defesa contra recursão; ok pro fluxo atual | OK |
| 13 | Digest semanal sem fallback heurístico — se LLM falha, status=failed | Mid — semana fica sem digest | Fase 3: fallback regex (key_people = top-3 senders) |
| 14 | Forms scope mas sem tools — forms 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 deget_session→worker_session(preventivo). - 2026-05-13 (commit
a1356e5) —whatsapp_send_callbackmigrado deget_session→worker_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, mig0111_council_kill) — Kill switch do Conselho separado do dos Agentes.settings_identity.council_enabledé independente deai_auto_reply_enabled. Ver memória [[conselho-e-agentes-kill-switches-separados]]. - 2026-05-13 (commit
d28b54d) — Scopemarketingsplit 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 usartenant_router.worker_session(NullPool ad-hoc). Pipeline async do Conselho persistia inbound mas nunca chamava o brain. - 2026-05-12 (commit
f7f0925, migs0089-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 criaagents_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_rolepra gate.