Skip to content

Instantly share code, notes, and snippets.

@iagocavalcante
Last active April 1, 2026 15:40
Show Gist options
  • Select an option

  • Save iagocavalcante/0fee928e0e8edeeff9da333f541f99fd to your computer and use it in GitHub Desktop.

Select an option

Save iagocavalcante/0fee928e0e8edeeff9da333f541f99fd to your computer and use it in GitHub Desktop.
Arquitetura de Agentes Multiplos com Bases de Conhecimento Independentes - i9-agents-bot

Arquitetura de Agentes Multiplos com Bases de Conhecimento Independentes

Visao Geral

Este documento descreve a arquitetura necessaria para escalar o sistema de agentes do i9-agents-bot de um unico agente virtual para N agentes independentes, cada um com suas proprias bases de conhecimento (Vaults), personalidade, configuracoes de LLM e canais de atendimento.

Alem disso, introduz a camada de Data Sources — conexoes diretas com bancos MySQL dos clientes, onde cada cliente possui seu proprio schema. Atraves de Materialized Views, os agentes podem responder perguntas baseadas nos dados reais e atualizados de cada cliente (pedidos, estoque, financeiro, etc.), combinando RAG semantico com consulta estruturada.


Indice

  1. Arquitetura Atual
  2. Arquitetura Proposta para N Agentes
  3. Modelagem de Dados
  4. Fontes de Conhecimento (Knowledge Sources)
  5. Data Sources: Conexao com MySQL dos Clientes
  6. Materialized Views para Agentes
  7. Pipeline Hibrido: RAG + SQL Estruturado
  8. Fluxo de Conversa com Multiplos Agentes
  9. Pipeline RAG por Agente
  10. Migracoes Necessarias
  11. Exemplos de Configuracao
  12. Seguranca e Isolamento de Dados
  13. Consideracoes de Performance

Arquitetura Atual

Hoje o sistema opera com um unico agente virtual implicito. Todos os componentes compartilham o mesmo contexto:

Webhook → MessageProcessor → RAG Pipeline → LLM → Resposta
                                  ↓
                          Todos os Documentos
                          Todos os Vaults
                          Todos os Prompts

Limitacoes

Componente Limitacao
Vaults/Documentos Todos os documentos sao buscados globalmente — nao ha separacao por agente
Prompts Prompts sao globais (por key) — todos usam a mesma personalidade
Conversas Nao ha vinculo entre conversa e agente virtual especifico
LLM Config Modelo e parametros sao fixos no codigo (gpt-4o-mini)
Dados do Cliente Nenhuma conexao com o banco de dados real do cliente (MySQL)

Arquitetura Proposta para N Agentes

Cada Agente Virtual (Bot) e uma entidade independente com:

  • Identidade propria: nome, avatar, personalidade
  • Bases de conhecimento dedicadas: vaults e documentos vinculados
  • Data Sources: conexoes com schemas MySQL do cliente para dados em tempo real
  • Materialized Views: views pre-computadas para consultas frequentes
  • Configuracao de LLM: modelo, temperatura, tokens maximos
  • Prompts personalizados: personalidade, regras, fallbacks
  • Canais de atendimento: numeros de WhatsApp, webhooks
                    ┌─────────────────────────────┐
                    │      Webhook Recebido        │
                    └──────────────┬──────────────┘
                                   │
                    ┌──────────────▼──────────────┐
                    │   Identificar Agente Virtual │
                    │   (por telefone/canal/rota)  │
                    └──────────────┬──────────────┘
                                   │
              ┌────────────────────┼────────────────────┐
              │                    │                     │
     ┌────────▼───────┐  ┌────────▼───────┐  ┌─────────▼──────┐
     │   Agente "Loja" │  │ Agente "Suporte"│  │ Agente "Vendas"│
     │                  │  │                 │  │                │
     │ Vault: Produtos  │  │ Vault: FAQ      │  │ Vault: Precos  │
     │ Vault: Entregas  │  │ Vault: Manuais  │  │ Vault: Propostas│
     │                  │  │                 │  │                │
     │ MySQL: loja_abc  │  │ MySQL: suporte  │  │ MySQL: vendas  │
     │  → mv_pedidos    │  │  → mv_tickets   │  │  → mv_pipeline │
     │  → mv_estoque    │  │  → mv_sla       │  │  → mv_comissoes│
     │                  │  │                 │  │                │
     │ LLM: gpt-4o      │  │ LLM: gpt-4o-mini│  │ LLM: claude    │
     └────────┬────────┘  └────────┬────────┘  └────────┬───────┘
              │                    │                     │
              └────────────────────┼────────────────────┘
                                   │
                    ┌──────────────▼──────────────┐
                    │  Pipeline Hibrido            │
                    │  RAG (Vaults) + SQL (MySQL)  │
                    └──────────────┬──────────────┘
                                   │
                    ┌──────────────▼──────────────┐
                    │        Resposta ao Usuario    │
                    └─────────────────────────────┘

Modelagem de Dados

Diagrama Entidade-Relacionamento

┌──────────────────┐       ┌──────────────────────┐
│   virtual_agents │       │  virtual_agent_vaults │
├──────────────────┤       ├──────────────────────┤
│ id (PK)          │──┐    │ id (PK)              │
│ name             │  │    │ virtual_agent_id (FK)│──→ virtual_agents.id
│ slug             │  │    │ vault_id (FK)        │──→ vaults.id
│ description      │  └───→│ priority             │
│ avatar_url       │       │ inserted_at          │
│ personality      │       └──────────────────────┘
│ llm_provider     │
│ llm_model        │       ┌──────────────────────┐
│ llm_config       │       │ virtual_agent_prompts│
│ is_active        │       ├──────────────────────┤
│ metadata         │       │ id (PK)              │
│ inserted_at      │  ┌───→│ virtual_agent_id (FK)│
│ updated_at       │──┘    │ prompt_id (FK)       │──→ prompts.id
└──────────────────┘       │ inserted_at          │
         │                 └──────────────────────┘
         │
         │  ┌──────────────────────────┐
         │  │    data_sources          │
         │  ├──────────────────────────┤
         └─→│ id (PK)                 │
            │ virtual_agent_id (FK)   │──→ virtual_agents.id
            │ name                    │
            │ db_type (mysql/pg)      │
            │ host                    │
            │ port                    │
            │ database_name           │
            │ schema_name             │    ← schema do cliente no MySQL
            │ credentials (encrypted) │
            │ is_active               │
            │ sync_config             │
            └────────────┬────────────┘
                         │
            ┌────────────▼────────────┐
            │  materialized_views     │
            ├─────────────────────────┤
            │ id (PK)                 │
            │ data_source_id (FK)     │──→ data_sources.id
            │ virtual_agent_id (FK)   │──→ virtual_agents.id
            │ view_name               │
            │ source_query            │    ← SELECT original no MySQL
            │ description             │    ← descricao para o LLM entender
            │ column_descriptions     │    ← JSONB com descricao de cada coluna
            │ refresh_strategy        │    ← cron, on_demand, event_driven
            │ refresh_interval_minutes│
            │ last_refreshed_at       │
            │ row_count               │
            │ status                  │
            └─────────────────────────┘

┌──────────────────┐       ┌──────────────────────┐
│     vaults       │       │     documents        │
├──────────────────┤       ├──────────────────────┤
│ id (PK)          │──────→│ vault_id (FK)        │
│ name             │       │ id (PK)              │
│ description      │       │ title                │
│ status           │       │ content              │
│ metadata         │       │ embedding (vector)   │
└──────────────────┘       │ metadata             │
                           └──────────────────────┘

┌──────────────────┐
│  conversations   │
├──────────────────┤
│ id (PK)          │
│ virtual_agent_id │──→ virtual_agents.id  (NOVO)
│ phone_number     │
│ session_id       │
│ status           │
│ assigned_agent_id│──→ support_agents.id
│ ...              │
└──────────────────┘

┌──────────────────────────┐
│  virtual_agent_channels  │
├──────────────────────────┤
│ id (PK)                  │
│ virtual_agent_id (FK)    │──→ virtual_agents.id
│ channel_type             │
│ channel_identifier       │
│ is_active                │
│ metadata                 │
└──────────────────────────┘

Tabelas Necessarias

1. virtual_agents — Agentes Virtuais (Bots)

Tabela central que define cada agente virtual com sua configuracao completa.

CREATE TABLE virtual_agents (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            VARCHAR(255) NOT NULL,
    slug            VARCHAR(100) NOT NULL UNIQUE,
    description     TEXT,
    avatar_url      VARCHAR(500),

    -- Personalidade e Comportamento
    personality     TEXT,
    welcome_message TEXT,
    fallback_message TEXT,

    -- Configuracao do LLM
    llm_provider    VARCHAR(50) NOT NULL DEFAULT 'openai',
    llm_model       VARCHAR(100) NOT NULL DEFAULT 'gpt-4o-mini',
    llm_config      JSONB DEFAULT '{}',

    -- Configuracao do RAG
    rag_config      JSONB DEFAULT '{
        "retrieval_limit": 10,
        "rerank_limit": 5,
        "similarity_threshold": 0.7,
        "cache_ttl": 3600
    }',

    -- Configuracao de Embeddings
    embedding_provider VARCHAR(50) DEFAULT 'openai',
    embedding_model    VARCHAR(100) DEFAULT 'text-embedding-3-small',
    embedding_dimensions INTEGER DEFAULT 1536,

    -- Estado
    is_active       BOOLEAN DEFAULT true,
    metadata        JSONB DEFAULT '{}',

    inserted_at     TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX virtual_agents_slug_index ON virtual_agents(slug);
CREATE INDEX virtual_agents_is_active_index ON virtual_agents(is_active);

2. virtual_agent_vaults — Vinculo Agente ↔ Base de Conhecimento

CREATE TABLE virtual_agent_vaults (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    virtual_agent_id  UUID NOT NULL REFERENCES virtual_agents(id) ON DELETE CASCADE,
    vault_id          UUID NOT NULL REFERENCES vaults(id) ON DELETE CASCADE,
    priority          INTEGER DEFAULT 0,

    inserted_at       TIMESTAMP NOT NULL DEFAULT NOW(),

    UNIQUE(virtual_agent_id, vault_id)
);

CREATE INDEX virtual_agent_vaults_agent_index ON virtual_agent_vaults(virtual_agent_id);

3. virtual_agent_prompts — Prompts Personalizados por Agente

CREATE TABLE virtual_agent_prompts (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    virtual_agent_id  UUID NOT NULL REFERENCES virtual_agents(id) ON DELETE CASCADE,
    prompt_id         UUID NOT NULL REFERENCES prompts(id) ON DELETE CASCADE,

    inserted_at       TIMESTAMP NOT NULL DEFAULT NOW(),

    UNIQUE(virtual_agent_id, prompt_id)
);

4. virtual_agent_channels — Canais de Atendimento por Agente

CREATE TABLE virtual_agent_channels (
    id                 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    virtual_agent_id   UUID NOT NULL REFERENCES virtual_agents(id) ON DELETE CASCADE,
    channel_type       VARCHAR(50) NOT NULL,
    channel_identifier VARCHAR(255) NOT NULL,
    is_active          BOOLEAN DEFAULT true,
    metadata           JSONB DEFAULT '{}',

    inserted_at        TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at         TIMESTAMP NOT NULL DEFAULT NOW(),

    UNIQUE(channel_type, channel_identifier)
);

CREATE INDEX virtual_agent_channels_lookup_index ON virtual_agent_channels(channel_type, channel_identifier);

5. data_sources — Conexoes com Bancos de Dados dos Clientes

Armazena as credenciais e configuracoes de conexao com o MySQL de cada cliente. Cada cliente tem seu proprio schema no MySQL, e cada agente pode acessar um ou mais data sources.

CREATE TABLE data_sources (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    virtual_agent_id  UUID NOT NULL REFERENCES virtual_agents(id) ON DELETE CASCADE,
    name              VARCHAR(255) NOT NULL,       -- "Banco Loja ABC", "ERP Cliente X"
    description       TEXT,

    -- Conexao
    db_type           VARCHAR(20) NOT NULL DEFAULT 'mysql',  -- mysql, postgres, mssql
    host              VARCHAR(255) NOT NULL,
    port              INTEGER NOT NULL DEFAULT 3306,
    database_name     VARCHAR(255) NOT NULL,
    schema_name       VARCHAR(255),                -- schema especifico do cliente no MySQL
    credentials       BYTEA NOT NULL,              -- credenciais criptografadas (AES-256-GCM)

    -- Configuracao de Sincronizacao
    sync_config       JSONB DEFAULT '{
        "pool_size": 2,
        "timeout_ms": 15000,
        "ssl_enabled": true,
        "read_only": true
    }',

    -- Pool de Conexao Dinamico
    pool_id           VARCHAR(100),                -- identificador do pool no DynamicSupervisor

    -- Estado
    is_active         BOOLEAN DEFAULT true,
    last_connected_at TIMESTAMP,
    last_error        TEXT,
    metadata          JSONB DEFAULT '{}',

    inserted_at       TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at        TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX data_sources_agent_index ON data_sources(virtual_agent_id);
CREATE INDEX data_sources_active_index ON data_sources(is_active);

6. materialized_views — Views Pre-Computadas dos Dados do Cliente

Cada materialized view e uma "foto" pre-processada dos dados do MySQL do cliente, armazenada localmente no PostgreSQL para consulta rapida pelo agente. O LLM usa as descricoes das colunas para entender o que pode consultar.

CREATE TABLE materialized_views (
    id                       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    data_source_id           UUID NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
    virtual_agent_id         UUID NOT NULL REFERENCES virtual_agents(id) ON DELETE CASCADE,

    -- Definicao da View
    view_name                VARCHAR(255) NOT NULL,    -- "mv_pedidos_recentes"
    display_name             VARCHAR(255),             -- "Pedidos Recentes" (para o LLM)
    description              TEXT NOT NULL,             -- "Pedidos dos ultimos 90 dias com status, valor e cliente"
    source_query             TEXT NOT NULL,             -- SELECT no MySQL de origem
    local_table_name         VARCHAR(255) NOT NULL,    -- tabela PostgreSQL onde os dados ficam

    -- Metadados para o LLM (Text-to-SQL)
    column_descriptions      JSONB NOT NULL DEFAULT '{}',
    -- Exemplo:
    -- {
    --   "pedido_id": "Identificador unico do pedido",
    --   "cliente_nome": "Nome completo do cliente",
    --   "valor_total": "Valor total do pedido em reais (BRL)",
    --   "status": "Status do pedido: pendente, aprovado, enviado, entregue, cancelado",
    --   "data_criacao": "Data em que o pedido foi realizado"
    -- }

    -- Queries permitidas (seguranca)
    allowed_query_patterns   JSONB DEFAULT '[]',
    -- Exemplo: ["SELECT * WHERE status = ?", "SELECT COUNT(*) GROUP BY ?"]

    -- Exemplos de perguntas naturais que esta view responde
    example_questions         JSONB DEFAULT '[]',
    -- Exemplo: [
    --   "Quantos pedidos foram feitos hoje?",
    --   "Qual o valor total de vendas do mes?",
    --   "Quais pedidos estao pendentes?"
    -- ]

    -- Refresh
    refresh_strategy         VARCHAR(50) DEFAULT 'scheduled',  -- scheduled, on_demand, event_driven, realtime
    refresh_cron             VARCHAR(50) DEFAULT '*/30 * * * *', -- a cada 30 min
    refresh_interval_minutes INTEGER DEFAULT 30,
    last_refreshed_at        TIMESTAMP,
    next_refresh_at          TIMESTAMP,
    refresh_duration_ms      INTEGER,                  -- quanto tempo levou o ultimo refresh

    -- Estatisticas
    row_count                INTEGER DEFAULT 0,
    data_size_bytes          BIGINT DEFAULT 0,

    -- Estado
    status                   VARCHAR(50) DEFAULT 'pending',  -- pending, syncing, active, error, stale
    last_error               TEXT,
    is_active                BOOLEAN DEFAULT true,

    inserted_at              TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at               TIMESTAMP NOT NULL DEFAULT NOW(),

    UNIQUE(virtual_agent_id, view_name)
);

CREATE INDEX materialized_views_agent_index ON materialized_views(virtual_agent_id);
CREATE INDEX materialized_views_source_index ON materialized_views(data_source_id);
CREATE INDEX materialized_views_status_index ON materialized_views(status);

7. materialized_view_refresh_log — Historico de Sincronizacao

CREATE TABLE materialized_view_refresh_log (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    materialized_view_id UUID NOT NULL REFERENCES materialized_views(id) ON DELETE CASCADE,

    started_at          TIMESTAMP NOT NULL,
    finished_at         TIMESTAMP,
    duration_ms         INTEGER,
    rows_synced         INTEGER DEFAULT 0,
    status              VARCHAR(50) NOT NULL,   -- success, error, timeout, partial
    error_message       TEXT,
    metadata            JSONB DEFAULT '{}',     -- bytes_transferred, query_time, etc.

    inserted_at         TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX mv_refresh_log_view_index ON materialized_view_refresh_log(materialized_view_id);
CREATE INDEX mv_refresh_log_status_index ON materialized_view_refresh_log(status);

8. query_audit_log — Auditoria de Consultas do Agente

Registra todas as queries SQL que o agente executou contra os dados do cliente, para auditoria e seguranca.

CREATE TABLE query_audit_log (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    virtual_agent_id    UUID NOT NULL REFERENCES virtual_agents(id),
    materialized_view_id UUID REFERENCES materialized_views(id),
    conversation_id     UUID REFERENCES conversations(id),

    -- Query
    user_question       TEXT NOT NULL,        -- Pergunta original do usuario
    generated_sql       TEXT NOT NULL,        -- SQL gerado pelo LLM
    sanitized_sql       TEXT,                 -- SQL apos sanitizacao
    was_executed        BOOLEAN DEFAULT false,
    execution_time_ms   INTEGER,
    rows_returned       INTEGER,

    -- Seguranca
    risk_score          FLOAT DEFAULT 0.0,    -- 0.0 = seguro, 1.0 = bloqueado
    blocked             BOOLEAN DEFAULT false,
    block_reason        TEXT,

    inserted_at         TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX query_audit_agent_index ON query_audit_log(virtual_agent_id);
CREATE INDEX query_audit_conversation_index ON query_audit_log(conversation_id);

9. Alteracao em conversations

ALTER TABLE conversations
    ADD COLUMN virtual_agent_id UUID REFERENCES virtual_agents(id);

CREATE INDEX conversations_virtual_agent_index ON conversations(virtual_agent_id);

Fontes de Conhecimento (Knowledge Sources)

O sistema agora tem dois tipos de fontes de conhecimento que trabalham em conjunto:

Tipo 1: Vaults (Conhecimento Semantico — RAG)

Os Vaults sao colecoes de documentos para busca semantica via embeddings:

  • Conteudo textual (content)
  • Embedding vetorial (embedding — pgvector 1536 dimensoes)
  • Metadados (metadata — JSON com tags, fonte, etc.)

Ideal para: FAQs, manuais, politicas, tutoriais, documentacao.

Tipo 2: Data Sources + Materialized Views (Dados Estruturados — SQL)

Os Data Sources conectam ao MySQL do cliente e as Materialized Views trazem snapshots dos dados para consulta local:

  • Dados tabulares reais do cliente (pedidos, estoque, clientes, financeiro)
  • Atualizados periodicamente via cron ou sob demanda
  • Consultados via Text-to-SQL — o LLM gera queries SQL a partir de perguntas naturais

Ideal para: "Quantos pedidos temos hoje?", "Qual o estoque do produto X?", "Quanto faturamos no mes?"

Como Cada Agente Usa Suas Fontes

Pergunta do Usuario
       │
       ▼
┌──────────────────────────────┐
│   Classificador de Intencao  │
│   (LLM decide o tipo)       │
└──────┬───────────┬───────────┘
       │           │
  Semantica     Estruturada
       │           │
       ▼           ▼
┌────────────┐ ┌────────────────┐
│ RAG Search │ │ Text-to-SQL    │
│ (Vaults)   │ │ (Mat. Views)   │
│            │ │                │
│ Documentos │ │ Dados reais    │
│ similares  │ │ do cliente     │
└──────┬─────┘ └───────┬────────┘
       │               │
       └───────┬───────┘
               │
       ┌───────▼───────┐
       │ Contexto      │
       │ Combinado     │
       │ (docs + dados)│
       └───────┬───────┘
               │
       ┌───────▼───────┐
       │ LLM gera      │
       │ resposta final │
       └───────────────┘

Estrategias de Compartilhamento

Cenario 1: Vaults Exclusivos + Data Source Proprio
──────────────────────────────────────────────────
Agente A → [Vault Produtos] + [MySQL: schema_cliente_a → mv_pedidos, mv_estoque]
Agente B → [Vault Suporte]  + [MySQL: schema_cliente_b → mv_tickets]

Cenario 2: Vaults Compartilhados + Data Sources Isolados
─────────────────────────────────────────────────────────
Agente A → [Vault FAQ Global] ← Agente B
Agente A → [MySQL: schema_a]   (NUNCA compartilhado)
Agente B → [MySQL: schema_b]   (NUNCA compartilhado)

Cenario 3: Multiplos Data Sources por Agente
────────────────────────────────────────────
Agente A → [MySQL: erp_producao → mv_pedidos, mv_clientes]
Agente A → [MySQL: erp_financeiro → mv_faturamento, mv_inadimplencia]
Agente A → [Vault: Manual de Operacoes]

Data Sources: Conexao com MySQL dos Clientes

Arquitetura Multi-Schema

Cada cliente tem seu proprio schema no MySQL. O sistema se conecta de forma isolada e read-only.

┌─────────────────────────────────────────────────┐
│              MySQL Server do Cliente             │
│                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐│
│  │ schema_loja_a│  │ schema_loja_b│  │schema_c  ││
│  │             │  │             │  │          ││
│  │ pedidos     │  │ pedidos     │  │ vendas   ││
│  │ produtos    │  │ produtos    │  │ clientes ││
│  │ clientes    │  │ clientes    │  │ estoque  ││
│  │ estoque     │  │ financeiro  │  │          ││
│  └──────┬──────┘  └──────┬──────┘  └────┬─────┘│
│         │                │               │      │
└─────────┼────────────────┼───────────────┼──────┘
          │                │               │
     ┌────▼────┐      ┌───▼────┐     ┌───▼────┐
     │Agente A │      │Agente B│     │Agente C│
     │(read)   │      │(read)  │     │(read)  │
     └─────────┘      └────────┘     └────────┘

Gerenciamento de Conexoes Dinamicas em Elixir

Cada data source ativo inicia um pool de conexoes dinamico via DynamicSupervisor:

defmodule AgentsBot.DataSources.ConnectionManager do
  @moduledoc """
  Gerencia pools de conexao MySQL dinamicos para cada data source ativo.
  Cada pool e supervisionado independentemente — se um cliente
  ficar offline, os outros agentes nao sao afetados.
  """

  use GenServer

  def start_pool(data_source) do
    config = decrypt_credentials(data_source)

    child_spec = {
      MyXQL,
      hostname: config.host,
      port: config.port,
      database: config.database_name,
      username: config.username,
      password: config.password,
      pool_size: data_source.sync_config["pool_size"] || 2,
      ssl: data_source.sync_config["ssl_enabled"] || true,
      # CRITICO: conexao somente leitura
      after_connect: &set_read_only/1,
      name: pool_name(data_source.id)
    }

    DynamicSupervisor.start_child(
      AgentsBot.DataSources.PoolSupervisor,
      child_spec
    )
  end

  def query(data_source_id, sql, params \\ []) do
    pool = pool_name(data_source_id)
    MyXQL.query(pool, sql, params, timeout: 15_000)
  end

  defp set_read_only(conn) do
    MyXQL.query!(conn, "SET SESSION TRANSACTION READ ONLY")
  end

  defp pool_name(data_source_id) do
    :"data_source_pool_#{data_source_id}"
  end
end

Startup: Inicializar Pools Ativos

defmodule AgentsBot.DataSources.PoolSupervisor do
  use DynamicSupervisor

  def start_link(init_arg) do
    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one)
  end
end

# No Application.start/2, apos o Repo:
defmodule AgentsBot.Application do
  def start(_type, _args) do
    children = [
      AgentsBot.Repo,
      AgentsBot.DataSources.PoolSupervisor,
      # ... outros supervisores
    ]

    # Apos startup, iniciar pools para data sources ativos
    Task.async(fn ->
      AgentsBot.DataSources.list_active()
      |> Enum.each(&AgentsBot.DataSources.ConnectionManager.start_pool/1)
    end)
  end
end

Materialized Views para Agentes

O Que Sao

Materialized Views neste contexto sao tabelas PostgreSQL locais que contem uma copia processada dos dados do MySQL do cliente. Diferente de views SQL tradicionais, elas armazenam os dados fisicamente para consulta rapida.

Por Que Nao Consultar o MySQL Diretamente?

Motivo Explicacao
Latencia Consultar o MySQL do cliente a cada pergunta do usuario adiciona 100-500ms
Carga no Cliente Queries do agente nao devem impactar o banco de producao do cliente
Disponibilidade Se o MySQL do cliente cair, o agente continua respondendo com dados recentes
Seguranca Dados pre-processados permitem remover colunas sensiveis (CPF, senha, etc.)
Transformacao Podemos agregar, filtrar e normalizar dados antes de disponibilizar

Ciclo de Vida de uma Materialized View

1. DEFINICAO
   Admin configura:
   - source_query: "SELECT id, nome, valor, status, created_at FROM schema_a.pedidos WHERE created_at > NOW() - INTERVAL 90 DAY"
   - column_descriptions: { "id": "ID do pedido", "valor": "Valor em R$", ... }
   - refresh_cron: "*/30 * * * *"

2. SYNC (Refresh)
   Worker Oban/GenServer executa periodicamente:
   - Conecta ao MySQL via pool do data_source
   - Executa source_query
   - Trunca tabela local no PostgreSQL
   - Insere resultados em batch
   - Atualiza last_refreshed_at e row_count

3. CONSULTA
   Quando usuario pergunta algo:
   - LLM identifica qual materialized view responde
   - LLM gera SQL seguro contra a tabela local
   - Query executada no PostgreSQL (rapido, local)
   - Resultado formatado e incluido na resposta

4. MONITORAMENTO
   - Refresh log registra cada sincronizacao
   - Alertas se uma view fica "stale" (sem refresh)
   - Metricas de tempo de sync e rows transferidos

Worker de Sincronizacao

defmodule AgentsBot.DataSources.SyncWorker do
  @moduledoc """
  Worker que sincroniza materialized views do MySQL para o PostgreSQL local.
  Pode ser executado via Oban (cron) ou sob demanda.
  """

  def sync_view(materialized_view) do
    data_source = get_data_source(materialized_view.data_source_id)

    started_at = DateTime.utc_now()

    with {:ok, result} <- fetch_from_mysql(data_source, materialized_view.source_query),
         {:ok, count}  <- write_to_local(materialized_view.local_table_name, result.rows, result.columns) do

      update_view_status(materialized_view, %{
        status: "active",
        last_refreshed_at: started_at,
        row_count: count,
        refresh_duration_ms: DateTime.diff(DateTime.utc_now(), started_at, :millisecond)
      })

      log_refresh(materialized_view, :success, count, started_at)
    else
      {:error, reason} ->
        update_view_status(materialized_view, %{status: "error", last_error: inspect(reason)})
        log_refresh(materialized_view, :error, 0, started_at, inspect(reason))
    end
  end

  defp fetch_from_mysql(data_source, query) do
    ConnectionManager.query(data_source.id, query)
  end

  defp write_to_local(table_name, rows, columns) do
    Repo.transaction(fn ->
      # Trunca tabela local
      Repo.query!("TRUNCATE TABLE #{table_name}")

      # Insere em batches de 1000
      rows
      |> Enum.chunk_every(1000)
      |> Enum.each(fn batch ->
        placeholders = build_placeholders(batch, columns)
        values = List.flatten(batch)
        Repo.query!(
          "INSERT INTO #{table_name} (#{Enum.join(columns, ", ")}) VALUES #{placeholders}",
          values
        )
      end)

      length(rows)
    end)
  end
end

Criacao Automatica de Tabelas Locais

Quando uma materialized view e configurada, o sistema cria automaticamente a tabela PostgreSQL correspondente:

defmodule AgentsBot.DataSources.TableBuilder do
  @moduledoc """
  Cria tabelas PostgreSQL locais a partir do schema da query MySQL.
  Mapeia tipos MySQL → PostgreSQL automaticamente.
  """

  @type_mapping %{
    "int" => "INTEGER",
    "bigint" => "BIGINT",
    "varchar" => "TEXT",
    "text" => "TEXT",
    "decimal" => "NUMERIC",
    "datetime" => "TIMESTAMP",
    "date" => "DATE",
    "tinyint" => "BOOLEAN",
    "float" => "DOUBLE PRECISION",
    "double" => "DOUBLE PRECISION",
    "json" => "JSONB"
  }

  def create_local_table(materialized_view) do
    data_source = get_data_source(materialized_view.data_source_id)

    # Descobre colunas da query original
    {:ok, columns} = introspect_query(data_source, materialized_view.source_query)

    # Gera DDL para PostgreSQL
    ddl = """
    CREATE TABLE IF NOT EXISTS #{materialized_view.local_table_name} (
      _sync_id BIGSERIAL PRIMARY KEY,
      _synced_at TIMESTAMP DEFAULT NOW(),
      #{columns |> Enum.map(&column_ddl/1) |> Enum.join(",\n      ")}
    )
    """

    Repo.query!(ddl)

    # Cria indices para colunas frequentemente filtradas
    create_smart_indices(materialized_view.local_table_name, columns)
  end

  defp column_ddl(%{name: name, mysql_type: type}) do
    pg_type = Map.get(@type_mapping, type, "TEXT")
    "#{name} #{pg_type}"
  end
end

Pipeline Hibrido: RAG + SQL Estruturado

Classificacao de Intencao

O LLM primeiro classifica se a pergunta do usuario requer:

  1. Busca semantica (RAG em vaults) — perguntas sobre politicas, processos, como fazer
  2. Consulta estruturada (SQL em materialized views) — perguntas sobre dados, numeros, status
  3. Ambos — perguntas que combinam contexto e dados
defmodule AgentsBot.AI.IntentClassifier do
  @moduledoc """
  Classifica a intencao do usuario para rotear entre RAG e SQL.
  """

  @classify_prompt """
  Voce e um classificador de intencao. Dada a pergunta do usuario e as fontes disponiveis,
  classifique a intencao em uma das categorias:

  FONTES SEMANTICAS (Vaults):
  {{vault_descriptions}}

  FONTES ESTRUTURADAS (Views de Dados):
  {{view_descriptions}}

  Pergunta: {{question}}

  Responda APENAS com JSON:
  {"intent": "semantic" | "structured" | "hybrid", "sources": ["nome_da_fonte1", "nome_da_fonte2"]}
  """

  def classify(question, virtual_agent) do
    vaults = get_vault_descriptions(virtual_agent.id)
    views = get_view_descriptions(virtual_agent.id)

    prompt = @classify_prompt
    |> String.replace("{{vault_descriptions}}", format_vaults(vaults))
    |> String.replace("{{view_descriptions}}", format_views(views))
    |> String.replace("{{question}}", question)

    {:ok, response} = LLM.generate(prompt, virtual_agent.llm_config)
    Jason.decode!(response)
  end
end

Text-to-SQL Seguro

Quando a intencao e structured ou hybrid, o LLM gera SQL contra as materialized views locais:

defmodule AgentsBot.AI.TextToSQL do
  @moduledoc """
  Converte perguntas em linguagem natural para SQL seguro.
  Opera APENAS contra materialized views locais (PostgreSQL),
  nunca diretamente contra o MySQL do cliente.
  """

  @text_to_sql_prompt """
  Voce e um gerador de SQL. Gere uma query SELECT read-only para responder a pergunta.

  TABELA: {{table_name}}
  COLUNAS:
  {{column_descriptions}}

  REGRAS OBRIGATORIAS:
  - Apenas SELECT (nunca INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE)
  - Apenas a tabela especificada (nunca joins com outras tabelas)
  - LIMIT maximo de 100 linhas
  - Nao usar subqueries
  - Nao usar funcoes de sistema (pg_*, information_schema, etc.)
  - Para datas, usar formato ISO: '2026-01-15'

  PERGUNTA: {{question}}

  Responda APENAS com o SQL, sem explicacao.
  """

  def generate_sql(question, materialized_view) do
    prompt = build_prompt(question, materialized_view)
    {:ok, raw_sql} = LLM.generate(prompt, %{"temperature" => 0.0})

    # Validacao de seguranca em multiplas camadas
    with {:ok, sql} <- SQLSanitizer.validate(raw_sql),
         {:ok, sql} <- SQLSanitizer.ensure_read_only(sql),
         {:ok, sql} <- SQLSanitizer.enforce_limit(sql, 100),
         {:ok, sql} <- SQLSanitizer.check_table_access(sql, materialized_view.local_table_name) do
      {:ok, sql}
    else
      {:error, reason} -> {:blocked, reason}
    end
  end
end

SQL Sanitizer — Camada de Seguranca

defmodule AgentsBot.Security.SQLSanitizer do
  @moduledoc """
  Valida e sanitiza queries SQL geradas pelo LLM.
  Garante que apenas operacoes read-only sejam executadas.
  """

  @forbidden_keywords ~w(
    INSERT UPDATE DELETE DROP ALTER TRUNCATE CREATE GRANT REVOKE
    EXECUTE CALL INTO OUTFILE DUMPFILE LOAD_FILE pg_read_file
    pg_ls_dir information_schema pg_catalog
  )

  def validate(sql) do
    normalized = String.upcase(String.trim(sql))

    cond do
      not String.starts_with?(normalized, "SELECT") ->
        {:error, "Query deve comecar com SELECT"}

      has_forbidden_keyword?(normalized) ->
        {:error, "Query contem operacao proibida"}

      has_comment_injection?(sql) ->
        {:error, "Query contem padrao de injecao SQL"}

      String.contains?(normalized, ";") ->
        {:error, "Multiplas statements nao sao permitidas"}

      true ->
        {:ok, sql}
    end
  end

  def ensure_read_only(sql) do
    # Parse adicional com regex para detectar subqueries perigosas
    if Regex.match?(~r/\b(INTO|OUTFILE|DUMPFILE)\b/i, sql) do
      {:error, "Clausula de escrita detectada"}
    else
      {:ok, sql}
    end
  end

  def enforce_limit(sql, max_limit) do
    if Regex.match?(~r/\bLIMIT\s+\d+/i, sql) do
      # Substituir limit se for maior que o maximo
      sql = Regex.replace(~r/LIMIT\s+(\d+)/i, sql, fn _, n ->
        limit = min(String.to_integer(n), max_limit)
        "LIMIT #{limit}"
      end)
      {:ok, sql}
    else
      {:ok, sql <> " LIMIT #{max_limit}"}
    end
  end

  def check_table_access(sql, allowed_table) do
    # Verifica que a query so acessa a tabela permitida
    tables = extract_table_names(sql)
    if Enum.all?(tables, &(&1 == allowed_table)) do
      {:ok, sql}
    else
      {:error, "Acesso a tabelas nao permitidas: #{inspect(tables)}"}
    end
  end

  defp has_forbidden_keyword?(normalized_sql) do
    Enum.any?(@forbidden_keywords, fn kw ->
      Regex.match?(~r/\b#{kw}\b/, normalized_sql)
    end)
  end

  defp has_comment_injection?(sql) do
    Regex.match?(~r/(--|\/\*|\*\/|#)/, sql)
  end
end

Pipeline Completo

defmodule AgentsBot.AI.HybridPipeline do
  @moduledoc """
  Pipeline hibrido que combina RAG semantico com consulta estruturada.
  """

  def process(question, %{virtual_agent: agent} = context) do
    # 1. Classificar intencao
    intent = IntentClassifier.classify(question, agent)

    # 2. Buscar informacoes de acordo com a intencao
    {docs_context, data_context} = case intent["intent"] do
      "semantic" ->
        docs = search_vaults(question, context)
        {format_docs(docs), nil}

      "structured" ->
        data = query_materialized_views(question, intent["sources"], context)
        {nil, format_data(data)}

      "hybrid" ->
        docs = search_vaults(question, context)
        data = query_materialized_views(question, intent["sources"], context)
        {format_docs(docs), format_data(data)}
    end

    # 3. Gerar resposta final com todo o contexto
    generate_response(question, docs_context, data_context, context)
  end

  defp search_vaults(question, context) do
    # Pipeline RAG existente — busca nos vaults do agente
    vault_ids = context.vault_ids
    embedding = Embeddings.generate(question)
    Documents.find_similar(embedding, vault_ids, context.virtual_agent.rag_config)
  end

  defp query_materialized_views(question, source_names, context) do
    views = MaterializedViews.get_by_names(context.virtual_agent.id, source_names)

    Enum.map(views, fn view ->
      case TextToSQL.generate_sql(question, view) do
        {:ok, sql} ->
          # Auditar a query
          audit_query(context, view, question, sql)
          # Executar contra PostgreSQL local
          {:ok, result} = Repo.query(sql)
          %{view: view.display_name, columns: result.columns, rows: result.rows}

        {:blocked, reason} ->
          audit_blocked_query(context, view, question, reason)
          %{view: view.display_name, error: "Consulta bloqueada: #{reason}"}
      end
    end)
  end

  defp generate_response(question, docs_context, data_context, context) do
    prompt = """
    #{context.prompts.personality}

    #{if docs_context, do: "DOCUMENTACAO RELEVANTE:\n#{docs_context}\n"}
    #{if data_context, do: "DADOS DO SISTEMA:\n#{data_context}\n"}

    CONVERSA ANTERIOR:
    #{format_history(context.conversation)}

    PERGUNTA: #{question}

    Responda de forma natural, citando os dados quando relevante.
    Se os dados mostram numeros, inclua-os na resposta.
    """

    LLM.generate(prompt, context.virtual_agent.llm_config)
  end
end

Fluxo de Conversa com Multiplos Agentes

1. Roteamento Inicial

Mensagem Recebida (WhatsApp +5511999999999)
    │
    ▼
Consultar virtual_agent_channels
WHERE channel_type = 'whatsapp'
AND channel_identifier = '+5511999999999'
    │
    ▼
Encontrou agente? ──→ Sim: Usar agente encontrado
    │                       │
    ▼                       ▼
Nao: Usar agente       Criar/Retomar conversa
     padrao            com virtual_agent_id

2. Processamento da Mensagem

defmodule AgentsBot.Webhooks.MessageProcessor do
  def process_message(message, channel_info) do
    # 1. Identificar o agente virtual pelo canal
    virtual_agent = Agents.get_agent_by_channel(
      channel_info.type,
      channel_info.identifier
    )

    # 2. Obter ou criar conversa vinculada ao agente
    conversation = Conversations.get_or_create(
      message.phone_number,
      virtual_agent_id: virtual_agent.id
    )

    # 3. Carregar prompts e fontes do agente
    prompts = Prompts.get_for_agent(virtual_agent.id)

    # 4. Executar Pipeline Hibrido (RAG + SQL)
    response = HybridPipeline.process(message.content, %{
      virtual_agent: virtual_agent,
      conversation: conversation,
      prompts: prompts,
      vault_ids: Agents.get_vault_ids(virtual_agent.id),
      materialized_views: MaterializedViews.list_active(virtual_agent.id)
    })

    # 5. Enviar resposta pelo canal correto
    Messaging.send(channel_info, response)
  end
end

3. Handoff para Humano (Escopado por Agente)

CREATE TABLE virtual_agent_support_agents (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    virtual_agent_id  UUID NOT NULL REFERENCES virtual_agents(id) ON DELETE CASCADE,
    support_agent_id  UUID NOT NULL REFERENCES support_agents(id) ON DELETE CASCADE,

    inserted_at       TIMESTAMP NOT NULL DEFAULT NOW(),

    UNIQUE(virtual_agent_id, support_agent_id)
);

Pipeline RAG por Agente

Configuracao Independente

{
  "retrieval_limit": 10,
  "rerank_limit": 5,
  "similarity_threshold": 0.7,
  "cache_ttl": 3600,
  "handoff_enabled": true,
  "handoff_patterns": ["falar com humano", "atendente"],
  "max_conversation_history": 10,
  "enable_data_sources": true,
  "data_source_timeout_ms": 5000
}

LLM Config por Agente

{
  "provider": "openai",
  "model": "gpt-4o",
  "temperature": 0.3,
  "max_tokens": 1000,
  "top_p": 0.9,
  "frequency_penalty": 0.0,
  "presence_penalty": 0.0
}

Providers Suportados

Provider Modelos Uso Recomendado
OpenAI gpt-4o, gpt-4o-mini Uso geral, melhor custo-beneficio
Anthropic claude-3.5-sonnet, claude-3-haiku Respostas longas, analise complexa
Google gemini-1.5-pro, gemini-1.5-flash Multimodal, documentos longos

Migracoes Necessarias

Ordem de Execucao

# 1. Tabela de agentes virtuais
mix ecto.gen.migration create_virtual_agents

# 2. Vinculo agente-vault
mix ecto.gen.migration create_virtual_agent_vaults

# 3. Prompts por agente
mix ecto.gen.migration create_virtual_agent_prompts

# 4. Canais por agente
mix ecto.gen.migration create_virtual_agent_channels

# 5. Data sources (conexoes MySQL)
mix ecto.gen.migration create_data_sources

# 6. Materialized views
mix ecto.gen.migration create_materialized_views

# 7. Log de refresh
mix ecto.gen.migration create_materialized_view_refresh_log

# 8. Auditoria de queries
mix ecto.gen.migration create_query_audit_log

# 9. Adicionar virtual_agent_id em conversations
mix ecto.gen.migration add_virtual_agent_to_conversations

# 10. (Opcional) Support agents por agente virtual
mix ecto.gen.migration create_virtual_agent_support_agents

Migracao Principal

defmodule AgentsBot.Repo.Migrations.CreateVirtualAgents do
  use Ecto.Migration

  def change do
    create table(:virtual_agents, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :name, :string, null: false
      add :slug, :string, null: false
      add :description, :text
      add :avatar_url, :string
      add :personality, :text
      add :welcome_message, :text
      add :fallback_message, :text
      add :llm_provider, :string, null: false, default: "openai"
      add :llm_model, :string, null: false, default: "gpt-4o-mini"
      add :llm_config, :map, default: %{}
      add :rag_config, :map, default: %{
        "retrieval_limit" => 10,
        "rerank_limit" => 5,
        "similarity_threshold" => 0.7,
        "cache_ttl" => 3600
      }
      add :embedding_provider, :string, default: "openai"
      add :embedding_model, :string, default: "text-embedding-3-small"
      add :embedding_dimensions, :integer, default: 1536
      add :is_active, :boolean, default: true
      add :metadata, :map, default: %{}

      timestamps()
    end

    create unique_index(:virtual_agents, [:slug])
    create index(:virtual_agents, [:is_active])
  end
end

Exemplos de Configuracao

Exemplo 1: Agente de E-commerce com Dados de Pedidos

# Agente Virtual
%VirtualAgent{
  name: "LojaBot",
  slug: "lojabot",
  personality: "Assistente de loja online, ajuda com pedidos e produtos",
  llm_provider: "openai",
  llm_model: "gpt-4o",
  llm_config: %{"temperature" => 0.2, "max_tokens" => 1500}
}

# Data Source — MySQL do ERP
%DataSource{
  name: "ERP Loja ABC",
  db_type: "mysql",
  host: "erp.lojaabc.com.br",
  port: 3306,
  database_name: "erp",
  schema_name: "loja_abc",
  sync_config: %{"pool_size" => 2, "ssl_enabled" => true, "read_only" => true}
}

# Materialized View — Pedidos Recentes
%MaterializedView{
  view_name: "mv_pedidos_recentes",
  display_name: "Pedidos Recentes",
  description: "Pedidos dos ultimos 90 dias com status, valor e cliente",
  source_query: """
    SELECT
      p.id AS pedido_id,
      c.nome AS cliente_nome,
      p.valor_total,
      p.status,
      p.forma_pagamento,
      p.data_criacao,
      p.data_atualizacao
    FROM loja_abc.pedidos p
    JOIN loja_abc.clientes c ON c.id = p.cliente_id
    WHERE p.data_criacao > NOW() - INTERVAL 90 DAY
  """,
  local_table_name: "mv_lojabot_pedidos_recentes",
  column_descriptions: %{
    "pedido_id" => "Numero do pedido",
    "cliente_nome" => "Nome do cliente que fez o pedido",
    "valor_total" => "Valor total do pedido em reais (BRL)",
    "status" => "Status: pendente, aprovado, separando, enviado, entregue, cancelado",
    "forma_pagamento" => "Forma de pagamento: pix, cartao, boleto",
    "data_criacao" => "Data em que o pedido foi realizado",
    "data_atualizacao" => "Data da ultima atualizacao do status"
  },
  example_questions: [
    "Quantos pedidos temos hoje?",
    "Qual o valor total de vendas do mes?",
    "Quais pedidos estao pendentes?",
    "Quantos pedidos por pix no ultimo mes?"
  ],
  refresh_cron: "*/15 * * * *",
  refresh_interval_minutes: 15
}

# Materialized View — Estoque
%MaterializedView{
  view_name: "mv_estoque_atual",
  display_name: "Estoque Atual",
  description: "Estoque atual de todos os produtos com quantidade e preco",
  source_query: """
    SELECT
      p.sku,
      p.nome AS produto_nome,
      p.categoria,
      e.quantidade_disponivel,
      e.quantidade_reservada,
      p.preco_venda,
      e.atualizado_em
    FROM loja_abc.produtos p
    JOIN loja_abc.estoque e ON e.produto_id = p.id
    WHERE p.ativo = 1
  """,
  local_table_name: "mv_lojabot_estoque_atual",
  column_descriptions: %{
    "sku" => "Codigo SKU do produto",
    "produto_nome" => "Nome do produto",
    "categoria" => "Categoria do produto",
    "quantidade_disponivel" => "Unidades disponiveis para venda",
    "quantidade_reservada" => "Unidades reservadas em pedidos pendentes",
    "preco_venda" => "Preco de venda em reais",
    "atualizado_em" => "Data da ultima atualizacao do estoque"
  },
  example_questions: [
    "Tem estoque do produto X?",
    "Quais produtos estao com estoque baixo?",
    "Qual o preco do produto Y?"
  ],
  refresh_cron: "*/10 * * * *",
  refresh_interval_minutes: 10
}

Conversa de exemplo:

Usuario: "Quantos pedidos tivemos hoje?"
→ Intent: structured → mv_pedidos_recentes
→ SQL: SELECT COUNT(*) as total FROM mv_lojabot_pedidos_recentes WHERE data_criacao::date = CURRENT_DATE
→ Resultado: 47
→ Resposta: "Hoje tivemos 47 pedidos ate agora! Posso te dar mais detalhes?"

Usuario: "E qual o valor total?"
→ Intent: structured → mv_pedidos_recentes
→ SQL: SELECT SUM(valor_total) as total FROM mv_lojabot_pedidos_recentes WHERE data_criacao::date = CURRENT_DATE
→ Resultado: 15847.90
→ Resposta: "O valor total de vendas de hoje e R$ 15.847,90. Quer ver separado por forma de pagamento?"

Usuario: "Qual a politica de devolucao?"
→ Intent: semantic → Vault "Politicas da Loja"
→ RAG: busca documentos sobre devolucao
→ Resposta: "De acordo com nossa politica, voce tem ate 7 dias para solicitar devolucao..."

Exemplo 2: Agente de Suporte com Dados de Tickets

%VirtualAgent{
  name: "SuporteBot",
  slug: "suportebot",
  personality: "Assistente de suporte tecnico, objetivo e empático",
  llm_provider: "openai",
  llm_model: "gpt-4o-mini",
  llm_config: %{"temperature" => 0.1, "max_tokens" => 2000}
}

# Data Source
%DataSource{
  name: "Helpdesk MySQL",
  schema_name: "helpdesk_cliente_x"
}

# View — Tickets Abertos
%MaterializedView{
  view_name: "mv_tickets_abertos",
  display_name: "Tickets em Aberto",
  description: "Tickets de suporte abertos com prioridade, categoria e SLA",
  source_query: """
    SELECT t.id, t.titulo, t.categoria, t.prioridade,
           t.status, t.sla_vencimento, t.criado_em,
           c.nome AS cliente_nome, c.email AS cliente_email
    FROM helpdesk_cliente_x.tickets t
    JOIN helpdesk_cliente_x.clientes c ON c.id = t.cliente_id
    WHERE t.status IN ('aberto', 'em_andamento', 'aguardando_cliente')
  """,
  local_table_name: "mv_suportebot_tickets_abertos",
  refresh_cron: "*/5 * * * *"  -- A cada 5 min (dados criticos)
}

Exemplo 3: Agente Financeiro com Dados de Faturamento

%VirtualAgent{
  name: "FinBot",
  slug: "finbot",
  personality: "Assistente financeiro, preciso com numeros e datas",
  llm_provider: "anthropic",
  llm_model: "claude-3.5-sonnet",
  llm_config: %{"temperature" => 0.0, "max_tokens" => 2000}
}

# View — Faturamento Mensal
%MaterializedView{
  view_name: "mv_faturamento_mensal",
  display_name: "Faturamento Mensal",
  description: "Faturamento agregado por mes, categoria e forma de pagamento",
  source_query: """
    SELECT
      DATE_FORMAT(data_venda, '%Y-%m') AS mes,
      categoria_produto,
      forma_pagamento,
      COUNT(*) AS total_vendas,
      SUM(valor_liquido) AS faturamento_liquido,
      SUM(valor_bruto) AS faturamento_bruto,
      AVG(valor_liquido) AS ticket_medio
    FROM financeiro_cliente.vendas
    WHERE data_venda > DATE_SUB(NOW(), INTERVAL 12 MONTH)
    GROUP BY mes, categoria_produto, forma_pagamento
  """,
  local_table_name: "mv_finbot_faturamento_mensal",
  column_descriptions: %{
    "mes" => "Mes no formato YYYY-MM",
    "categoria_produto" => "Categoria do produto vendido",
    "forma_pagamento" => "Forma de pagamento utilizada",
    "total_vendas" => "Quantidade de vendas no periodo",
    "faturamento_liquido" => "Faturamento liquido em reais (descontadas taxas)",
    "faturamento_bruto" => "Faturamento bruto em reais",
    "ticket_medio" => "Valor medio por venda"
  },
  refresh_cron: "0 */6 * * *"  -- A cada 6 horas (dados consolidados)
}

Seguranca e Isolamento de Dados

Principios

┌─────────────────────────────────────────────────────────────┐
│                    CAMADAS DE SEGURANCA                      │
│                                                              │
│  1. CONEXAO         → read-only, SSL, credenciais cifradas  │
│  2. MATERIALIZATION → dados copiados, sem acesso direto     │
│  3. SQL SANITIZER   → valida toda query gerada pelo LLM    │
│  4. TABLE ACCESS    → agente so acessa suas proprias views  │
│  5. AUDIT LOG       → toda query registrada para auditoria  │
│  6. RATE LIMITING   → limite de queries por agente/minuto   │
└─────────────────────────────────────────────────────────────┘

Isolamento entre Agentes

REGRA: Um agente NUNCA pode acessar dados de outro agente.

Agente A → mv_agentea_pedidos    ✅
Agente A → mv_agenteb_tickets    ❌ BLOQUEADO

Implementacao:
- Tabelas locais prefixadas: mv_{agent_slug}_{view_name}
- Query sanitizer verifica prefixo antes de executar
- PostgreSQL RLS (Row Level Security) como camada adicional

Criptografia de Credenciais

defmodule AgentsBot.DataSources.Credentials do
  @moduledoc """
  Gerencia criptografia de credenciais de conexao.
  Usa AES-256-GCM com chave derivada do application secret.
  """

  @aad "data_source_credentials"

  def encrypt(%{username: user, password: pass}) do
    key = derive_key()
    iv = :crypto.strong_rand_bytes(16)
    plaintext = Jason.encode!(%{username: user, password: pass})

    {ciphertext, tag} = :crypto.crypto_one_time_aead(
      :aes_256_gcm, key, iv, plaintext, @aad, true
    )

    iv <> tag <> ciphertext
  end

  def decrypt(encrypted) do
    key = derive_key()
    <<iv::binary-16, tag::binary-16, ciphertext::binary>> = encrypted

    plaintext = :crypto.crypto_one_time_aead(
      :aes_256_gcm, key, iv, ciphertext, @aad, tag, false
    )

    Jason.decode!(plaintext)
  end

  defp derive_key do
    secret = Application.get_env(:agents_bot, :secret_key_base)
    :crypto.hash(:sha256, secret)
  end
end

Dados Sensiveis nas Materialized Views

A source_query deve excluir dados sensiveis na origem:

-- ❌ ERRADO: traz dados sensiveis
SELECT * FROM clientes

-- ✅ CORRETO: exclui PII desnecessario
SELECT
  id,
  CONCAT(LEFT(nome, 1), '***') AS nome_parcial,
  cidade,
  estado,
  -- CPF, email, telefone NAO incluidos
  data_cadastro
FROM clientes

Consideracoes de Performance

Cache por Agente

cache_key = "#{virtual_agent_id}:#{:crypto.hash(:sha256, query)}"

Refresh Inteligente

Nem todas as views precisam do mesmo intervalo:

Tipo de Dado Intervalo Sugerido Motivo
Estoque 5-10 min Muda frequentemente
Pedidos do dia 10-15 min Volume medio de mudancas
Faturamento mensal 6h Dados consolidados
Catalogo de produtos 1h Muda pouco
Dados historicos 24h Nao mudam

Observabilidade

# Telemetria para monitorar saude das materialized views
:telemetry.execute(
  [:agents_bot, :materialized_view, :refresh],
  %{duration_ms: duration, rows: row_count},
  %{view_name: view.view_name, agent_id: view.virtual_agent_id, status: :success}
)

:telemetry.execute(
  [:agents_bot, :text_to_sql, :query],
  %{execution_time_ms: time, rows_returned: count},
  %{agent_id: agent_id, view_name: view_name, blocked: false}
)

Pool de Conexoes por Provider

CircuitBreaker.create(:openai_chat, failure_threshold: 5)
CircuitBreaker.create(:anthropic_chat, failure_threshold: 5)
CircuitBreaker.create(:google_chat, failure_threshold: 5)

Rate Limiting por Agente

RateLimiter.check("agent:#{virtual_agent_id}", max_requests: 100, window: :timer.minutes(1))
RateLimiter.check("agent:#{virtual_agent_id}:sql", max_requests: 30, window: :timer.minutes(1))

Resumo das Tabelas

Tabela Descricao Relacoes
virtual_agents Definicao de cada agente virtual Central
virtual_agent_vaults N:N entre agentes e bases de conhecimento virtual_agentsvaults
virtual_agent_prompts Prompts personalizados por agente virtual_agentsprompts
virtual_agent_channels Canais de atendimento por agente virtual_agents → canais
virtual_agent_support_agents Atendentes humanos por agente virtual_agentssupport_agents
data_sources Conexoes com bancos MySQL dos clientes virtual_agents → MySQL schemas
materialized_views Views pre-computadas dos dados do cliente data_sources → tabelas locais PG
materialized_view_refresh_log Historico de sincronizacoes materialized_views → log
query_audit_log Auditoria de queries SQL do agente virtual_agents → auditoria
conversations (alterada) Adicionado virtual_agent_id conversationsvirtual_agents
vaults (existente) Bases de conhecimento Ja existe
documents (existente) Documentos com embeddings Ja existe
prompts (existente) Templates de prompts Ja existe
support_agents (existente) Atendentes humanos Ja existe

Proximos Passos

  1. Criar migracoes para as novas tabelas (incluindo data_sources e materialized_views)
  2. Implementar schemas Ecto para VirtualAgent, DataSource, MaterializedView
  3. Criar contexto Agents com funcoes CRUD e consultas
  4. Implementar ConnectionManager com pools dinamicos (MyXQL + DynamicSupervisor)
  5. Criar SyncWorker para refresh periodico das materialized views
  6. Implementar TextToSQL com sanitizacao em multiplas camadas
  7. Criar HybridPipeline combinando RAG + SQL
  8. Modificar MessageProcessor para identificar agente pelo canal
  9. Implementar SQLSanitizer e QueryAuditLog
  10. Criar interface de administracao para gerenciar agentes, data sources e views
  11. Implementar testes para isolamento entre agentes e seguranca SQL
  12. Adicionar metricas por agente no dashboard de analytics
  13. Configurar alertas para views stale e erros de sincronizacao

Documento gerado para o projeto i9-agents-bot — Abril 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment