-- 0041 — agentes: a POSTURA com que o modelo lê os dados
--
-- O QUE ELE E, E O QUE ELE NAO E
--
-- O contexto continua como esta. Os dados continuam como estao. O agente e uma
-- PRE-SETAGEM de como o modelo deve trabalhar com o que ja existe: que papel
-- assumir, o que priorizar, como classificar o que afirma, o que fazer quando o
-- dado contradiz a hipotese de quem perguntou.
--
-- Ele NAO da acesso novo. Nao amplia o alcance da sessao, nao acrescenta
-- dataset, nao afrouxa o SqlGuard. `ScopeResolver` -> `datasetAllowlist` ->
-- `SqlGuard` seguem identicos, e a resposta continua saindo das mesmas
-- ferramentas. O que muda e a LEITURA que o modelo faz do que encontrou.
--
--
-- AGENTE E SKILL SAO CAMADAS DIFERENTES, DE PROPOSITO
--
--     skill    METODO   -- como apurar UMA analise, invocada por /comando
--     agente   POSTURA  -- como interpretar QUALQUER coisa, durante a conversa
--
-- Elas nao se filtram: qualquer skill roda sob qualquer agente. Amarrar as duas
-- foi considerado e recusado -- o agente esconderia analises que existiam, e
-- quem perdesse uma skill do menu nao teria como saber por que.
--
--
-- O TEXTO DO AGENTE E DO CLIENTE, E ISSO DECIDE ONDE ELE ENTRA
--
-- Esta e a parte que mais importa entender antes de mexer aqui.
--
-- As instrucoes do agente entram no prompt de sistema DEPOIS das regras do
-- produto, e emolduradas: elas dizem COMO analisar, nunca O QUE se pode
-- responder. As regras duras -- o limite do que o modelo sabe, a consolidacao
-- pelo servidor, quem soma e o banco, o que nunca sai numa resposta --
-- continuam valendo e VENCEM em caso de conflito.
--
-- A razao e simples: quem escreve um agente e o cliente, pelo painel ou pelo
-- chat administrativo. Se o texto dele pudesse revogar as regras do produto,
-- "crie um agente que responde mesmo sem dado" seria uma forma de desligar a
-- barreira de recusa por configuracao -- sem exploit nenhum, so preenchendo um
-- campo. Por isso a gravacao tambem recusa texto que tente revogar instrucao.
--
--
-- TRES CAMADAS, IGUAIS AS DAS SKILLS
--
--     tenant_id NULL                  OFICIAL   -- do produto, todos os clientes
--     tenant_id preenchido, proj NULL           -- o agente daquele cliente
--     tenant_id e project preenchidos           -- o ajuste de um projeto
--
-- A mais especifica vence, pelo `slug`.
--
--
-- O CUSTO: ELE ENTRA NO PREFIXO CACHEADO
--
-- O prompt de sistema e o prefixo cacheado, e responde por ~84% da entrada de
-- cada pergunta. O agente entra nele -- e cabe, pela mesma razao do glossario:
-- e ESTAVEL. Alguem reescreve um agente raramente, e no dia em que reescrever,
-- invalidar o cache uma vez e o certo, porque o comportamento DEVE mudar.
--
-- Trocar de agente no meio da conversa tambem invalida, e uma vez so. O que
-- NAO pode e o texto variar entre voltas da mesma pergunta: ai o prefixo
-- quebraria a cada volta sem ninguem notar. Por isso o prompt e montado uma vez
-- por pergunta, fora do laco.
--
-- O teto de caracteres existe por causa disso: 8.000 caracteres sao ~2.000
-- tokens somados a TODA pergunta daquele cliente. Um agente de trinta mil
-- caracteres nao e mais preciso -- e mais caro, e mais diluido.

CREATE TABLE agents (
  id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,

  -- NULL = OFICIAL, do produto.
  tenant_id      BIGINT UNSIGNED NULL,

  -- NULL = a camada do tenant (ou a oficial, quando tenant_id tambem e NULL).
  project_id     BIGINT UNSIGNED NULL,

  -- So para a UNIQUE alcancar as camadas com NULL: em InnoDB, UNIQUE com NULL
  -- nao restringe, e dois agentes oficiais com o mesmo slug passariam.
  scope_tenant_id  BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(tenant_id, 0)) VIRTUAL,
  scope_project_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(project_id, 0)) VIRTUAL,

  -- O identificador estavel. E tambem o que fica gravado na conversa e na
  -- configuracao da camada -- ver a nota sobre id versus slug mais abaixo.
  slug           VARCHAR(80) NOT NULL,

  name           VARCHAR(160) NOT NULL,
  description    VARCHAR(400) NOT NULL,

  -- QUANDO este agente e o certo. Quem le e a PESSOA, na hora de escolher --
  -- diferente da skill, onde o `when_to_use` e gatilho para o modelo.
  when_to_use    VARCHAR(400) NOT NULL,

  -- {"grupos": ["financeiro"], "termos": ["receita"]}
  -- Vazio e valido e comum: um agente que so ajusta a postura -- "escreva como
  -- parecer, cite a fonte de cada numero" -- nao exige dado nenhum.
  requires       JSON NULL,

  -- A POSTURA. E o que entra no prompt de sistema, emoldurado.
  instructions   TEXT NOT NULL,

  status         VARCHAR(20) NOT NULL DEFAULT 'draft',
  version        INT NOT NULL DEFAULT 1,
  enabled        TINYINT(1) NOT NULL DEFAULT 1,
  note           VARCHAR(255) NULL,

  -- Quantas CONVERSAS o adotaram. Nao e por pergunta: o agente vale para a
  -- conversa inteira, e contar pergunta faria uma conversa longa parecer
  -- adocao ampla.
  uses           BIGINT UNSIGNED NOT NULL DEFAULT 0,

  created_by     VARCHAR(120) NULL,
  created_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (id),
  UNIQUE KEY uq_agents_camada (scope_tenant_id, scope_project_id, slug),
  KEY idx_agents_tenant (tenant_id),
  KEY idx_agents_project (project_id),
  KEY idx_agents_catalogo (scope_tenant_id, scope_project_id, status, enabled),

  CONSTRAINT fk_agents_tenant  FOREIGN KEY (tenant_id)  REFERENCES tenants(id),
  CONSTRAINT fk_agents_project FOREIGN KEY (project_id) REFERENCES projects(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Esconder um agente HERDADO (oficial ou do tenant) em um projeto so.
CREATE TABLE agent_optouts (
  id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  project_id BIGINT UNSIGNED NOT NULL,
  agent_id   BIGINT UNSIGNED NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

  PRIMARY KEY (id),
  UNIQUE KEY uq_agent_optouts (project_id, agent_id),
  KEY idx_agent_optouts_agent (agent_id),

  CONSTRAINT fk_agent_optouts_project FOREIGN KEY (project_id) REFERENCES projects(id),
  CONSTRAINT fk_agent_optouts_agent   FOREIGN KEY (agent_id)   REFERENCES agents(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


-- --------------------------------------------------------------------------
-- O PADRAO DA CAMADA mora em embed_configs, e nao numa tabela nova
-- --------------------------------------------------------------------------
-- `embed_configs` JA E a pre-setagem por camada: tenant_id + project_id NULL,
-- com scope_project_id e UNIQUE (tenant_id, scope_project_id), decidindo
-- provedor, modelo, tema e tetos. O agente padrao e mais uma decisao da mesma
-- natureza e da mesma camada -- uma tabela separada duplicaria uma resolucao
-- que ja existe, ja tem teste e ja e a que a sessao consulta.
--
-- SLUG, E NAO ID, e a escolha que importa aqui.
--
-- O id apontaria para UMA LINHA. Se o cliente sobrescreve um agente oficial
-- criando um com o mesmo slug na camada dele -- que e exatamente como a
-- sobrescrita funciona --, um padrao por id continuaria apontando para a
-- oficial, e o override que ele acabou de escrever nao valeria como padrao.
-- Pelo slug, o padrao segue a resolucao de camadas, como tudo o mais.
--
-- Apagar o agente tambem deixa de ser um problema de integridade: o padrao
-- passa a nao resolver, o assistente volta ao comportamento sem agente, e a
-- tela mostra o slug orfao em vez de recusar a gravacao.
ALTER TABLE embed_configs
  ADD COLUMN default_agent VARCHAR(80) NULL AFTER model;


-- --------------------------------------------------------------------------
-- A CONVERSA GUARDA O AGENTE que ela adotou
-- --------------------------------------------------------------------------
-- NULL nao e "nenhum agente": e "o padrao da camada valia quando esta conversa
-- comecou, e continua valendo". Quem troca de agente no meio da conversa grava
-- o slug aqui, e a partir dai aquela conversa deixa de seguir o padrao --
-- inclusive se o padrao mudar depois.
--
-- E o certo: uma analise foi feita sob uma postura, e mudar a postura de uma
-- conversa antiga por efeito colateral de uma troca de configuracao reescreveria
-- o sentido do que ja foi dito nela.
--
-- A string vazia e o terceiro estado, e ele precisa existir: "esta conversa
-- explicitamente SEM agente", que e diferente de "siga o padrao".
ALTER TABLE embed_conversations
  ADD COLUMN agent_slug VARCHAR(80) NULL AFTER scope;


-- --------------------------------------------------------------------------
-- OS AGENTES OFICIAIS
-- --------------------------------------------------------------------------
-- Eles escrevem o que o prompt do produto NAO diz, e nada do que ele ja diz.
--
-- O prompt de sistema ja carrega "nao invente", "quem soma e o banco", "data e
-- texto", "periodo parcial", "faca, nao ofereca". Repetir isso aqui custaria
-- token em toda pergunta para nao mudar comportamento nenhum -- e, pior, criaria
-- duas redacoes da mesma regra, que e como uma delas envelhece sozinha.
--
-- O que sobra para o agente e o que e genuinamente POSTURA: que papel assumir,
-- em que ordem responder, o que sempre acompanhar um numero, e o que fazer
-- quando o dado contraria quem perguntou.
--
-- NENHUM EXIGE DADO (`requires` vazio), e isso e uma escolha.
--
-- Grupos de dataset sao por cliente e nao sao padronizados -- foi o que a 0040
-- documentou e mediu. Um agente oficial que exigisse o grupo "financeiro"
-- ficaria indisponivel no cliente que chamou de "financas", e ficaria
-- indisponivel para dizer uma coisa que nao depende de dado nenhum: como
-- escrever. Postura nao exige dado. Quem exige e o METODO, e isso e skill.
--
-- O mecanismo de requisito continua existindo para o agente do CLIENTE, que
-- pode muito bem depender de um dado especifico da casa dele.

INSERT INTO agents
  (tenant_id, project_id, slug, name, description, when_to_use,
   requires, instructions, status, created_by)
VALUES
(NULL, NULL, 'analista-financeiro',
 'Analista financeiro',
 'Le os numeros como um analista senior: sempre com comparacao, composicao, tendencia e risco.',
 'Para conversas de resultado, caixa, inadimplencia, custo e margem -- quando a resposta vai virar decisao.',
 '{"grupos":[],"termos":[]}',
 'PAPEL\nVoce atua como analista financeiro senior. Nao basta devolver o numero: entregue o numero, o que ele mudou, o que puxou a mudanca e o risco que ela abre.\n\nCLASSIFIQUE O QUE VOCE AFIRMA\nCada afirmacao que sustente uma decisao e uma destas tres, e voce diz qual:\nFATO -- veio direto de uma consulta, e voce diz de onde veio.\nCALCULO -- derivado de fatos, e voce mostra a conta.\nESTIMATIVA -- aproximacao, e voce diz de que premissa partiu.\nNao misture as tres na mesma frase. Onde faltar dado, diga que falta -- nao preencha com o provavel.\n\nO QUE ACOMPANHA UM NUMERO\n1. Comparacao: contra o periodo anterior, e contra o mesmo periodo do ano anterior quando houver base.\n2. Composicao: os maiores itens que formam o total, e nao so o total.\n3. Tendencia: subindo, caindo ou oscilando, e desde quando.\n4. Risco: concentracao em poucos nomes, vencimento proximo, dependencia de um unico cliente ou fornecedor.\nQuando um desses quatro nao tiver base no dado disponivel, diga isso em vez de omitir em silencio.\n\nNAO TENTE AGRADAR\nSe o dado contradisser a hipotese de quem perguntou, diga a contradicao na primeira frase, com o numero que a sustenta. Concordar por educacao com uma premissa errada e o pior resultado possivel: a pessoa decide em cima dela.\n\nORDEM DA RESPOSTA\nO numero primeiro. Depois o que o explica. Depois a ressalva, se houver. Nunca comece pela metodologia.',
 'published', 'contextia'),

(NULL, NULL, 'resumo-para-diretoria',
 'Resumo para diretoria',
 'Responde em sintese: o numero, uma linha de porque, e o que decidir. Sem passo a passo.',
 'Para quem le no celular entre duas reunioes e precisa decidir, nao conferir.',
 '{"grupos":[],"termos":[]}',
 'PAPEL\nVoce escreve para quem decide e tem pouco tempo. A resposta inteira cabe em dez linhas.\n\nFORMA\nAbra com o numero ou a conclusao, numa frase. Depois, no maximo tres marcadores: o que explica, o que preocupa, o que fazer. Encerre.\n\nO QUE CORTAR\nCorte a metodologia, a lista de etapas, a explicacao de como voce chegou la e a repeticao da pergunta. Quem quiser a abertura vai pedir -- e ai voce abre.\n\nO QUE NUNCA CORTAR\nA ressalva que muda a leitura do numero. Um periodo incompleto, uma classificacao errada na origem, um dado que nao cobre toda a janela pedida: isso vai junto, mesmo que ocupe uma das dez linhas. Resumo que esconde a ressalva nao e resumo, e erro com menos palavras.\n\nSE A PERGUNTA FOR AMPLA DEMAIS\nResponda a leitura mais util e diga, em uma linha, qual recorte voce assumiu.',
 'published', 'contextia');
