Modelo de Dados¶
Diagrama de Relacionamentos¶
┌─────────────────┐
│ Tenant │
└────────┬────────┘
│
├──────────────┬──────────────┬──────────────┬──────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
│ User │ │ Agent │ │WhatsAppInstance│ │ LlmUsage │ │ Subscription │
└──────────┘ └────┬─────┘ └───────┬───────┘ └──────────┘ └──────┬───────┘
│ │ │
┌────────────┼────────┬───────┘ ┌───────────────┼───────────────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│Knowledge │ │ FAQ │ │ Agent │ │ Credit │ │ Subscription │ │ Subscription │
│ Block │ │ │ │ Step │ │ Transaction │ │ Event │ │ Item │
└────┬─────┘ └──────────┘ └────┬────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
│ │ │
▼ ┌────┴─────┐ ▼
┌──────────┐ │ Prompt │ ┌──────────────┐
│Knowledge │ │ Version │ │ Subscription │
│ Chunk │ └──────────┘ │ Product │
└──────────┘ └──────────────┘
Agent Agent / AgentStep
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Attachment │ │ FollowUpConfig│
└──────────────┘ └──────┬───────┘
│
┌─────────────┘
▼
┌──────────────────┐
│ConversationFollowUp│
└──────────────────┘
▲
│
┌──────┴───────┐
│ Conversation │
└──────┬───────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ Message │ │ Step │ │ Pending │
└────┬─────┘ │Transition│ │ Aggregation │
│ └──────────┘ └──────────────────┘
▼
┌──────────┐
│ Reaction │
└──────────┘
┌─────────────────┐ ┌─────────────────┐
│InstanceWhitelist│ │InstanceBlacklist│
└─────────────────┘ └─────────────────┘
▲ ▲
└────────┬───────────────┘
│
WhatsAppInstance
Subscription ──N:1──► CreditPackage
Nota: LlmUsage armazena referências (agentId, conversationId, etc.) em metadata JSONB
Entidades¶
Tenant¶
Empresa/organização que utiliza a plataforma (multi-tenancy).
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| Name | string | Nome da empresa |
| Timezone | string? | Fuso horário do tenant (IANA timezone ID) |
| IsActive | bool | Se o tenant está ativo |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Relacionamentos: - 1:N Users - 1:N Agents - 1:N WhatsAppInstances - 1:1 Subscription (opcional)
User¶
Usuários do portal administrativo.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| TenantId | Guid | FK Tenant (obrigatório) |
| string | Login único | |
| PasswordHash | string | Senha BCrypt |
| Name | string | Nome de exibição |
| IsAdmin | bool | Admin do tenant |
| IsSuperAdmin | bool | Admin global (acesso a páginas admin) |
| IsActive | bool | Se pode fazer login |
| MustChangePassword | bool | Força troca de senha no próximo login |
| OnboardingCompleted | bool | Se completou o onboarding |
| OnboardingDismissed | bool | Se dispensou o onboarding |
| CreatedAt | DateTime | Data de criação |
| LastLoginAt | DateTime? | Último acesso |
Regras de acesso:
- IsSuperAdmin = true: Acesso a páginas admin (Tenants, LLM Usage, Settings) — dados filtrados pelo seu tenant
- IsAdmin = true: Gerencia usuários e recursos do tenant
- IsAdmin = false: Acesso limitado ao tenant
Restrições: - Email deve ser único globalmente (não apenas por tenant)
Agent¶
Agentes IA que processam conversas.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| TenantId | Guid | FK Tenant |
| Name | string | Nome do agente ("Suporte TI") |
| Description | string? | Descrição interna |
| Prompt | string? | Prompt principal do agente (texto livre, nullable) |
| IsActive | bool | Se está processando mensagens |
| IsArchived | bool | Se o agente está arquivado |
| ArchivedAt | DateTime? | Data de arquivamento |
| ErrorMessage | string? | Mensagem customizada de erro |
| WelcomeMessage | string? | Mensagem de boas-vindas |
| RateLimitMessage | string? | Mensagem de rate limit |
| OutOfHoursMessage | string? | Mensagem fora do horário |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Relacionamentos: - 1:N WhatsAppInstances - 1:N AgentKnowledge - 1:N AgentAttachment - 1:N AgentFaq - 1:N AgentStep - 1:1 FollowUpConfig (opcional)
AgentStep¶
Etapas de atendimento que guiam a IA por um fluxo estruturado. Quando configuradas, a IA segue as instruções da etapa atual e avança conforme os objetivos são cumpridos.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| AgentId | Guid | FK Agent |
| TenantId | Guid | FK Tenant (denormalizado do Agent — elimina JOIN nos query filters) |
| Name | string | Nome da etapa (máx 100) |
| Instructions | string | Instruções da etapa |
| Order | int | Posição na sequência |
| IsActive | bool | Se está ativa no fluxo |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Comportamento: - Sem etapas: agente responde livremente usando apenas o prompt base - Com etapas: IA segue a etapa atual, pode avançar/voltar conforme necessidade - IA pode pular etapas se todas as anteriores foram atendidas - Ao completar todas as etapas, a conversa é resolvida automaticamente
Relacionamentos: - N:1 Agent (cascade delete) - 1:1 FollowUpConfig (opcional)
PromptVersion¶
Snapshot automático de prompts e instruções, criado a cada atualização de Agent.Prompt ou AgentStep.Instructions.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| AgentId | Guid | FK Agent |
| AgentStepId | Guid? | FK AgentStep (null = prompt do Agent) |
| Version | int | Número sequencial da versão |
| Content | string | Conteúdo do prompt/instrução no momento do snapshot |
| CreatedAt | DateTime | Data de criação |
Discriminação:
- AgentStepId == null: versão do Agent.Prompt
- AgentStepId != null: versão do AgentStep.Instructions
Comportamento: - Snapshot criado automaticamente ANTES de cada update (captura estado anterior) - Restaurar uma versão aplica o conteúdo e cria um novo snapshot do estado atual - Versões são sequenciais por escopo (por Agent ou por AgentStep)
Índices:
- UNIQUE (agent_id, agent_step_id, version) — sequencialidade
- (agent_id) WHERE agent_step_id IS NULL — histórico do agent
- (agent_step_id) WHERE agent_step_id IS NOT NULL — histórico do step
Relacionamentos: - N:1 Agent (cascade delete) - N:1 AgentStep (cascade delete, opcional)
AgentKnowledge¶
Base de conhecimento em blocos para compor o contexto do agente. O conteúdo é dividido em chunks (AgentKnowledgeChunk) para busca semântica com pgvector.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| AgentId | Guid | FK Agent |
| TenantId | Guid | FK Tenant (denormalizado do Agent — elimina JOIN nos query filters) |
| Title | string | Título do bloco |
| Content | string | Conteúdo (markdown, máx 25.000 caracteres) |
| DisplayOrder | int | Ordem de exibição |
| IsActive | bool | Se está incluído no prompt |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Busca Semântica: - O conteúdo é dividido em chunks (AgentKnowledgeChunk) que possuem embeddings individuais - Índice HNSW nos chunks para busca eficiente por similaridade - Retorna os chunks mais relevantes para cada pergunta - Veja knowledge-retrieval.md para detalhes
Relacionamentos: - N:1 Agent (cascade delete) - 1:N AgentKnowledgeChunk (cascade delete)
AgentKnowledgeChunk¶
Fragmento de um bloco de conhecimento com embedding vetorial para busca semântica.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| KnowledgeBlockId | Guid | FK AgentKnowledge |
| ChunkIndex | int | Posição do chunk dentro do bloco |
| Content | string | Conteúdo textual do chunk |
| Embedding | Vector? | Vetor de 1536 dimensões para busca semântica |
| EmbeddingUpdatedAt | DateTime? | Última atualização do embedding |
| CreatedAt | DateTime | Data de criação |
Busca Semântica:
- Embeddings gerados automaticamente ao criar/atualizar blocos de conhecimento
- Índice HNSW (vector_cosine_ops) para busca eficiente por similaridade
- Permite granularidade maior na recuperação de contexto relevante
Índices:
- (knowledge_block_id) — lookup por bloco
- (knowledge_block_id, chunk_index) — ordem dos chunks
- HNSW (embedding vector_cosine_ops) — busca semântica
Relacionamentos: - N:1 AgentKnowledge (cascade delete)
AgentAttachment¶
Arquivos anexados a um agente, armazenados no R2. Enviados junto com as respostas da IA quando relevantes.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| AgentId | Guid | FK Agent |
| TenantId | Guid | FK Tenant (denormalizado do Agent — elimina JOIN nos query filters) |
| FileName | string | Nome do arquivo (sanitizado) |
| MimeType | string | MIME type do arquivo |
| FileSizeBytes | long | Tamanho em bytes |
| StorageUrl | string | URL no R2 |
| DisplayName | string | Nome de exibição |
| Description | string? | Descrição do anexo |
| Tags | string? | Tags para categorização |
| IsActive | bool | Se está disponível para uso |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Relacionamentos: - N:1 Agent (cascade delete)
AgentFaq¶
Perguntas frequentes do agente. Cada FAQ é um par pergunta/resposta atômico que participa da busca semântica junto com a base de conhecimento.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| AgentId | Guid | FK Agent |
| TenantId | Guid | FK Tenant (denormalizado do Agent — elimina JOIN nos query filters) |
| Question | string | Pergunta (máx 500 caracteres) |
| Answer | string | Resposta |
| Embedding | Vector? | Vetor de 1536 dimensões para busca semântica |
| EmbeddingUpdatedAt | DateTime? | Última atualização do embedding |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Diferença de AgentKnowledge: - AgentKnowledge: Blocos de texto/documentos que são divididos em chunks para embedding - AgentFaq: Pares Q&A atômicos com embedding direto na entidade (não precisa de chunks)
Busca Semântica:
- Embeddings gerados no formato: "Pergunta: {Question}\nResposta: {Answer}"
- Participam da mesma busca semântica que AgentKnowledgeChunk
- Resultados combinados por relevância (distância de cosseno)
FollowUpConfig¶
Configuração de follow-up automático para um agente ou etapa. Exatamente um dono (agente OU etapa, nunca ambos).
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| AgentId | Guid? | FK Agent (mutuamente exclusivo com AgentStepId) |
| AgentStepId | Guid? | FK AgentStep (mutuamente exclusivo com AgentId) |
| Message | string | Mensagem de follow-up (máx 2000) |
| DelayMinutes | int | Delay em minutos antes do envio |
| MaxSends | int | Número máximo de envios (default 1) |
| AllowedHoursJson | string? | Horários permitidos para envio (JSONB) |
| IsActive | bool | Se está ativo |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Restrição de integridade:
- CHECK: (agent_id IS NOT NULL AND agent_step_id IS NULL) OR (agent_id IS NULL AND agent_step_id IS NOT NULL)
- UNIQUE filtrado em agent_id (no máximo um por agente)
- UNIQUE filtrado em agent_step_id (no máximo um por etapa)
Relacionamentos: - 1:1 Agent (cascade delete, opcional) - 1:1 AgentStep (cascade delete, opcional)
ConversationFollowUp¶
Instância de follow-up agendado para uma conversa específica. Rastreia envios e agendamento via TickerQ.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| ConversationId | Guid | FK Conversation |
| FollowUpConfigId | Guid | FK FollowUpConfig |
| TenantId | Guid | FK Tenant (para query filter) |
| SendCount | int | Número de envios realizados (default 0) |
| TickerJobId | Guid? | ID do job agendado no TickerQ |
| NextScheduledAt | DateTime? | Próximo envio agendado |
| LastSentAt | DateTime? | Último envio realizado |
| Status | ConversationFollowUpStatus | Scheduled, Completed, Cancelled |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Relacionamentos: - N:1 Conversation (cascade delete) - N:1 FollowUpConfig (cascade delete)
StepTransition¶
Registro de transição de etapa em uma conversa. Usado para auditoria e analytics do fluxo de atendimento.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| ConversationId | Guid | FK Conversation |
| AgentId | Guid | FK Agent |
| FromStepId | Guid? | FK AgentStep de origem (null = início) |
| ToStepId | Guid | FK AgentStep de destino |
| TransitionedAt | DateTime | Quando ocorreu a transição |
Relacionamentos: - N:1 Conversation (cascade delete) - N:1 Agent (cascade delete) - N:1 AgentStep (FromStep, SetNull on delete, opcional) - N:1 AgentStep (ToStep, Restrict on delete)
WhatsAppInstance¶
Instância de conexão com WhatsApp via Evolution API.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único (usado como nome da instância na Evolution API) |
| TenantId | Guid | FK Tenant |
| Name | string | Nome da instância |
| PhoneNumber | string? | Número conectado |
| Status | ConnectionStatus | Estado da conexão |
| AgentId | Guid? | FK Agent vinculado |
| StartsWithAI | bool | Inicia conversas em modo IA (default true) |
| AiAvailabilityHoursJson | string? | Horários de atendimento da IA (JSONB) |
| SignatureText | string? | Texto da assinatura nas mensagens |
| SignaturePosition | SignaturePosition | Posição da assinatura (default Disabled) |
| ShowTyping | bool | Mostra indicador de digitação (default true) |
| DelaySeconds | int | Delay antes de enviar resposta (0-10, default 0) |
| ReopenWindowMinutes | int | Janela em minutos para reabrir conversa resolvida (0-1440, default 10) |
| MarkAsReadOnAiReply | bool | Marca mensagem como lida após resposta da IA (default true) |
| MaxAiResponsesPerConversation | int? | Limite de respostas IA por conversa (null = default 50, 0 = ilimitado) |
| AllowImageAnalysis | bool | Habilita análise de imagem via Claude Vision (default true) |
| AllowAudioAnalysis | bool | Habilita análise de áudio via Whisper (default true) |
| MaxMediaPerMessage | int? | Limite de mídias analisadas por mensagem (1-10, null = default 3) |
| IsArchived | bool | Se a instância está arquivada |
| ArchivedAt | DateTime? | Data de arquivamento |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Análise de Mídia:
- AllowImageAnalysis e AllowAudioAnalysis controlam se a instância processa mídia recebida
- MaxMediaPerMessage limita quantas mídias são analisadas por mensagem (excedentes são salvas mas não processadas). Null usa o padrão do sistema (3)
- Quando desabilitado, a instância envia uma resposta padrão e nenhum crédito é consumido
- Custo: 1 crédito base (resposta IA) + 3 créditos por mídia processada. Ex: 2 imagens = 7 créditos (1 + 3 + 3)
- Veja media-processing.md e subscription-enforcement.md para detalhes
Relacionamentos: - N:1 Tenant (cascade delete) - N:1 Agent (SetNull on delete) - 1:N Conversations - 1:N InstanceWhitelist - 1:N InstanceBlacklist
InstanceWhitelist¶
Lista de contatos autorizados a interagir com a instância.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| InstanceId | Guid | FK WhatsAppInstance |
| PhoneNumber | string | Número no formato E.164 |
| ContactName | string? | Nome do contato |
| Notes | string? | Observações |
| IsActive | bool | Se está ativo |
| CreatedAt | DateTime | Data de criação |
Lógica: Se a whitelist estiver vazia ou todos desativados, aceita qualquer número.
InstanceBlacklist¶
Lista de contatos bloqueados (mensagens ignoradas).
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| InstanceId | Guid | FK WhatsAppInstance |
| PhoneNumber | string | Número no formato E.164 |
| Reason | string? | Motivo do bloqueio |
| BlockedAt | DateTime | Quando foi bloqueado |
| BlockedBy | Guid? | FK User que bloqueou |
Prioridade: Blacklist é verificada ANTES da whitelist.
Conversation¶
Sessão de atendimento entre um contato e a instância.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| InstanceId | Guid | FK WhatsAppInstance |
| AgentId | Guid? | FK Agent (pode ser null, SetNull on delete) |
| ContactPhone | string | Número do contato |
| ContactName | string? | Nome (pushName) |
| ContactProfilePictureUrl | string? | URL da foto de perfil |
| Status | ConversationStatus | Active, Resolved |
| Mode | ConversationMode | AI ou Manual |
| ModeChangedAt | DateTime? | Quando mudou de modo |
| StartedAt | DateTime | Início da conversa |
| LastMessageAt | DateTime | Última mensagem |
| LastReadAt | DateTime? | Última leitura do operador |
| ResolvedAt | DateTime? | Quando foi resolvida |
| Summary | string? | Resumo da conversa |
| CurrentStepId | Guid? | FK AgentStep — etapa atual do atendimento |
| StartedHour | int | Hora de início (0-23, pré-computado para dashboard) |
| StartedDayOfWeek | int | Dia da semana de início (0-6, pré-computado para dashboard) |
| ResolutionTimeMinutes | double? | Tempo de resolução em minutos (pré-computado ao resolver) |
| AiResponseCount | int | Contador de respostas IA na conversa (default 0, reseta ao trocar para AI ou reabrir) |
Colunas pré-computadas:
- StartedHour, StartedDayOfWeek e ResolutionTimeMinutes existem porque c.StartedAt.Date, (int)c.DayOfWeek e TimeSpan.TotalMinutes não são traduzíveis pelo Npgsql em GroupBy
- Preenchidos automaticamente no Create() e Resolve()
Relacionamentos: - N:1 WhatsAppInstance (cascade delete) - N:1 Agent (SetNull on delete, opcional) - N:1 AgentStep (CurrentStep, SetNull on delete, opcional) - 1:N Messages (cascade delete) - 1:N ConversationFollowUp (cascade delete)
Message¶
Mensagem individual em uma conversa.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| ConversationId | Guid | FK Conversation |
| Role | MessageRole | User, Assistant, System |
| Type | MessageType | Text, Image, Audio, etc. |
| Content | string? | Conteúdo textual / caption original |
| ExtractedText | string? | Texto extraído (transcrição ou descrição de imagem) |
| MediaUrl | string? | URL da mídia (R2) |
| MediaMimeType | string? | MIME type do arquivo |
| MediaFileName | string? | Nome do arquivo |
| SystemEventType | SystemEventType? | Tipo do evento de sistema (null para mensagens normais) |
| ActorName | string? | Quem causou o evento (nome do operador, agente IA, ou null para automático) |
| WhatsAppMessageId | string? | ID do WhatsApp (para status) |
| Status | MessageStatus | Pending, Sent, Delivered, Read, Failed |
| SentAt | DateTime? | Quando foi enviada |
| DeliveredAt | DateTime? | Quando foi entregue |
| ReadAt | DateTime? | Quando foi lida |
| CreatedAt | DateTime | Data de criação |
Tracking de Tokens:
- O tracking de tokens foi movido para a entidade LlmUsage
- Permite tracking granular por operação, modelo e tipo de uso
- Veja llm-usage.md para detalhes
Mensagens de Sistema:
- Role = System identifica mensagens de evento interno (nunca enviadas ao WhatsApp)
- SystemEventType categoriza o evento (cap atingido, falha IA, créditos esgotados, jailbreak bloqueado, etc.)
- ActorName registra quem causou: nome do operador (JWT), nome do agente IA, ou null (automático)
- Status = Read (sem lifecycle de entrega)
- Excluídas do histórico enviado à IA (LoadMessageHistoryStep)
- Visíveis apenas no portal Ciba
Processamento de Mídia:
- Áudios são transcritos via OpenAI Whisper → resultado em ExtractedText
- Imagens são descritas via Claude Vision → resultado em ExtractedText
- Veja media-processing.md para detalhes
Relacionamentos: - N:1 Conversation (cascade delete) - 1:N Reactions
Reaction¶
Reação (emoji) a uma mensagem.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| MessageId | Guid | FK Message |
| Emoji | string | Emoji da reação |
| SenderPhone | string | Quem reagiu |
| FromMe | bool | Se foi enviada pelo sistema |
| CreatedAt | DateTime | Data de criação |
PendingAggregation¶
Mensagens pendentes aguardando agregação antes do processamento. Usa PostgreSQL para garantias ACID e coordenação multi-instância.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| ConversationId | Guid | FK Conversation |
| InstanceId | Guid | FK WhatsAppInstance |
| AgentId | Guid? | FK Agent |
| ContactPhone | string | Número do contato |
| MessagesJson | string | Array JSON de mensagens pendentes (JSONB, default []) |
| FirstMessageAt | DateTime | Timestamp da primeira mensagem |
| LastMessageAt | DateTime | Timestamp da última mensagem |
| ProcessAt | DateTime | Quando processar (debounce — resetado a cada nova mensagem) |
| MaxWaitUntil | DateTime | Tempo máximo de espera (não resetado) |
| Status | AggregationStatus | Pending ou Processing |
| CreatedAt | DateTime | Data de criação |
Comportamento:
- ProcessAt é resetado a cada nova mensagem (debounce)
- MaxWaitUntil não é resetado, mantém o deadline original
- Status = Processing impede que outras instâncias processem simultaneamente
Relacionamentos: - N:1 Conversation (cascade delete) - N:1 WhatsAppInstance (cascade delete) - N:1 Agent (SetNull on delete, opcional)
LlmUsage¶
Registro de uso de LLM para tracking de tokens e custos.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| TenantId | Guid | FK Tenant |
| Provider | LlmProvider | Anthropic, OpenAI |
| Model | string | ID do modelo (claude-3-5-sonnet-20241022) |
| OperationType | LlmOperationType | Tipo da operação |
| InputTokens | int | Tokens de entrada |
| OutputTokens | int | Tokens de saída |
| CacheCreationInputTokens | int | Tokens de criação de cache (prompt caching) |
| CacheReadInputTokens | int | Tokens lidos do cache (prompt caching) |
| EstimatedCost | decimal? | Custo estimado em USD (inclui custo de cache) |
| Metadata | string? | Metadados estruturados (JSONB) — contém agentId, traceId, conversationId, whatsAppMessageIds e campos específicos por operação |
| CreatedAt | DateTime | Data de criação |
Relacionamentos: - N:1 Tenant
Índices:
- (tenant_id, created_at) - Consultas por período
- (tenant_id, operation_type, created_at) - Consultas por tipo
- (created_at) - Consultas globais por período
Subscription¶
Assinatura de um tenant. Controla limites de recursos e créditos de IA via modelo modular de precificação. Relação 1:1 com Tenant.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| TenantId | Guid | FK Tenant (unique) |
| Instances | int | Número de instâncias contratadas |
| ExtraAgents | int | Agentes adicionais além dos gratuitos |
| ExtraUsers | int | Usuários adicionais |
| ExtraKbBlocks | int | Blocos adicionais de base de conhecimento |
| ExtraStepBlocks | int | Blocos adicionais de etapas |
| ExtraAttachmentBlocks | int | Blocos adicionais de storage para anexos |
| ExtraFaqBlocks | int | Blocos adicionais de FAQs |
| MonthlyCreditPackage | int | Franquia mensal de créditos |
| CreditPackageId | Guid? | FK CreditPackage contratado |
| MonthlyPriceBrl | decimal | Preço mensal calculado em BRL |
| MaxAgents | int | Limite calculado de agentes |
| MaxUsers | int | Limite calculado de usuários |
| MaxKnowledgeDocs | int | Limite calculado de blocos de conhecimento |
| MaxSteps | int | Limite calculado de etapas (global, não por agente) |
| MaxFaqs | int | Limite calculado de FAQs |
| MaxAttachmentStorageMb | int | Limite calculado de storage para anexos (MB) |
| CreditBalance | int | Saldo de créditos (atualizado atomicamente) |
| Status | SubscriptionStatus | Estado do lifecycle |
| PastDueAt | DateTime? | Quando entrou em PastDue |
| SuspendedAt | DateTime? | Quando foi suspenso |
| CancelledAt | DateTime? | Quando foi cancelado |
| CurrentPeriodStart | DateTime | Início do ciclo atual |
| CurrentPeriodEnd | DateTime | Fim do ciclo atual |
| RowVersion | uint | Concurrency token (xmin) |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Fórmulas de limites (modelo modular):
Constantes base:
- FreeAgentCount = 2 (agentes gratuitos inclusos)
- KbPerAgent = 5, StepsPerAgent = 5, FaqsPerAgent = 100, AttachmentMbPerAgent = 100
- KbPerBlock = 5, StepsPerBlock = 5, FaqsPerBlock = 100, AttachmentMbPerBlock = 200
- UsersPerInstance = 2
Cálculos:
totalAgents = FreeAgentCount + ExtraAgents
MaxAgents = totalAgents
MaxUsers = (Instances × UsersPerInstance) + ExtraUsers
MaxKnowledgeDocs = (totalAgents × KbPerAgent) + (ExtraKbBlocks × KbPerBlock)
MaxSteps = (totalAgents × StepsPerAgent) + (ExtraStepBlocks × StepsPerBlock)
MaxAttachmentStorageMb = (totalAgents × AttachmentMbPerAgent) + (ExtraAttachmentBlocks × AttachmentMbPerBlock)
MaxFaqs = (totalAgents × FaqsPerAgent) + (ExtraFaqBlocks × FaqsPerBlock)
Enforcement (soft):
- Tenant sem subscription: sem restrições (graceful degradation)
- Downgrade: recursos existentes mantidos, novos bloqueados até count < limit
- Créditos: consumo atômico via SQL (credit_balance - N WHERE credit_balance >= N)
- Custo variável por tipo de operação: texto = 1 crédito, imagem = 3, áudio = 3 (configurado em CreditCostConfig)
Relacionamentos: - 1:1 Tenant - N:1 CreditPackage (Restrict on delete, opcional) - 1:N SubscriptionItem (cascade delete) - 1:N CreditTransactions - 1:N SubscriptionEvents
Índices:
- UNIQUE (tenant_id)
- (status, past_due_at) WHERE past_due_at IS NOT NULL — background job (grace period)
- (status, suspended_at) WHERE suspended_at IS NOT NULL — background job (suspension)
- (current_period_end) — consultas de renovação de período
SubscriptionItem¶
Item de linha de uma assinatura, representando um produto contratado com quantidade e preço.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| SubscriptionId | Guid | FK Subscription |
| ProductId | Guid | FK SubscriptionProduct |
| Quantity | int | Quantidade contratada (mínimo 1) |
| UnitPriceBrl | decimal | Preço unitário em BRL |
| UnitPriceUsd | decimal | Preço unitário em USD |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Relacionamentos: - N:1 Subscription (cascade delete) - N:1 SubscriptionProduct (Restrict on delete)
SubscriptionProduct¶
Catálogo de produtos disponíveis para composição de assinaturas.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| Code | string | Código único do produto (máx 50) |
| Name | string | Nome do produto (máx 100) |
| Description | string? | Descrição (máx 500) |
| PriceBrl | decimal | Preço de tabela em BRL |
| PriceUsd | decimal | Preço de tabela em USD |
| SortOrder | int | Ordem de exibição |
| IsActive | bool | Se está disponível para contratação |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Índices:
- UNIQUE (code)
- (sort_order)
Relacionamentos: - 1:N SubscriptionItem (Restrict on delete)
CreditPackage¶
Pacote de créditos disponível para contratação (mensal ou add-on).
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| Credits | int | Quantidade de créditos no pacote |
| PriceBrl | decimal | Preço em BRL |
| PriceUsd | decimal | Preço em USD |
| DiscountPercent | int | Percentual de desconto (0-100) |
| IsAddon | bool | Se é pacote adicional (vs. pacote mensal) |
| SortOrder | int | Ordem de exibição |
| IsActive | bool | Se está disponível para contratação |
| CreatedAt | DateTime | Data de criação |
| UpdatedAt | DateTime | Última atualização |
Índices:
- (is_active, is_addon, sort_order) — listagem filtrada
Relacionamentos: - 1:N Subscription (Restrict on delete)
CreditTransaction¶
Registro de auditoria de movimentação de créditos. Append-only (sem UpdatedAt).
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| SubscriptionId | Guid | FK Subscription |
| TenantId | Guid | FK Tenant (para query filter) |
| Type | CreditTransactionType | Tipo da transação |
| Amount | int | Quantidade (negativo = consumo) |
| BalanceAfter | int | Saldo após transação |
| Description | string? | Descrição (máx 500) |
| ConversationId | Guid? | Conversa que originou o consumo |
| MessageId | Guid? | Mensagem AI que gerou o consumo (vinculada após persist) |
| CreatedAt | DateTime | Data de criação |
Relacionamentos: - N:1 Subscription
Índices:
- (tenant_id, created_at) — consultas por período
- (subscription_id, created_at) — consultas por subscription
- (type) — consultas por tipo de transação
- (conversation_id, created_at) WHERE conversation_id IS NOT NULL — consultas por conversa
- UNIQUE (message_id) WHERE message_id IS NOT NULL — lookup por mensagem (1:1)
SubscriptionEvent¶
Registro de eventos do lifecycle da assinatura. Append-only (sem UpdatedAt). Usado para auditoria e histórico de alterações.
| Campo | Tipo | Descrição |
|---|---|---|
| Id | Guid | Identificador único |
| SubscriptionId | Guid | FK Subscription |
| TenantId | Guid | FK Tenant (para query filter) |
| Type | SubscriptionEventType | Tipo do evento |
| FromStatus | string? | Status anterior (em transições de estado) |
| ToStatus | string? | Status novo (em transições de estado) |
| Description | string? | Descrição legível do evento |
| Metadata | string? | Dados adicionais (JSON) |
| CreatedAt | DateTime | Data de criação |
Relacionamentos: - N:1 Subscription
Índices:
- (subscription_id, created_at) — consultas por subscription
- (type) — consultas por tipo de evento
- (subscription_id, type, created_at) — consultas compostas
Enums¶
SubscriptionStatus¶
Estado do lifecycle da assinatura.
| Valor | Descrição |
|---|---|
| Active | Assinatura ativa, todos os recursos disponíveis |
| AiQuotaExhausted | Créditos de IA esgotados, sistema funciona sem IA |
| PastDue | Pagamento vencido, grace period de 24h |
| Suspended | Suspenso após grace period, instâncias desconectadas |
| Cancelled | Cancelado após 30 dias de suspensão, créditos zerados |
CreditTransactionType¶
Tipo de movimentação de créditos.
| Valor | Descrição |
|---|---|
| MonthlyRenewal | Franquia mensal adicionada |
| AddOnPurchase | Compra de pacote adicional |
| AiConsumption | Consumo por resposta de IA |
| ManualAdjustment | Ajuste manual pelo admin |
| SuspensionZeroing | Zeramento por cancelamento |
SubscriptionEventType¶
Tipo de evento do lifecycle da assinatura.
| Valor | Descrição |
|---|---|
| Created | Assinatura criada |
| ConfigurationUpdated | Limites ou pacote alterados |
| CreditsAdded | Créditos adicionados (renovação ou compra) |
| AiQuotaExhausted | Créditos de IA esgotados |
| MarkedPastDue | Marcado como inadimplente |
| Suspended | Assinatura suspensa |
| Cancelled | Assinatura cancelada |
| Reactivated | Assinatura reativada |
| PeriodRenewed | Período renovado |
LlmProvider¶
Provider de LLM.
| Valor | Descrição |
|---|---|
| Anthropic | Claude API |
| OpenAI | OpenAI API |
LlmOperationType¶
Tipo de operação com LLM.
| Valor | Descrição |
|---|---|
| Chat | Resposta em conversa |
| Vision | Descrição de imagem |
| Transcription | Transcrição de áudio |
| Embedding | Geração de embeddings |
| Chunking | Processamento de knowledge |
| Playground | Testes no playground |
| QueryRewrite | Reescrita de query para busca semântica |
| AiAssistant | Assistente IA interno do portal |
| IntentClassification | Classificação de intenção da mensagem |
ConnectionStatus¶
Status de conexão da instância WhatsApp.
| Valor | Descrição |
|---|---|
| Disconnected | Desconectado |
| Connecting | Conectando |
| Connected | Conectado |
| Open | Conexão aberta |
| Close | Conexão fechada |
ConversationStatus¶
Status da conversa.
| Valor | Descrição |
|---|---|
| Active | Conversa ativa |
| Resolved | Conversa resolvida |
ConversationMode¶
Modo de atendimento da conversa.
| Valor | Descrição |
|---|---|
| AI | IA está atendendo |
| Manual | Humano está atendendo |
MessageRole¶
Papel do autor da mensagem.
| Valor | Descrição |
|---|---|
| User | Mensagem do usuário (cliente) |
| Assistant | Mensagem do assistente (IA ou operador) |
| System | Mensagem do sistema |
MessageType¶
Tipo de conteúdo da mensagem.
| Valor | Descrição |
|---|---|
| Text | Texto |
| Image | Imagem |
| Audio | Áudio (voice message) |
| Video | Vídeo |
| Document | Documento |
| Sticker | Sticker |
MessageStatus¶
Status de entrega da mensagem no WhatsApp.
| Valor | Descrição |
|---|---|
| Pending | Aguardando envio |
| Sent | Enviada (check) |
| Delivered | Entregue (check check) |
| Read | Lida (check check azul) |
| Failed | Falhou |
SystemEventType¶
Tipo de evento de sistema registrado em mensagens internas.
| Valor | Descrição |
|---|---|
| AiCapReached | Limite de respostas IA por conversa atingido |
| AiFailure | Falha na API do LLM (timeout, erro, parse) |
| CreditsExhausted | Créditos de IA esgotados |
| EscalatedByAi | IA decidiu escalar para humano |
| ResolvedByAi | IA resolveu a conversa |
| ResolvedByOperator | Operador resolveu a conversa |
| ModeChangedToManual | Modo mudou para manual (por operador) |
| ModeChangedToAi | Modo mudou para IA (por operador) |
| JailbreakBlocked | Tentativa de jailbreak bloqueada pelo guardrail |
| OffTopicBlocked | Mensagem fora do tópico bloqueada pelo guardrail |
SignaturePosition¶
Posição da assinatura nas mensagens.
| Valor | Descrição |
|---|---|
| Disabled | Sem assinatura (padrão) |
| StartSameLine | Início da mensagem, mesma linha |
| StartNewLine | Início da mensagem, nova linha |
| EndSameLine | Final da mensagem, mesma linha |
| EndNewLine | Final da mensagem, nova linha |
AggregationStatus¶
Status de uma agregação de mensagens pendentes.
| Valor | Descrição |
|---|---|
| Pending | Aguardando expiração do debounce/max-wait |
| Processing | Sendo processada pelo pipeline |
ConversationFollowUpStatus¶
Status de um follow-up de conversa.
| Valor | Descrição |
|---|---|
| Scheduled | Agendado para envio |
| Completed | Todos os envios realizados |
| Cancelled | Cancelado |
IntentCategory¶
Categoria de intenção classificada para mensagens recebidas.
| Valor | Descrição |
|---|---|
| FaqSimple | Pergunta simples respondível por FAQ |
| ComplexQuery | Consulta complexa que requer raciocínio |
| ActionRequest | Solicitação de ação/operação |
| OffTopic | Fora do tópico do agente |
| Jailbreak | Tentativa de manipulação/jailbreak |
Multi-tenancy¶
O sistema implementa multi-tenancy via query filters globais no EF Core:
// AppDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Filtro global por TenantId
modelBuilder.Entity<Agent>()
.HasQueryFilter(a => a.TenantId == _tenantProvider.TenantId);
}
- Todas as entidades com
TenantIdsão automaticamente filtradas - Todo usuário (incluindo SuperAdmin) tem
TenantIdobrigatório — dados filtrados pelo tenant do JWT TenantIdé extraído do JWT viaITenantProvider
Convenções de Banco¶
| Contexto | Convenção |
|---|---|
| Tabelas e colunas | snake_case (via NamingConventions) |
| Chaves primárias | id (Guid) |
| Chaves estrangeiras | {entidade}_id |
| Timestamps | created_at, updated_at |
| Soft delete | is_active (não usa deleted_at) |