From 5ca21b598fd4720f49d7b38f465a580f28752961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Fri, 27 Mar 2026 17:32:12 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A9=20feat(shared):=20definir=20fronte?= =?UTF-8?q?iras=20de=20dados=20e=20configuracao=20da=20fase=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formaliza os contratos compartilhados para leitura operacional, estrategia de relatorios e configuracao funcional governada entre admin e product. Tambem separa explicitamente o runtime do bot de atendimento do runtime de geracao de tools, detalha quais configuracoes do bot entram sob governanca administrativa e documenta as regras de publicacao, rollback e leitura sem acoplar o hot path do atendimento. --- .../admin-bot-governed-configuration-scope.md | 108 +++++ .../admin-functional-configuration-scope.md | 197 +++++++++ .../admin-model-runtime-separation.md | 90 +++++ .../admin-operational-data-scope.md | 299 ++++++++++++++ .../admin-report-materialization-strategy.md | 128 ++++++ .../admin-report-reading-strategy.md | 154 +++++++ .../shared-contracts-and-access-hierarchy.md | 187 ++++++++- shared/contracts/README.md | 35 +- shared/contracts/__init__.py | 76 ++++ .../contracts/bot_governed_configuration.py | 151 +++++++ shared/contracts/model_runtime_separation.py | 85 ++++ shared/contracts/product_operational_data.py | 375 ++++++++++++++++++ .../system_functional_configuration.py | 258 ++++++++++++ tests/test_shared_contracts.py | 324 ++++++++++++++- 14 files changed, 2435 insertions(+), 32 deletions(-) create mode 100644 docs/architecture/admin-bot-governed-configuration-scope.md create mode 100644 docs/architecture/admin-functional-configuration-scope.md create mode 100644 docs/architecture/admin-model-runtime-separation.md create mode 100644 docs/architecture/admin-operational-data-scope.md create mode 100644 docs/architecture/admin-report-materialization-strategy.md create mode 100644 docs/architecture/admin-report-reading-strategy.md create mode 100644 shared/contracts/bot_governed_configuration.py create mode 100644 shared/contracts/model_runtime_separation.py create mode 100644 shared/contracts/product_operational_data.py create mode 100644 shared/contracts/system_functional_configuration.py diff --git a/docs/architecture/admin-bot-governed-configuration-scope.md b/docs/architecture/admin-bot-governed-configuration-scope.md new file mode 100644 index 0000000..519bc9f --- /dev/null +++ b/docs/architecture/admin-bot-governed-configuration-scope.md @@ -0,0 +1,108 @@ +# Configuracoes Do Bot Governadas Pelo Admin + +## Objetivo + +Definir exatamente quais configuracoes do bot de atendimento entram sob governanca do `orquestrador-admin`. + +Esta etapa detalha, em nivel de campo, a parte do runtime do bot que pode ser consultada por `colaborador` e alterada por `diretor`. + +## Decisao + +O `admin` governa apenas configuracoes funcionais do bot de atendimento. + +Isso inclui: + +- escolha do modelo homologado usado no atendimento +- politicas de resposta do bot +- politicas de uso de tools +- politicas de fallback e handoff humano +- politicas operacionais por canal + +Essa fronteira fica formalizada em `shared/contracts/bot_governed_configuration.py`. + +## Configuracoes governadas + +### 1. Selecao de modelo do bot + +Campos governados: + +- `provider` +- `model_name` + +Esses campos definem qual modelo homologado responde ao cliente final. + +### 2. Geracao de resposta + +Campos governados: + +- `temperature` +- `max_output_tokens` +- `prompt_profile_ref` + +Esses campos controlam o perfil funcional da resposta, sem expor o painel a segredos ou internals de infraestrutura. + +### 3. Uso de tools + +Campos governados: + +- `tool_policy_ref` +- `max_tool_calls_per_turn` +- `confirmation_policy` + +Esses campos definem como o bot pode usar tools e quando precisa de confirmacao antes de acao critica. + +### 4. Fallback e handoff + +Campos governados: + +- `fallback_mode` +- `handoff_enabled` +- `handoff_intents` + +Esses campos governam quando o fluxo segue fallback controlado e quando encaminha para atendimento humano. + +### 5. Operacao por canal + +Campos governados: + +- `enabled` +- `maintenance_mode` +- `default_route` +- `operation_window_ref` + +Esses campos permitem controlar disponibilidade e comportamento funcional por canal homologado. + +## O que nao entra como configuracao do bot + +As seguintes superficies ficam fora desta governanca: + +- configuracao de modelo para geracao de tools +- credenciais de provedor e segredos +- conteudo bruto de prompt sensivel +- variaveis de ambiente e infraestrutura +- implementacao interna das tools +- alteracao direta em tabelas operacionais do `product` + +## Regras obrigatorias + +### 1. Leitura por `colaborador`, alteracao por `diretor` + +- `colaborador` consulta via `view_system` +- `diretor` consulta e altera via `manage_settings` + +### 2. Sem escrita direta no runtime do produto + +O painel registra estado desejado e governado. +O `product` consome apenas configuracao publicada, versionada e auditavel. + +### 3. Separacao do runtime de geracao + +O runtime usado para gerar tools continua em trilha propria. +Ele nao deve ser tratado como configuracao do bot de atendimento. + +## Consequencias positivas + +- deixa a tela de configuracao do bot mais clara e segura +- evita que a UI misture atendimento com geracao de tools +- preserva a governanca de publicacao entre `admin` e `product` +- prepara a proxima etapa de rotas administrativas para configuracao funcional do sistema diff --git a/docs/architecture/admin-functional-configuration-scope.md b/docs/architecture/admin-functional-configuration-scope.md new file mode 100644 index 0000000..676d8c5 --- /dev/null +++ b/docs/architecture/admin-functional-configuration-scope.md @@ -0,0 +1,197 @@ +# Escopo De Configuracao Funcional Governada No Admin + +## Objetivo + +Definir quais configuracoes funcionais o `orquestrador-admin` pode consultar e alterar sem transformar o painel em uma superficie de mudanca irrestrita do runtime do `orquestrador-product`. + +Esta etapa fixa a **fronteira funcional de configuracao**. +As telas e rotas especificas da fase 4 vao consumir esse contrato depois. + +## Decisao + +O `admin` pode consultar um conjunto governado de configuracoes funcionais do sistema. +Dessas configuracoes, apenas o papel `diretor` pode alterar o estado desejado. +O papel `colaborador` fica com leitura para acompanhamento operacional do sistema. + +A fronteira compartilhada inicial fica em `shared/contracts/system_functional_configuration.py`. +O detalhamento especifico do que o painel governa no bot de atendimento fica em `docs/architecture/admin-bot-governed-configuration-scope.md`.`r`nA separacao entre o runtime de atendimento e o runtime de geracao de tools fica em `docs/architecture/admin-model-runtime-separation.md`. + +## O que entra na fronteira administrativa + +As primeiras configuracoes funcionais aprovadas para o painel sao: + +1. `allowed_model_catalog` +2. `atendimento_runtime_profile` +3. `tool_generation_runtime_profile` +4. `bot_behavior_policy` +5. `channel_operation_policy` +6. `published_runtime_state` + +### 1. `allowed_model_catalog` + +Superficie somente leitura usada para o painel saber quais modelos estao homologados pela plataforma. + +Serve para: + +- montar listas de selecao na tela administrativa +- impedir configuracao de modelo fora do catalogo permitido +- diferenciar modelos liberados para atendimento e para geracao de tools + +Nao serve para: + +- cadastrar credenciais de provedor +- alterar limites de infraestrutura +- homologar modelo novo diretamente pela UI + +### 2. `atendimento_runtime_profile` + +Configuracao funcional governada do modelo do bot que atende o cliente final. + +Inclui: + +- provedor selecionado +- modelo selecionado +- temperatura +- limite de saida +- referencia de prompt publicada +- referencia de politica de tools + +Regra: + +- `colaborador` consulta +- `diretor` altera + +### 3. `tool_generation_runtime_profile` + +Configuracao funcional governada do modelo usado para gerar e validar novas tools. + +Inclui: + +- provedor selecionado +- modelo selecionado +- perfil de raciocinio +- limite de saida +- referencia de politica de validacao + +Regra obrigatoria: + +- esse perfil e separado do perfil de atendimento +- trocar o modelo de geracao nao troca automaticamente o modelo do bot + +### 4. `bot_behavior_policy` + +Politicas funcionais do fluxo do bot. + +Inclui: + +- modo de fallback +- handoff para humano +- intencoes que forcam escalonamento +- limite de chamadas de tool por turno +- politica de confirmacao para acao critica + +Essa configuracao existe para o painel governar o comportamento funcional do atendimento, e nao o codigo interno do orquestrador. + +### 5. `channel_operation_policy` + +Politicas funcionais por canal. + +Inclui: + +- canal habilitado ou desabilitado +- modo de manutencao +- rota funcional padrao +- referencia da janela operacional + +Essa superficie permite governar disponibilidade funcional sem dar acesso a infraestrutura bruta. + +### 6. `published_runtime_state` + +Superficie somente leitura do estado efetivo publicado no `product`. + +Inclui: + +- escopo configurado +- versao ativa +- quem publicou +- quando publicou +- quando o produto aplicou a mudanca + +Serve para: + +- auditoria +- transparencia na dashboard +- comparacao entre estado desejado no admin e estado efetivo no produto + +## O que fica fora da fronteira administrativa + +As seguintes superficies nao entram como configuracao funcional alteravel no painel: + +- segredos e credenciais de provedor +- API keys +- strings de conexao com banco +- variaveis de ambiente de deploy +- configuracao de autoscaling e infraestrutura +- schema de banco operacional +- payloads tecnicos internos de execucao +- alteracao direta em tabelas operacionais do `product` + +## Regras obrigatorias + +### 1. Leitura ampla, escrita governada + +A leitura dessas configuracoes nasce sob `view_system`. + +Consequencia pratica: + +- `colaborador` pode consultar a configuracao funcional vigente +- `diretor` tambem consulta +- apenas `diretor` altera configuracoes governadas com `manage_settings` + +### 2. Sem escrita direta no produto + +O painel administrativo nao escreve diretamente no runtime do `product` durante uma request de UI. + +A fronteira correta eh: + +- o `admin` registra estado funcional desejado +- o estado e versionado, auditado e aprovado +- o `product` consome apenas configuracao publicada + +### 3. Separacao entre atendimento e geracao de tools + +Os dois runtimes precisam continuar independentes. + +Portanto: + +- o modelo do atendimento vive em `atendimento_runtime_profile` +- o modelo de geracao vive em `tool_generation_runtime_profile` +- cada perfil pode ter rollout, auditoria e fallback proprios + +### 4. Estado efetivo precisa ser observavel + +Toda configuracao governada precisa gerar uma superficie de consulta sobre o estado efetivo publicado no `product`. + +Consequencia pratica: + +- a dashboard administrativa consegue mostrar o que esta ativo de verdade +- o sistema evita divergencia silenciosa entre desejo do admin e runtime do produto + +## Consequencias positivas + +- permite escolher modelo do bot pelo painel sem expor segredos de infraestrutura +- prepara a tela de configuracoes do sistema para `diretor` +- mantem `colaborador` com visibilidade do fluxo do bot e do estado vigente +- reforca a separacao entre governanca administrativa e hot path do atendimento +- prepara versionamento e auditoria das configuracoes antes da integracao completa entre `admin` e `product` + +## Proximos passos naturais + +- criar rotas administrativas para configuracao funcional do sistema +- criar tela administrativa de configuracoes do sistema +- criar superficie visual para estado publicado e versoes ativas +- definir publicacao e consumo dessas configuracoes entre `admin` e `product` + + + + diff --git a/docs/architecture/admin-model-runtime-separation.md b/docs/architecture/admin-model-runtime-separation.md new file mode 100644 index 0000000..f3c09dc --- /dev/null +++ b/docs/architecture/admin-model-runtime-separation.md @@ -0,0 +1,90 @@ +# Separacao Entre Modelo Do Atendimento E Modelo De Geracao De Tools + +## Objetivo + +Definir a fronteira entre o runtime de modelo usado no atendimento ao cliente e o runtime de modelo usado para gerar e validar novas tools. + +Esta etapa consolida uma regra importante da arquitetura: os dois perfis de modelo nao podem compartilhar configuracao nem ciclo de publicacao. + +## Decisao + +O sistema passa a tratar esses runtimes como perfis independentes. + +Perfis: + +1. `atendimento_runtime_profile` +2. `tool_generation_runtime_profile` + +A separacao formal fica em `shared/contracts/model_runtime_separation.py`. + +## Regras obrigatorias + +### 1. Configuracoes distintas + +Cada runtime possui sua propria `config_key`. + +Portanto: + +- o atendimento usa `atendimento_runtime_profile` +- a geracao de tools usa `tool_generation_runtime_profile` +- uma mudanca de configuracao nunca reutiliza a mesma chave para os dois contextos + +### 2. Catalogos com alvo separado + +Os modelos homologados precisam carregar o alvo funcional correto. + +Portanto: + +- modelos homologados para atendimento entram sob `runtime_target = atendimento` +- modelos homologados para geracao entram sob `runtime_target = tool_generation` +- um modelo pode existir nos dois catalogos, mas a selecao continua independente + +### 3. Publicacao independente + +Os dois runtimes possuem publicacao independente. + +Consequencia pratica: + +- publicar uma mudanca no atendimento nao publica a geracao de tools +- publicar uma mudanca na geracao de tools nao muda o bot que responde ao cliente +- cada perfil pode ter sua propria auditoria e versao ativa + +### 4. Rollback independente + +Cada runtime precisa poder voltar ao estado anterior sem afetar o outro. + +Consequencia pratica: + +- rollback do atendimento nao mexe no runtime de geracao +- rollback da geracao nao mexe no atendimento em producao + +### 5. Sem propagacao implicita + +Nao e permitido que uma alteracao em um runtime seja espelhada automaticamente no outro. + +Isso impede: + +- trocar o modelo do bot e, por efeito colateral, trocar o modelo de geracao +- usar defaults compartilhados para empurrar mudancas silenciosas nos dois fluxos +- misturar SLO, custo e risco do atendimento com o pipeline de tools + +## Responsabilidade por runtime + +### Atendimento + +- alvo funcional: responder ao cliente final +- servico consumidor: `product` +- impacto direto: experiencia do atendimento e fluxo conversacional + +### Geracao de tools + +- alvo funcional: gerar e validar novas tools +- servico consumidor: `admin` +- impacto direto: pipeline de governanca, geracao e validacao + +## Consequencias positivas + +- protege o atendimento de experimentos de geracao de codigo +- permite escolher modelos diferentes para custo, latencia e qualidade em cada fluxo +- simplifica auditoria e rollback de configuracao +- prepara as futuras telas e rotas de configuracao do sistema sem ambiguidade diff --git a/docs/architecture/admin-operational-data-scope.md b/docs/architecture/admin-operational-data-scope.md new file mode 100644 index 0000000..fdd560d --- /dev/null +++ b/docs/architecture/admin-operational-data-scope.md @@ -0,0 +1,299 @@ +# Escopo De Dados Operacionais Do Product Visiveis No Admin + +## Objetivo + +Definir, de forma explicita, quais dados operacionais do `orquestrador-product` podem ser consultados pelo `orquestrador-admin` na fase inicial de relatorios e configuracao. + +Esta definicao cobre o **que** o admin pode ler. +A estrategia de leitura desses dados sem acoplar o hot path do atendimento fica detalhada em `docs/architecture/admin-report-reading-strategy.md`. +A materializacao concreta desses relatorios fica detalhada em `docs/architecture/admin-report-materialization-strategy.md`. + +## Principios obrigatorios + +1. O `product` continua sendo a fonte operacional primaria. +2. O `admin` nasce com acesso de leitura orientado a relatorios, nunca como escritor direto dessas tabelas. +3. O hot path do atendimento nao deve depender de consulta online ao `admin`. +4. Dados de identidade do cliente final, texto livre e segredos operacionais nao entram automaticamente na fronteira administrativa. +5. Sempre que um indicador puder ser atendido por agregado, o agregado deve ser preferido a leitura detalhada. + +## Datasets permitidos nesta fase + +O contrato compartilhado correspondente fica em `shared/contracts/product_operational_data.py`. + +### 1. Estoque comercial + +Fonte atual: + +- `vehicles` + +Uso administrativo esperado: + +- disponibilidade comercial +- distribuicao por categoria +- faixa de preco +- entrada de novos itens no estoque + +Campos permitidos: + +- `id` +- `modelo` +- `categoria` +- `preco` +- `created_at` + +### 2. Pedidos de venda + +Fonte atual: + +- `orders` + +Uso administrativo esperado: + +- volume de pedidos +- pedidos ativos e cancelados +- ticket medio +- cancelamentos por periodo + +Campos permitidos: + +- `numero_pedido` +- `vehicle_id` +- `modelo_veiculo` +- `valor_veiculo` +- `status` +- `motivo_cancelamento` +- `data_cancelamento` +- `created_at` +- `updated_at` + +Campos bloqueados: + +- `user_id` +- `cpf` + +### 3. Agenda de revisoes + +Fonte atual: + +- `review_schedules` + +Uso administrativo esperado: + +- ocupacao de slots +- revisoes agendadas por periodo +- taxa de cancelamento +- fila operacional da oficina + +Campos permitidos: + +- `protocolo` +- `placa` +- `data_hora` +- `status` +- `created_at` + +Campos bloqueados: + +- `user_id` + +### 4. Frota de locacao + +Fonte atual: + +- `rental_vehicles` + +Uso administrativo esperado: + +- disponibilidade da frota +- status operacional por categoria +- tarifa diaria vigente + +Campos permitidos: + +- `id` +- `placa` +- `modelo` +- `categoria` +- `ano` +- `valor_diaria` +- `status` +- `created_at` + +### 5. Contratos de locacao + +Fonte atual: + +- `rental_contracts` + +Uso administrativo esperado: + +- contratos ativos e encerrados +- devolucoes em atraso +- receita prevista versus receita final +- ocupacao da frota no tempo + +Campos permitidos: + +- `contrato_numero` +- `rental_vehicle_id` +- `placa` +- `modelo_veiculo` +- `categoria` +- `data_inicio` +- `data_fim_prevista` +- `data_devolucao` +- `valor_diaria` +- `valor_previsto` +- `valor_final` +- `status` +- `created_at` +- `updated_at` + +Campos bloqueados: + +- `user_id` +- `cpf` +- `observacoes` + +### 6. Pagamentos de locacao + +Fonte atual: + +- `rental_payments` + +Uso administrativo esperado: + +- arrecadacao por periodo +- pagamentos conciliados por contrato +- inadimplencia operacional + +Campos permitidos: + +- `protocolo` +- `contrato_numero` +- `placa` +- `valor` +- `data_pagamento` +- `created_at` + +Campos bloqueados: + +- `user_id` +- `rental_contract_id` +- `favorecido` +- `identificador_comprovante` +- `observacoes` + +### 7. Telemetria conversacional + +Fonte atual: + +- `conversation_turns` + +Uso administrativo esperado: + +- volume de atendimento +- latencia por turno +- distribuicao por dominio +- uso de tools +- falhas operacionais por status + +Campos permitidos: + +- `request_id` +- `conversation_id` +- `channel` +- `turn_status` +- `intent` +- `domain` +- `action` +- `tool_name` +- `elapsed_ms` +- `started_at` +- `completed_at` + +Campos bloqueados: + +- `user_id` +- `external_id` +- `username` +- `user_message` +- `assistant_response` +- `tool_arguments` +- `error_detail` + +### 8. Entregas de integracao + +Fonte atual: + +- `integration_deliveries` + +Uso administrativo esperado: + +- taxa de sucesso por provedor +- volume de eventos entregues +- entregas pendentes ou com falha +- tentativas de reenvio + +Campos permitidos: + +- `route_id` +- `event_type` +- `provider` +- `status` +- `attempts` +- `dispatched_at` +- `created_at` +- `updated_at` + +Campos bloqueados: + +- `payload_json` +- `recipient_email` +- `recipient_name` +- `rendered_subject` +- `rendered_body` +- `provider_message_id` +- `last_error` + +## Fontes fora do escopo administrativo nesta fase + +O admin **nao** deve consultar diretamente, nesta fase: + +- `customers` +- `users` +- stores de estado conversacional de hot path +- payloads brutos de tools e mensagens do usuario +- comprovantes e identificadores sensiveis de pagamento +- configuracoes internas de provedor e credenciais + +## Regra de autorizacao + +A leitura desses dados nasce amarrada a `view_reports`. + +Consequencia pratica: + +- `colaborador` pode consultar os dados operacionais liberados para relatorio +- `diretor` herda essa leitura e acumula as etapas de aprovacao e configuracao +- permissao adicional sera exigida apenas quando a consulta implicar governanca, aprovacao ou configuracao + +## Decisao tomada nesta etapa + +O `admin` pode consultar apenas datasets operacionais explicitamente declarados em contrato compartilhado e sempre em modo somente leitura. + +A fronteira inicial favorece relatorios de: + +- vendas +- arrecadacao +- operacao +- telemetria de atendimento +- entregas de integracao + +## Decisao de materializacao relacionada + +Para esses datasets, a fase inicial escolhe: + +- `etl_incremental` como estrategia de sincronizacao +- `snapshot_table` no lado administrativo como persistencia de leitura +- `dedicated_view` sobre os snapshots como superficie de consulta para APIs e UI +- nenhuma replica operacional do banco do produto no dashboard administrativo diff --git a/docs/architecture/admin-report-materialization-strategy.md b/docs/architecture/admin-report-materialization-strategy.md new file mode 100644 index 0000000..9f0868f --- /dev/null +++ b/docs/architecture/admin-report-materialization-strategy.md @@ -0,0 +1,128 @@ +# Estrategia De Materializacao Dos Relatorios Administrativos + +## Objetivo + +Escolher como os relatorios administrativos vao materializar o read model definido para a fase 4. + +A decisao precisava fechar quatro alternativas candidatas: + +- replica +- ETL +- snapshots +- views dedicadas + +## Decisao + +A fase inicial de relatorios do `orquestrador-admin` vai usar a seguinte composicao: + +1. `etl_incremental` como mecanismo de sincronizacao +2. `snapshot_table` no lado administrativo como persistencia de leitura +3. `dedicated_view` sobre os snapshots como superficie de consulta para APIs e UI +4. nenhuma replica operacional do banco do `product` para abrir dashboards administrativos + +Em resumo: + +- **nao** usar replica como mecanismo primario da fase inicial +- **sim** usar ETL incremental +- **sim** persistir snapshots sanitizados +- **sim** expor views dedicadas sobre esses snapshots + +## Por que nao comecar por replica + +Replica isolaria menos do que parece. +Ela ainda manteria o admin muito proximo do schema operacional live, incentivando query ad hoc, joins pesados e acoplamento ao desenho interno do `product`. + +Tambem traria custo operacional cedo demais: + +- infraestrutura adicional +- observabilidade de replicacao +- risco de leitura errada por atraso ou schema drift +- falsa sensacao de que qualquer tabela do produto pode virar dashboard + +Para a fase inicial, replica aumenta a superficie tecnica sem resolver a necessidade principal, que eh governar exatamente **o que** sai do produto e **como** isso chega ao admin. + +## Por que ETL incremental + +ETL incremental encaixa melhor no que ja decidimos para o sistema: + +- preserva o hot path do atendimento +- permite sanitizacao e minimizacao antes do dado chegar ao admin +- suporta watermark, cursor e reprocessamento controlado +- facilita auditoria do ciclo de consolidacao +- prepara evolucao futura para jobs, workers ou pipelines por evento + +O ETL aqui nao precisa nascer grande. +Ele pode comecar como job incremental simples e evoluir sem quebrar o contrato do painel. + +## Por que snapshots + +Snapshots sao a melhor base inicial de persistencia para relatorios administrativos porque: + +- congelam um recorte coerente do dataset consolidado +- permitem metadados como `generated_at`, `source_watermark` e `dataset_version` +- reduzem risco de consultas inconsistentes durante sincronizacao +- simplificam retry, backfill e comparacao entre execucoes + +Na pratica, os snapshots pertencem ao contexto administrativo, nao ao banco operacional do produto. + +## Por que views dedicadas + +Views dedicadas ficam por cima dos snapshots para desacoplar a UI e as APIs do formato bruto de consolidacao. + +Elas permitem: + +- esconder colunas tecnicas de ETL +- estabilizar o contrato consumido pelos relatorios +- organizar uma view por caso de uso de negocio +- evoluir agregacoes e joins internos sem quebrar a tela + +Regra importante: + +- essas views sao dedicadas ao contexto administrativo +- elas nao apontam para tabelas live do produto +- elas leem apenas snapshots ja sanitizados + +## Fluxo alvo + +```text +product operational tables + | + v +etl_incremental boundary + | + v +admin snapshot tables + | + v +admin dedicated views + | + v +admin report routes and dashboard +``` + +## Regras obrigatorias + +1. O painel nunca consulta replica ou tabela live do produto durante request web. +2. O ETL incremental so exporta datasets e campos aprovados em contrato compartilhado. +3. Cada snapshot precisa carregar watermark e timestamp de geracao. +4. Cada view dedicada existe para um caso de uso de relatorio, nunca como espelho generico do schema operacional. +5. Escrita administrativa em tabela operacional do produto continua proibida. + +## Consequencias praticas para a fase 4 + +Com essa decisao, os proximos itens da fase ficam orientados assim: + +- rotas administrativas de relatorio devem ler views dedicadas do admin +- relatorios de vendas, arrecadacao e operacao devem nascer sobre snapshots sanitizados +- a UI deve exibir frescor e estado da ultima consolidacao +- qualquer refresh manual conversa com a camada de sincronizacao, nao com o banco operacional live + +## Evolucao futura permitida + +Se no futuro houver escala suficiente para replica, ela pode entrar como **fonte de extração** do ETL, e nao como backend direto do dashboard. + +Ou seja: + +- replica pode aparecer depois como detalhe de implementacao +- o contrato do painel continua o mesmo +- a fronteira principal segue sendo ETL -> snapshots -> views dedicadas diff --git a/docs/architecture/admin-report-reading-strategy.md b/docs/architecture/admin-report-reading-strategy.md new file mode 100644 index 0000000..0a360ef --- /dev/null +++ b/docs/architecture/admin-report-reading-strategy.md @@ -0,0 +1,154 @@ +# Estrategia De Leitura De Relatorios Sem Acoplar O Hot Path + +## Objetivo + +Definir como o `orquestrador-admin` deve ler dados operacionais para relatorios sem transformar o `orquestrador-product` em backend sincrono de dashboard. + +Esta etapa fixa a **topologia de leitura**. +A materializacao concreta dessa topologia foi definida em `docs/architecture/admin-report-materialization-strategy.md`. + +## Decisao + +Os relatorios administrativos devem ser servidos a partir de um **read model administrativo assincrono**. + +Em outras palavras: + +1. O `product` continua escrevendo o estado operacional primario. +2. Uma camada de sincronizacao fora do hot path materializa dados de leitura para relatorio. +3. O `admin` consulta apenas esse read model, nunca as tabelas operacionais live do `product` em uma request web do painel. + +## Topologia alvo + +```text +product operational writes + | + v +sync/export boundary outside hot path + | + v +admin reporting read model + | + v +admin report APIs and dashboard +``` + +## Regras obrigatorias + +### 1. Sem query direta do painel no banco operacional do produto + +Nao e permitido que uma rota web do painel administrativo execute consultas pesadas ou agregacoes diretamente nas tabelas operacionais do `product`. + +Isso inclui: + +- scans amplos em `orders`, `rental_contracts`, `rental_payments`, `conversation_turns` +- joins ad hoc para dashboard em tempo de request +- leituras que disputem lock, cache ou I/O com o atendimento + +### 2. Leitura eventual, nao transacional + +O painel administrativo deve operar com **consistencia eventual**. + +Consequencia pratica: + +- relatorios mostram o dado consolidado mais recente disponivel +- a UI deve exibir metadados de frescor, como `updated_at`, `generated_at` ou `source_watermark` +- o sistema nao promete refletir cada evento operacional no mesmo instante em que ele acontece + +### 3. Materializacao fora do hot path + +Toda consolidacao, enriquecimento, agregacao ou recorte temporal deve acontecer em processo assincrono. + +Exemplos validos de processo assincrono: + +- job agendado +- worker orientado a eventos +- pipeline incremental por cursor +- rotina batch com watermark + +### 4. Read model proprio do admin + +O `admin` deve ter sua propria superficie de leitura para relatorios. + +Essa superficie pode morar: + +- no banco administrativo +- em um schema analitico separado +- em tabelas materializadas especificas para relatorio + +O importante nesta etapa nao e o lugar fisico, e sim a regra: + +- a query do painel le um read model pronto +- a transformacao do dado acontece antes da request do usuario interno + +### 5. Escrita administrativa continua proibida + +A estrategia de leitura nao muda a fronteira de escrita. + +Portanto: + +- o `admin` nao escreve diretamente nas tabelas operacionais do `product` +- qualquer acao de governanca que altere operacao deve seguir fluxo proprio, versionado e auditavel + +## Responsabilidades por servico + +### `orquestrador-product` + +Responsavel por: + +- persistir o estado operacional primario +- manter ids tecnicos, timestamps e chaves publicas necessarias para reconciliacao +- expor uma fronteira segura para exportacao ou sincronizacao +- continuar respondendo ao atendimento sem depender do `admin` + +### `orquestrador-admin` + +Responsavel por: + +- armazenar ou consultar o read model de relatorio +- servir rotas administrativas de relatorio +- informar frescor e origem do dado para a UI +- aplicar filtros e agregacoes sobre a superficie de leitura consolidada + +## O que a UI deve assumir + +As telas administrativas de relatorio devem nascer com a expectativa de dado consolidado e nao de espelho instantaneo do operacional. + +Consequencia pratica: + +- cada relatorio deve carregar carimbo de atualizacao +- um refresh manual dispara no maximo uma rotina de sincronizacao, nunca uma query pesada live no produto +- empty states e avisos de defasagem fazem parte do contrato visual + +## Padrao de frescor inicial + +Enquanto a implementacao completa nao chega, os datasets operacionais ficam classificados em metas de frescor no contrato compartilhado: + +- `near_real_time` para vendas, revisoes e locacao +- `intra_hour` para estoque, telemetria e entregas de integracao + +Essas metas servem para orientar UX, monitoracao e futuras implementacoes da sincronizacao. +Elas nao significam leitura live do banco operacional. + +## O que fica explicitamente proibido + +- dashboard administrativo consultando `product` por HTTP sincrono a cada abertura de pagina +- admin executando agregacao pesada em banco primario de atendimento durante request web +- relatorios dependendo de lock em tabela operacional para responder ao usuario interno +- uso de payload bruto e PII fora do contrato compartilhado de dados operacionais + +## Consequencias positivas + +- protege latencia do atendimento +- reduz risco de regressao operacional por carga analitica +- permite evoluir relatorios com independencia do runtime conversacional +- facilita observabilidade de frescor e falha da sincronizacao +- prepara o terreno para ETL incremental, snapshots sanitizados e views dedicadas sem quebrar o painel + +## Decisao complementar ja tomada + +A topologia acima agora foi materializada assim: + +- `etl_incremental` como fronteira de sincronizacao +- `snapshot_table` no admin para persistencia de leitura +- `dedicated_view` sobre snapshots para servir APIs e dashboard +- sem replica operacional do banco do produto nesta fase diff --git a/docs/architecture/shared-contracts-and-access-hierarchy.md b/docs/architecture/shared-contracts-and-access-hierarchy.md index fd9207e..5557abc 100644 --- a/docs/architecture/shared-contracts-and-access-hierarchy.md +++ b/docs/architecture/shared-contracts-and-access-hierarchy.md @@ -9,6 +9,8 @@ Criar uma base comum para: - autenticacao e autorizacao administrativa - publicacao de tools do `admin` para o `product` +- leitura operacional segura do `admin` sobre o `product` +- configuracao funcional governada entre `admin` e `product` - evolucao independente dos dois servicos sem acoplamento indevido ## Hierarquia inicial de acesso @@ -17,37 +19,28 @@ Os papeis administrativos ficam centralizados em `shared/contracts/access_contro Hierarquia: -1. `viewer` -2. `staff` -3. `admin` +1. `colaborador` +2. `diretor` -### `viewer` +### `colaborador` -Responsavel por leitura operacional. +Responsavel por operacao interna de acompanhamento e cadastro inicial de tools. Permissoes iniciais: - `view_system` - `view_reports` - `view_audit_logs` - -### `staff` - -Responsavel por operacao interna e governanca de drafts. - -Permissoes iniciais: - -- todas as de `viewer` - `manage_tool_drafts` -- `review_tool_generations` -### `admin` +### `diretor` -Responsavel por configuracao, publicacao e gestao de acesso. +Responsavel por configuracao, aprovacao, publicacao e gestao de acesso interno. Permissoes iniciais: -- todas as de `staff` +- todas as de `colaborador` +- `review_tool_generations` - `publish_tools` - `manage_settings` - `manage_staff_accounts` @@ -72,6 +65,153 @@ Ele cobre: - `PublishedToolContract` - `ToolPublicationEnvelope` +## Contrato de leitura operacional do produto + +O contrato inicial fica em `shared/contracts/product_operational_data.py`. + +Ele cobre: + +- datasets operacionais que o `admin` pode consultar do `product` +- dominios atuais: `inventory`, `sales`, `review`, `rental`, `conversation`, `integration` +- granularidade inicial de leitura por registro e por agregado +- campos liberados para relatorio e operacao +- campos bloqueados quando carregam identidade do cliente, texto livre ou segredos operacionais +- estrategia de leitura por `admin_read_model`, consistencia eventual e leitura sem query direta do painel no banco operacional do produto +- estrategia de materializacao inicial por `etl_incremental`, persistida em `snapshot_table` e exposta ao painel por `dedicated_view` + +### Regra de permissao + +A leitura desses datasets nasce sob `view_reports`. + +Isso significa: + +- `colaborador` pode consultar relatorios e snapshots operacionais +- `diretor` herda essa leitura +- a permissao nao autoriza escrita nem governanca sobre tabelas operacionais + +### Regra de minimizacao + +Mesmo quando um dataset do produto entra na fronteira compartilhada, o `admin` nao recebe automaticamente todos os seus campos. + +A fronteira correta eh: + +- expor indicadores operacionais, ids tecnicos e chaves publicas necessarias ao relatorio +- bloquear `cpf`, `email`, `external_id`, payloads brutos, mensagens livres e identificadores sensiveis +- preferir agregados quando o mesmo objetivo nao exigir leitura linha a linha + +### Regra de isolamento do hot path + +As consultas administrativas de relatorio nao devem ser executadas diretamente sobre as tabelas operacionais do produto a partir de uma request web do painel. + +A fronteira correta eh: + +- o `product` escreve estado operacional +- uma camada assincrona de `etl_incremental` materializa snapshots sanitizados no admin +- o painel e as APIs administrativas consultam `dedicated_view` construidas sobre esses snapshots +- nenhuma view administrativa pode apontar diretamente para tabelas live do `product` + +## Contrato de configuracao funcional governada + +O contrato inicial fica em `shared/contracts/system_functional_configuration.py`. + +Ele cobre: + +- quais configuracoes funcionais o `admin` pode consultar do sistema +- quais configuracoes podem ser alteradas apenas por `diretor` +- a separacao entre runtime de atendimento e runtime de geracao de tools +- a diferenca entre estado governado no `admin` e estado efetivo publicado no `product` +- a proibicao de alterar segredos, infra e tabelas operacionais a partir do painel + +### Superficies iniciais declaradas + +As primeiras configuracoes funcionais compartilhadas sao: + +- `allowed_model_catalog` +- `atendimento_runtime_profile` +- `tool_generation_runtime_profile` +- `bot_behavior_policy` +- `channel_operation_policy` +- `published_runtime_state` + +### Regra de permissao + +A leitura dessas configuracoes nasce sob `view_system`. + +Isso significa: + +- `colaborador` pode consultar configuracoes efetivas, catalogos homologados e estado publicado +- `diretor` herda essa leitura +- apenas `diretor` pode alterar configuracoes governadas, usando `manage_settings` + +### Regra de governanca + +A fronteira correta eh: + +- `diretor` altera apenas configuracoes funcionais governadas +- toda alteracao nasce como estado administrativo versionado +- o `product` consome apenas configuracao publicada e aprovada +- o painel nao altera segredos, variaveis de ambiente, credenciais, schema operacional ou comportamento interno sem governanca + +### Regra de separacao entre modelos + +A escolha de modelo do bot de atendimento e a escolha de modelo para geracao de tools nao devem compartilhar a mesma chave de configuracao. + +A fronteira correta eh: + +- `atendimento_runtime_profile` governa o modelo que responde ao cliente final +- `tool_generation_runtime_profile` governa o modelo usado para gerar e validar tools +- cada perfil pode evoluir, ser auditado e ser publicado em ritmos diferentes + +## Contrato de governanca do bot + +O contrato inicial fica em `shared/contracts/bot_governed_configuration.py`. + +Ele cobre, em nivel de campo, quais configuracoes do bot de atendimento ficam sob governanca administrativa. + +As primeiras superficies governadas sao: + +- selecao de modelo do bot: `provider`, `model_name` +- geracao de resposta: `temperature`, `max_output_tokens`, `prompt_profile_ref` +- uso de tools: `tool_policy_ref`, `max_tool_calls_per_turn`, `confirmation_policy` +- fallback e handoff: `fallback_mode`, `handoff_enabled`, `handoff_intents` +- operacao por canal: `enabled`, `maintenance_mode`, `default_route`, `operation_window_ref` + +### Regra de permissao + +A leitura dessas configuracoes continua sob `view_system`. + +A alteracao governada continua restrita a `diretor`, com `manage_settings`. + +### Regra de fronteira + +Esse contrato deixa explicito que: + +- runtime de geracao de tools nao entra como configuracao do bot de atendimento +- o painel governa referencias e politicas funcionais, nao segredos nem infraestrutura +- nenhuma configuracao do bot e aplicada por escrita direta no banco ou runtime live do `product` +- toda mudanca passa por publicacao versionada e auditavel + +## Contrato de separacao entre runtimes de modelo + +O contrato inicial fica em `shared/contracts/model_runtime_separation.py`. + +Ele cobre: + +- os alvos `atendimento` e `tool_generation` +- a `config_key` de cada runtime +- o servico consumidor de cada perfil de modelo +- a exigencia de publicacao e rollback independentes +- a proibicao de propagacao implicita entre os dois runtimes + +### Regra de fronteira + +Esse contrato deixa explicito que: + +- o runtime de atendimento e consumido pelo `product` +- o runtime de geracao e consumido pelo `admin` +- trocar um perfil nao troca automaticamente o outro +- os dois podem compartilhar um provedor homologado, mas nao compartilham estado de configuracao + ## Como isso sera usado depois ### No `orquestrador-admin` @@ -80,16 +220,23 @@ Ele cobre: - associar `StaffAccount.role` - controlar acesso a UI, as rotas e a aprovacao de tools - emitir `ToolPublicationEnvelope` quando uma tool for publicada +- construir relatorios usando apenas datasets declarados no contrato compartilhado +- consultar read models administrativos em vez de tabelas live do produto +- governar configuracoes funcionais sem escrever diretamente no banco operacional do `product` ### No `orquestrador-product` - consumir apenas tools publicadas - validar status e versao do contrato recebido +- expor fronteiras seguras para sincronizacao incremental dos datasets permitidos ao `admin` +- consumir apenas configuracoes funcionais publicadas e aprovadas - evitar dependencia do runtime do admin no hot path ## Proximos passos naturais -- criar a entidade `StaffAccount` -- plugar a role do usuario interno ao contrato compartilhado -- modelar a persistencia de drafts/publicacoes de tool +- criar rotas administrativas para relatorios +- criar rotas administrativas para configuracao funcional do sistema +- estruturar snapshots e views de vendas, arrecadacao e operacao +- manter a escrita administrativa fora das tabelas operacionais do produto + diff --git a/shared/contracts/README.md b/shared/contracts/README.md index 84df72d..cdfda32 100644 --- a/shared/contracts/README.md +++ b/shared/contracts/README.md @@ -13,15 +13,46 @@ Nesta fase, os primeiros contratos compartilhados sao: - `access_control.py` - define a hierarquia inicial de acesso interno - - papeis: `viewer`, `staff`, `admin` - - permissoes iniciais para relatorios, configuracao, revisao e publicacao + - papeis: `colaborador`, `diretor` + - `colaborador` consulta o fluxo operacional e cadastra novas tools em draft + - `diretor` revisa, aprova, publica tools e cadastra novos colaboradores - `tool_publication.py` - define o contrato minimo de publicacao de tools do `admin` para o `product` - inclui envelope de publicacao, status de ciclo de vida e schema de parametros +- `product_operational_data.py` + - define quais datasets operacionais do `product` podem ser consultados pelo `admin` + - explicita dominios, granularidade de leitura, campos permitidos e campos bloqueados + - reforca que o acesso administrativo nasce como leitura orientada a relatorios + - declara que a leitura deve acontecer por `admin_read_model`, com consistencia eventual e sem query direta do painel no banco operacional do produto + - formaliza a materializacao inicial por `etl_incremental` em `snapshot_table`, servida por `dedicated_view` + - deixa explicito que a fase inicial nao usa replica operacional do produto para abrir dashboards administrativos + +- `system_functional_configuration.py` + - define quais configuracoes funcionais o `admin` pode consultar e quais podem ser alteradas + - separa o runtime do bot de atendimento do runtime de geracao de tools + - estabelece catalogo homologado de modelos, politicas do bot, politicas de canal e estado efetivo publicado + - reforca que apenas `diretor` altera configuracoes governadas com `manage_settings` + - deixa explicito que o painel nao altera segredos, credenciais ou tabelas operacionais do produto + +- `bot_governed_configuration.py` + - detalha quais campos do bot ficam sob governanca administrativa + - cobre selecao de modelo, geracao de resposta, uso de tools, fallback, handoff e operacao por canal + - deixa explicito que a governanca do bot usa publicacao versionada e nao escrita direta no runtime do produto + - reforca que runtime de geracao de tools nao e configuracao do bot de atendimento + +- `model_runtime_separation.py` + - formaliza que atendimento e geracao de tools usam perfis de modelo distintos + - separa config key, catalogo alvo, publicacao e rollback entre os dois runtimes + - deixa explicito que uma mudanca em um runtime nao propaga automaticamente para o outro + ## Regras - `shared/contracts` deve guardar apenas contratos estaveis entre servicos - nada aqui deve importar modulos internos de `app/` ou `admin_app/` - as mudancas devem ser additive-first para permitir deploy independente entre `product` e `admin` +- contratos de leitura operacional nao autorizam escrita administrativa nas tabelas do produto +- relatorios administrativos devem consumir read models assincronos, nunca scans pesados no hot path do atendimento +- views dedicadas de relatorio so podem ser construidas sobre snapshots sanitizados do admin, nunca sobre tabelas live do produto +- configuracoes funcionais governadas nao autorizam escrita direta no runtime do `product` durante request web do painel diff --git a/shared/contracts/__init__.py b/shared/contracts/__init__.py index d376904..013c1a5 100644 --- a/shared/contracts/__init__.py +++ b/shared/contracts/__init__.py @@ -3,10 +3,52 @@ from shared.contracts.access_control import ( AdminPermission, StaffRole, + normalize_staff_role, permissions_for_role, role_has_permission, role_includes, ) +from shared.contracts.bot_governed_configuration import ( + BOT_GOVERNED_SETTINGS, + BotGovernanceArea, + BotGovernanceMutability, + BotGovernedSettingContract, + get_bot_governed_setting, +) +from shared.contracts.model_runtime_separation import ( + MODEL_RUNTIME_PROFILES, + MODEL_RUNTIME_SEPARATION_RULES, + ModelRuntimePurpose, + ModelRuntimeSeparationContract, + ModelRuntimeSeparationRule, + ModelRuntimeTarget, + get_model_runtime_contract, +) +from shared.contracts.product_operational_data import ( + PRODUCT_OPERATIONAL_DATASETS, + OperationalConsistencyModel, + OperationalDataDomain, + OperationalDataSensitivity, + OperationalDatasetContract, + OperationalFieldContract, + OperationalFreshnessTarget, + OperationalQuerySurface, + OperationalReadGranularity, + OperationalReadModel, + OperationalStorageShape, + OperationalSyncStrategy, + get_operational_dataset, +) +from shared.contracts.system_functional_configuration import ( + SYSTEM_FUNCTIONAL_CONFIGURATIONS, + FunctionalConfigurationContract, + FunctionalConfigurationDomain, + FunctionalConfigurationFieldContract, + FunctionalConfigurationMutability, + FunctionalConfigurationPropagation, + FunctionalConfigurationSource, + get_functional_configuration, +) from shared.contracts.tool_publication import ( PublishedToolContract, ServiceName, @@ -18,13 +60,47 @@ from shared.contracts.tool_publication import ( __all__ = [ "AdminPermission", + "BOT_GOVERNED_SETTINGS", + "MODEL_RUNTIME_PROFILES", + "MODEL_RUNTIME_SEPARATION_RULES", + "PRODUCT_OPERATIONAL_DATASETS", "PublishedToolContract", + "SYSTEM_FUNCTIONAL_CONFIGURATIONS", "ServiceName", "StaffRole", "ToolLifecycleStatus", "ToolParameterContract", "ToolParameterType", "ToolPublicationEnvelope", + "BotGovernanceArea", + "BotGovernanceMutability", + "BotGovernedSettingContract", + "ModelRuntimePurpose", + "ModelRuntimeSeparationContract", + "ModelRuntimeSeparationRule", + "ModelRuntimeTarget", + "OperationalConsistencyModel", + "OperationalDataDomain", + "OperationalDataSensitivity", + "OperationalDatasetContract", + "OperationalFieldContract", + "OperationalFreshnessTarget", + "OperationalQuerySurface", + "OperationalReadGranularity", + "OperationalReadModel", + "OperationalStorageShape", + "OperationalSyncStrategy", + "FunctionalConfigurationContract", + "FunctionalConfigurationDomain", + "FunctionalConfigurationFieldContract", + "FunctionalConfigurationMutability", + "FunctionalConfigurationPropagation", + "FunctionalConfigurationSource", + "get_bot_governed_setting", + "get_functional_configuration", + "get_model_runtime_contract", + "get_operational_dataset", + "normalize_staff_role", "permissions_for_role", "role_has_permission", "role_includes", diff --git a/shared/contracts/bot_governed_configuration.py b/shared/contracts/bot_governed_configuration.py new file mode 100644 index 0000000..1d43fac --- /dev/null +++ b/shared/contracts/bot_governed_configuration.py @@ -0,0 +1,151 @@ +"""Define quais configuracoes do bot ficam sob governanca administrativa.""" + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel + +from shared.contracts.access_control import AdminPermission + + +class BotGovernanceArea(str, Enum): + MODEL_SELECTION = "model_selection" + RESPONSE_GENERATION = "response_generation" + TOOL_USAGE = "tool_usage" + FALLBACK_AND_HANDOFF = "fallback_and_handoff" + CHANNEL_OPERATION = "channel_operation" + + +class BotGovernanceMutability(str, Enum): + DIRECTOR_GOVERNED = "director_governed" + + +class BotGovernedSettingContract(BaseModel): + setting_key: str + parent_config_key: str + field_name: str + area: BotGovernanceArea + description: str + read_permission: AdminPermission = AdminPermission.VIEW_SYSTEM + write_permission: AdminPermission = AdminPermission.MANAGE_SETTINGS + mutability: BotGovernanceMutability = BotGovernanceMutability.DIRECTOR_GOVERNED + versioned_publication_required: bool = True + direct_product_write_allowed: bool = False + + +BOT_GOVERNED_SETTINGS: tuple[BotGovernedSettingContract, ...] = ( + BotGovernedSettingContract( + setting_key="bot_model_provider", + parent_config_key="atendimento_runtime_profile", + field_name="provider", + area=BotGovernanceArea.MODEL_SELECTION, + description="Provedor do modelo usado pelo bot de atendimento.", + ), + BotGovernedSettingContract( + setting_key="bot_model_name", + parent_config_key="atendimento_runtime_profile", + field_name="model_name", + area=BotGovernanceArea.MODEL_SELECTION, + description="Modelo selecionado para responder ao cliente final.", + ), + BotGovernedSettingContract( + setting_key="bot_temperature", + parent_config_key="atendimento_runtime_profile", + field_name="temperature", + area=BotGovernanceArea.RESPONSE_GENERATION, + description="Temperatura aplicada nas respostas do bot.", + ), + BotGovernedSettingContract( + setting_key="bot_max_output_tokens", + parent_config_key="atendimento_runtime_profile", + field_name="max_output_tokens", + area=BotGovernanceArea.RESPONSE_GENERATION, + description="Limite de saida usado no runtime de atendimento.", + ), + BotGovernedSettingContract( + setting_key="bot_prompt_profile_ref", + parent_config_key="atendimento_runtime_profile", + field_name="prompt_profile_ref", + area=BotGovernanceArea.RESPONSE_GENERATION, + description="Referencia do perfil de prompt publicado para o bot.", + ), + BotGovernedSettingContract( + setting_key="bot_tool_policy_ref", + parent_config_key="atendimento_runtime_profile", + field_name="tool_policy_ref", + area=BotGovernanceArea.TOOL_USAGE, + description="Referencia da politica de uso de tools pelo bot.", + ), + BotGovernedSettingContract( + setting_key="bot_fallback_mode", + parent_config_key="bot_behavior_policy", + field_name="fallback_mode", + area=BotGovernanceArea.FALLBACK_AND_HANDOFF, + description="Modo funcional de fallback quando o bot nao conclui a tarefa.", + ), + BotGovernedSettingContract( + setting_key="bot_handoff_enabled", + parent_config_key="bot_behavior_policy", + field_name="handoff_enabled", + area=BotGovernanceArea.FALLBACK_AND_HANDOFF, + description="Habilita o encaminhamento para atendimento humano.", + ), + BotGovernedSettingContract( + setting_key="bot_handoff_intents", + parent_config_key="bot_behavior_policy", + field_name="handoff_intents", + area=BotGovernanceArea.FALLBACK_AND_HANDOFF, + description="Lista de intencoes que exigem handoff humano.", + ), + BotGovernedSettingContract( + setting_key="bot_max_tool_calls_per_turn", + parent_config_key="bot_behavior_policy", + field_name="max_tool_calls_per_turn", + area=BotGovernanceArea.TOOL_USAGE, + description="Limite de chamadas de tools por turno conversacional.", + ), + BotGovernedSettingContract( + setting_key="bot_confirmation_policy", + parent_config_key="bot_behavior_policy", + field_name="confirmation_policy", + area=BotGovernanceArea.TOOL_USAGE, + description="Politica de confirmacao antes de acao critica no fluxo.", + ), + BotGovernedSettingContract( + setting_key="channel_enabled", + parent_config_key="channel_operation_policy", + field_name="enabled", + area=BotGovernanceArea.CHANNEL_OPERATION, + description="Habilita ou desabilita o bot em um canal homologado.", + ), + BotGovernedSettingContract( + setting_key="channel_maintenance_mode", + parent_config_key="channel_operation_policy", + field_name="maintenance_mode", + area=BotGovernanceArea.CHANNEL_OPERATION, + description="Liga manutencao controlada em um canal do bot.", + ), + BotGovernedSettingContract( + setting_key="channel_default_route", + parent_config_key="channel_operation_policy", + field_name="default_route", + area=BotGovernanceArea.CHANNEL_OPERATION, + description="Define a rota funcional padrao por canal.", + ), + BotGovernedSettingContract( + setting_key="channel_operation_window_ref", + parent_config_key="channel_operation_policy", + field_name="operation_window_ref", + area=BotGovernanceArea.CHANNEL_OPERATION, + description="Referencia a janela operacional aplicada por canal.", + ), +) + + +def get_bot_governed_setting(setting_key: str) -> BotGovernedSettingContract | None: + normalized = str(setting_key or "").strip().lower() + for setting in BOT_GOVERNED_SETTINGS: + if setting.setting_key == normalized: + return setting + return None diff --git a/shared/contracts/model_runtime_separation.py b/shared/contracts/model_runtime_separation.py new file mode 100644 index 0000000..dfaeacb --- /dev/null +++ b/shared/contracts/model_runtime_separation.py @@ -0,0 +1,85 @@ +"""Define a separacao entre runtime de atendimento e runtime de geracao de tools.""" + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel + +from shared.contracts.access_control import AdminPermission +from shared.contracts.tool_publication import ServiceName + + +class ModelRuntimeTarget(str, Enum): + ATENDIMENTO = "atendimento" + TOOL_GENERATION = "tool_generation" + + +class ModelRuntimePurpose(str, Enum): + CUSTOMER_RESPONSE = "customer_response" + TOOL_GENERATION_AND_VALIDATION = "tool_generation_and_validation" + + +class ModelRuntimeSeparationRule(str, Enum): + SEPARATE_CONFIG_KEYS = "separate_config_keys" + SEPARATE_CATALOG_TARGETS = "separate_catalog_targets" + INDEPENDENT_PUBLICATION = "independent_publication" + INDEPENDENT_ROLLBACK = "independent_rollback" + NO_IMPLICIT_PROPAGATION = "no_implicit_propagation" + + +class ModelRuntimeSeparationContract(BaseModel): + runtime_target: ModelRuntimeTarget + config_key: str + catalog_runtime_target: ModelRuntimeTarget + purpose: ModelRuntimePurpose + consumed_by_service: ServiceName + description: str + read_permission: AdminPermission = AdminPermission.VIEW_SYSTEM + write_permission: AdminPermission = AdminPermission.MANAGE_SETTINGS + published_independently: bool = True + rollback_independently: bool = True + cross_target_propagation_allowed: bool = False + affects_customer_response: bool = False + can_generate_code: bool = False + + +MODEL_RUNTIME_PROFILES: tuple[ModelRuntimeSeparationContract, ...] = ( + ModelRuntimeSeparationContract( + runtime_target=ModelRuntimeTarget.ATENDIMENTO, + config_key="atendimento_runtime_profile", + catalog_runtime_target=ModelRuntimeTarget.ATENDIMENTO, + purpose=ModelRuntimePurpose.CUSTOMER_RESPONSE, + consumed_by_service=ServiceName.PRODUCT, + description="Runtime do modelo que responde ao cliente final no fluxo de atendimento.", + affects_customer_response=True, + can_generate_code=False, + ), + ModelRuntimeSeparationContract( + runtime_target=ModelRuntimeTarget.TOOL_GENERATION, + config_key="tool_generation_runtime_profile", + catalog_runtime_target=ModelRuntimeTarget.TOOL_GENERATION, + purpose=ModelRuntimePurpose.TOOL_GENERATION_AND_VALIDATION, + consumed_by_service=ServiceName.ADMIN, + description="Runtime do modelo usado para gerar e validar novas tools no contexto administrativo.", + affects_customer_response=False, + can_generate_code=True, + ), +) + + +MODEL_RUNTIME_SEPARATION_RULES: tuple[ModelRuntimeSeparationRule, ...] = ( + ModelRuntimeSeparationRule.SEPARATE_CONFIG_KEYS, + ModelRuntimeSeparationRule.SEPARATE_CATALOG_TARGETS, + ModelRuntimeSeparationRule.INDEPENDENT_PUBLICATION, + ModelRuntimeSeparationRule.INDEPENDENT_ROLLBACK, + ModelRuntimeSeparationRule.NO_IMPLICIT_PROPAGATION, +) + + +def get_model_runtime_contract(runtime_target: ModelRuntimeTarget | str) -> ModelRuntimeSeparationContract | None: + normalized = str(runtime_target or "").strip().lower() + for runtime_contract in MODEL_RUNTIME_PROFILES: + if runtime_contract.runtime_target.value == normalized: + return runtime_contract + return None diff --git a/shared/contracts/product_operational_data.py b/shared/contracts/product_operational_data.py new file mode 100644 index 0000000..ed77bb1 --- /dev/null +++ b/shared/contracts/product_operational_data.py @@ -0,0 +1,375 @@ +"""Define o escopo de leitura operacional do admin sobre o servico de produto.""" + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel, Field + +from shared.contracts.access_control import AdminPermission + + +class OperationalDataDomain(str, Enum): + INVENTORY = "inventory" + SALES = "sales" + REVIEW = "review" + RENTAL = "rental" + CONVERSATION = "conversation" + INTEGRATION = "integration" + + +class OperationalDataSensitivity(str, Enum): + OPERATIONAL = "operational" + INTERNAL_IDENTIFIER = "internal_identifier" + CUSTOMER_IDENTIFIER = "customer_identifier" + FREE_TEXT = "free_text" + SECRET = "secret" + + +class OperationalReadGranularity(str, Enum): + AGGREGATE = "aggregate" + RECORD = "record" + + +class OperationalReadModel(str, Enum): + ADMIN_READ_MODEL = "admin_read_model" + + +class OperationalConsistencyModel(str, Enum): + EVENTUAL = "eventual" + + +class OperationalFreshnessTarget(str, Enum): + NEAR_REAL_TIME = "near_real_time" + INTRA_HOUR = "intra_hour" + INTRA_DAY = "intra_day" + + +class OperationalSyncStrategy(str, Enum): + ETL_INCREMENTAL = "etl_incremental" + + +class OperationalStorageShape(str, Enum): + SNAPSHOT_TABLE = "snapshot_table" + + +class OperationalQuerySurface(str, Enum): + DEDICATED_VIEW = "dedicated_view" + + +class OperationalFieldContract(BaseModel): + name: str + description: str + sensitivity: OperationalDataSensitivity = OperationalDataSensitivity.OPERATIONAL + + +class OperationalDatasetContract(BaseModel): + dataset_key: str + domain: OperationalDataDomain + description: str + source_table: str + read_permission: AdminPermission = AdminPermission.VIEW_REPORTS + report_read_model: OperationalReadModel = OperationalReadModel.ADMIN_READ_MODEL + consistency_model: OperationalConsistencyModel = OperationalConsistencyModel.EVENTUAL + sync_strategy: OperationalSyncStrategy = OperationalSyncStrategy.ETL_INCREMENTAL + storage_shape: OperationalStorageShape = OperationalStorageShape.SNAPSHOT_TABLE + query_surface: OperationalQuerySurface = OperationalQuerySurface.DEDICATED_VIEW + uses_product_replica: bool = False + direct_product_query_allowed: bool = False + freshness_target: OperationalFreshnessTarget = OperationalFreshnessTarget.INTRA_HOUR + allowed_granularities: tuple[OperationalReadGranularity, ...] = Field( + default=( + OperationalReadGranularity.AGGREGATE, + OperationalReadGranularity.RECORD, + ) + ) + write_allowed: bool = False + allowed_fields: tuple[OperationalFieldContract, ...] + blocked_fields: tuple[OperationalFieldContract, ...] = Field(default_factory=tuple) + + +PRODUCT_OPERATIONAL_DATASETS: tuple[OperationalDatasetContract, ...] = ( + OperationalDatasetContract( + dataset_key="vehicle_inventory", + domain=OperationalDataDomain.INVENTORY, + description="Estoque operacional de veiculos disponiveis para atendimento comercial.", + source_table="vehicles", + freshness_target=OperationalFreshnessTarget.INTRA_HOUR, + allowed_fields=( + OperationalFieldContract(name="id", description="Identificador tecnico do veiculo."), + OperationalFieldContract(name="modelo", description="Modelo comercial do veiculo."), + OperationalFieldContract(name="categoria", description="Categoria comercial do veiculo."), + OperationalFieldContract(name="preco", description="Preco anunciado no estoque."), + OperationalFieldContract(name="created_at", description="Data de entrada do registro no estoque."), + ), + ), + OperationalDatasetContract( + dataset_key="sales_orders", + domain=OperationalDataDomain.SALES, + description="Pedidos de venda usados para operacao, conversao e cancelamentos.", + source_table="orders", + freshness_target=OperationalFreshnessTarget.NEAR_REAL_TIME, + allowed_fields=( + OperationalFieldContract(name="numero_pedido", description="Numero publico do pedido."), + OperationalFieldContract(name="vehicle_id", description="Veiculo associado ao pedido."), + OperationalFieldContract(name="modelo_veiculo", description="Modelo comercial reservado no pedido."), + OperationalFieldContract(name="valor_veiculo", description="Valor negociado do veiculo."), + OperationalFieldContract(name="status", description="Status operacional do pedido."), + OperationalFieldContract(name="motivo_cancelamento", description="Motivo operacional do cancelamento."), + OperationalFieldContract(name="data_cancelamento", description="Momento em que o pedido foi cancelado."), + OperationalFieldContract(name="created_at", description="Data de criacao do pedido."), + OperationalFieldContract(name="updated_at", description="Data da ultima atualizacao do pedido."), + ), + blocked_fields=( + OperationalFieldContract( + name="user_id", + description="Identificador interno do usuario final no produto.", + sensitivity=OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ), + OperationalFieldContract( + name="cpf", + description="Identificador civil do cliente final.", + sensitivity=OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ), + ), + ), + OperationalDatasetContract( + dataset_key="review_schedules", + domain=OperationalDataDomain.REVIEW, + description="Agenda operacional de revisoes e disponibilidade de slots.", + source_table="review_schedules", + freshness_target=OperationalFreshnessTarget.NEAR_REAL_TIME, + allowed_fields=( + OperationalFieldContract(name="protocolo", description="Protocolo publico do agendamento."), + OperationalFieldContract(name="placa", description="Placa do veiculo agendado."), + OperationalFieldContract(name="data_hora", description="Data e hora do slot de revisao."), + OperationalFieldContract(name="status", description="Status operacional do agendamento."), + OperationalFieldContract(name="created_at", description="Data de criacao do agendamento."), + ), + blocked_fields=( + OperationalFieldContract( + name="user_id", + description="Identificador interno do usuario final no produto.", + sensitivity=OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ), + ), + ), + OperationalDatasetContract( + dataset_key="rental_fleet", + domain=OperationalDataDomain.RENTAL, + description="Frota operacional de locacao disponivel para consulta administrativa.", + source_table="rental_vehicles", + freshness_target=OperationalFreshnessTarget.NEAR_REAL_TIME, + allowed_fields=( + OperationalFieldContract(name="id", description="Identificador tecnico do veiculo de locacao."), + OperationalFieldContract(name="placa", description="Placa do veiculo de locacao."), + OperationalFieldContract(name="modelo", description="Modelo do veiculo de locacao."), + OperationalFieldContract(name="categoria", description="Categoria comercial da locacao."), + OperationalFieldContract(name="ano", description="Ano de fabricacao do veiculo."), + OperationalFieldContract(name="valor_diaria", description="Valor de diaria vigente."), + OperationalFieldContract(name="status", description="Status operacional do veiculo na frota."), + OperationalFieldContract(name="created_at", description="Data de cadastro do veiculo na frota."), + ), + ), + OperationalDatasetContract( + dataset_key="rental_contracts", + domain=OperationalDataDomain.RENTAL, + description="Contratos de locacao usados para operacao, retorno e inadimplencia.", + source_table="rental_contracts", + freshness_target=OperationalFreshnessTarget.NEAR_REAL_TIME, + allowed_fields=( + OperationalFieldContract(name="contrato_numero", description="Numero publico do contrato."), + OperationalFieldContract(name="rental_vehicle_id", description="Identificador tecnico do veiculo locado."), + OperationalFieldContract(name="placa", description="Placa do veiculo vinculado ao contrato."), + OperationalFieldContract(name="modelo_veiculo", description="Modelo do veiculo locado."), + OperationalFieldContract(name="categoria", description="Categoria da locacao."), + OperationalFieldContract(name="data_inicio", description="Inicio da locacao."), + OperationalFieldContract(name="data_fim_prevista", description="Fim previsto da locacao."), + OperationalFieldContract(name="data_devolucao", description="Momento efetivo da devolucao."), + OperationalFieldContract(name="valor_diaria", description="Valor unitario da diaria."), + OperationalFieldContract(name="valor_previsto", description="Valor previsto ao abrir o contrato."), + OperationalFieldContract(name="valor_final", description="Valor final consolidado da locacao."), + OperationalFieldContract(name="status", description="Status operacional do contrato."), + OperationalFieldContract(name="created_at", description="Data de criacao do contrato."), + OperationalFieldContract(name="updated_at", description="Data da ultima atualizacao do contrato."), + ), + blocked_fields=( + OperationalFieldContract( + name="user_id", + description="Identificador interno do usuario final no produto.", + sensitivity=OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ), + OperationalFieldContract( + name="cpf", + description="Identificador civil do cliente final.", + sensitivity=OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ), + OperationalFieldContract( + name="observacoes", + description="Campo livre informado durante a operacao de locacao.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + ), + ), + OperationalDatasetContract( + dataset_key="rental_payments", + domain=OperationalDataDomain.RENTAL, + description="Pagamentos de locacao usados para arrecadacao e conciliacao operacional.", + source_table="rental_payments", + freshness_target=OperationalFreshnessTarget.NEAR_REAL_TIME, + allowed_fields=( + OperationalFieldContract(name="protocolo", description="Protocolo publico do pagamento."), + OperationalFieldContract(name="contrato_numero", description="Contrato associado ao pagamento."), + OperationalFieldContract(name="placa", description="Placa vinculada ao contrato pago."), + OperationalFieldContract(name="valor", description="Valor liquidado no pagamento."), + OperationalFieldContract(name="data_pagamento", description="Momento do pagamento."), + OperationalFieldContract(name="created_at", description="Data de registro do pagamento."), + ), + blocked_fields=( + OperationalFieldContract( + name="user_id", + description="Identificador interno do usuario final no produto.", + sensitivity=OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ), + OperationalFieldContract( + name="rental_contract_id", + description="Chave tecnica interna do contrato no banco operacional.", + sensitivity=OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ), + OperationalFieldContract( + name="favorecido", + description="Nome textual do favorecido no comprovante.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="identificador_comprovante", + description="Identificador do comprovante de pagamento.", + sensitivity=OperationalDataSensitivity.SECRET, + ), + OperationalFieldContract( + name="observacoes", + description="Campo livre informado durante o pagamento.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + ), + ), + OperationalDatasetContract( + dataset_key="conversation_turns", + domain=OperationalDataDomain.CONVERSATION, + description="Telemetria operacional das conversas para eficiencia, erro e uso de tools.", + source_table="conversation_turns", + freshness_target=OperationalFreshnessTarget.INTRA_HOUR, + allowed_fields=( + OperationalFieldContract(name="request_id", description="Identificador tecnico do turno processado."), + OperationalFieldContract(name="conversation_id", description="Identificador tecnico da conversa."), + OperationalFieldContract(name="channel", description="Canal do atendimento."), + OperationalFieldContract(name="turn_status", description="Status do turno conversacional."), + OperationalFieldContract(name="intent", description="Intencao classificada para o turno."), + OperationalFieldContract(name="domain", description="Dominio operacional associado ao turno."), + OperationalFieldContract(name="action", description="Acao tomada pelo orquestrador."), + OperationalFieldContract(name="tool_name", description="Tool chamada durante o turno."), + OperationalFieldContract(name="elapsed_ms", description="Tempo de processamento do turno em milissegundos."), + OperationalFieldContract(name="started_at", description="Inicio do processamento do turno."), + OperationalFieldContract(name="completed_at", description="Fim do processamento do turno."), + ), + blocked_fields=( + OperationalFieldContract( + name="user_id", + description="Identificador interno do usuario final no produto.", + sensitivity=OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ), + OperationalFieldContract( + name="external_id", + description="Identificador externo do usuario final no canal.", + sensitivity=OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ), + OperationalFieldContract( + name="username", + description="Username do usuario final no canal.", + sensitivity=OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ), + OperationalFieldContract( + name="user_message", + description="Mensagem original do usuario final.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="assistant_response", + description="Resposta textual completa enviada ao usuario final.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="tool_arguments", + description="Payload bruto dos argumentos enviados para tools.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="error_detail", + description="Detalhe bruto de erro que pode carregar contexto sensivel.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + ), + ), + OperationalDatasetContract( + dataset_key="integration_deliveries", + domain=OperationalDataDomain.INTEGRATION, + description="Entrega operacional de eventos para provedores externos e observabilidade de falhas.", + source_table="integration_deliveries", + freshness_target=OperationalFreshnessTarget.INTRA_HOUR, + allowed_fields=( + OperationalFieldContract(name="route_id", description="Rota interna que originou a entrega."), + OperationalFieldContract(name="event_type", description="Tipo de evento entregue."), + OperationalFieldContract(name="provider", description="Provedor de integracao usado na entrega."), + OperationalFieldContract(name="status", description="Status atual da entrega."), + OperationalFieldContract(name="attempts", description="Quantidade de tentativas realizadas."), + OperationalFieldContract(name="dispatched_at", description="Momento do disparo da entrega."), + OperationalFieldContract(name="created_at", description="Data de criacao do registro de entrega."), + OperationalFieldContract(name="updated_at", description="Data da ultima atualizacao da entrega."), + ), + blocked_fields=( + OperationalFieldContract( + name="payload_json", + description="Payload bruto do evento entregue.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="recipient_email", + description="Email do destinatario final da integracao.", + sensitivity=OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ), + OperationalFieldContract( + name="recipient_name", + description="Nome do destinatario final da integracao.", + sensitivity=OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ), + OperationalFieldContract( + name="rendered_subject", + description="Assunto renderizado da mensagem enviada.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="rendered_body", + description="Corpo renderizado da mensagem enviada.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + OperationalFieldContract( + name="provider_message_id", + description="Identificador bruto devolvido pelo provedor externo.", + sensitivity=OperationalDataSensitivity.SECRET, + ), + OperationalFieldContract( + name="last_error", + description="Detalhe textual do ultimo erro de entrega.", + sensitivity=OperationalDataSensitivity.FREE_TEXT, + ), + ), + ), +) + + +def get_operational_dataset(dataset_key: str) -> OperationalDatasetContract | None: + normalized = str(dataset_key or "").strip().lower() + for dataset in PRODUCT_OPERATIONAL_DATASETS: + if dataset.dataset_key == normalized: + return dataset + return None diff --git a/shared/contracts/system_functional_configuration.py b/shared/contracts/system_functional_configuration.py new file mode 100644 index 0000000..b589c8d --- /dev/null +++ b/shared/contracts/system_functional_configuration.py @@ -0,0 +1,258 @@ +"""Define o escopo de configuracao funcional governada entre admin e product.""" + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel + +from shared.contracts.access_control import AdminPermission + + +class FunctionalConfigurationDomain(str, Enum): + MODEL_CATALOG = "model_catalog" + ATENDIMENTO_RUNTIME = "atendimento_runtime" + TOOL_GENERATION_RUNTIME = "tool_generation_runtime" + BOT_POLICY = "bot_policy" + CHANNEL_OPERATION = "channel_operation" + CONFIG_PUBLICATION = "config_publication" + + +class FunctionalConfigurationMutability(str, Enum): + READ_ONLY = "read_only" + DIRECTOR_GOVERNED = "director_governed" + + +class FunctionalConfigurationSource(str, Enum): + PLATFORM_CATALOG = "platform_catalog" + ADMIN_GOVERNED_STATE = "admin_governed_state" + PRODUCT_EFFECTIVE_STATE = "product_effective_state" + + +class FunctionalConfigurationPropagation(str, Enum): + OBSERVATION_ONLY = "observation_only" + VERSIONED_PUBLICATION = "versioned_publication" + + +class FunctionalConfigurationFieldContract(BaseModel): + name: str + description: str + writable: bool = True + secret: bool = False + + +class FunctionalConfigurationContract(BaseModel): + config_key: str + domain: FunctionalConfigurationDomain + description: str + source: FunctionalConfigurationSource + read_permission: AdminPermission = AdminPermission.VIEW_SYSTEM + write_permission: AdminPermission | None = AdminPermission.MANAGE_SETTINGS + mutability: FunctionalConfigurationMutability = FunctionalConfigurationMutability.DIRECTOR_GOVERNED + propagation: FunctionalConfigurationPropagation = ( + FunctionalConfigurationPropagation.VERSIONED_PUBLICATION + ) + affects_product_runtime: bool = True + direct_product_write_allowed: bool = False + fields: tuple[FunctionalConfigurationFieldContract, ...] + + +SYSTEM_FUNCTIONAL_CONFIGURATIONS: tuple[FunctionalConfigurationContract, ...] = ( + FunctionalConfigurationContract( + config_key="allowed_model_catalog", + domain=FunctionalConfigurationDomain.MODEL_CATALOG, + description="Catalogo de modelos liberados pela plataforma para atendimento e geracao de tools.", + source=FunctionalConfigurationSource.PLATFORM_CATALOG, + write_permission=None, + mutability=FunctionalConfigurationMutability.READ_ONLY, + propagation=FunctionalConfigurationPropagation.OBSERVATION_ONLY, + affects_product_runtime=False, + fields=( + FunctionalConfigurationFieldContract( + name="runtime_target", + description="Destino funcional do modelo, como atendimento ou geracao de tools.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="provider", + description="Provedor homologado para o modelo.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="model_name", + description="Nome tecnico do modelo liberado.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="capability_tags", + description="Capacidades suportadas pelo modelo homologado.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="status", + description="Estado de homologacao do modelo no catalogo da plataforma.", + writable=False, + ), + ), + ), + FunctionalConfigurationContract( + config_key="atendimento_runtime_profile", + domain=FunctionalConfigurationDomain.ATENDIMENTO_RUNTIME, + description="Perfil funcional ativo para o bot de atendimento no servico de produto.", + source=FunctionalConfigurationSource.ADMIN_GOVERNED_STATE, + fields=( + FunctionalConfigurationFieldContract( + name="provider", + description="Provedor selecionado para o atendimento.", + ), + FunctionalConfigurationFieldContract( + name="model_name", + description="Modelo selecionado para o atendimento.", + ), + FunctionalConfigurationFieldContract( + name="temperature", + description="Temperatura aplicada nas respostas do atendimento.", + ), + FunctionalConfigurationFieldContract( + name="max_output_tokens", + description="Limite de saida usado pelo atendimento.", + ), + FunctionalConfigurationFieldContract( + name="prompt_profile_ref", + description="Referencia da estrategia de prompt publicada para o atendimento.", + ), + FunctionalConfigurationFieldContract( + name="tool_policy_ref", + description="Referencia da politica de uso de tools pelo atendimento.", + ), + ), + ), + FunctionalConfigurationContract( + config_key="tool_generation_runtime_profile", + domain=FunctionalConfigurationDomain.TOOL_GENERATION_RUNTIME, + description="Perfil funcional usado para geracao e validacao automatica de novas tools.", + source=FunctionalConfigurationSource.ADMIN_GOVERNED_STATE, + fields=( + FunctionalConfigurationFieldContract( + name="provider", + description="Provedor selecionado para a geracao de tools.", + ), + FunctionalConfigurationFieldContract( + name="model_name", + description="Modelo selecionado para a geracao de tools.", + ), + FunctionalConfigurationFieldContract( + name="reasoning_profile", + description="Perfil de raciocinio aprovado para geracao de codigo.", + ), + FunctionalConfigurationFieldContract( + name="max_output_tokens", + description="Limite de saida usado na geracao de tools.", + ), + FunctionalConfigurationFieldContract( + name="validation_profile_ref", + description="Referencia da politica de validacao automatica de tools.", + ), + ), + ), + FunctionalConfigurationContract( + config_key="bot_behavior_policy", + domain=FunctionalConfigurationDomain.BOT_POLICY, + description="Politicas funcionais do fluxo do bot para fallback, handoff e uso de tools.", + source=FunctionalConfigurationSource.ADMIN_GOVERNED_STATE, + fields=( + FunctionalConfigurationFieldContract( + name="fallback_mode", + description="Modo funcional de fallback quando o bot nao conclui a tarefa.", + ), + FunctionalConfigurationFieldContract( + name="handoff_enabled", + description="Sinaliza se o fluxo pode encaminhar para atendimento humano.", + ), + FunctionalConfigurationFieldContract( + name="handoff_intents", + description="Lista de intencoes que forcam handoff humano.", + ), + FunctionalConfigurationFieldContract( + name="max_tool_calls_per_turn", + description="Limite de chamadas de tools por turno de atendimento.", + ), + FunctionalConfigurationFieldContract( + name="confirmation_policy", + description="Politica de confirmacao antes de acao critica no fluxo.", + ), + ), + ), + FunctionalConfigurationContract( + config_key="channel_operation_policy", + domain=FunctionalConfigurationDomain.CHANNEL_OPERATION, + description="Politicas funcionais por canal, incluindo habilitacao, manutencao e janela operacional.", + source=FunctionalConfigurationSource.ADMIN_GOVERNED_STATE, + fields=( + FunctionalConfigurationFieldContract( + name="channel", + description="Canal operacional ao qual a politica se aplica.", + ), + FunctionalConfigurationFieldContract( + name="enabled", + description="Indica se o canal esta habilitado para atendimento.", + ), + FunctionalConfigurationFieldContract( + name="maintenance_mode", + description="Sinaliza se o canal esta em manutencao controlada.", + ), + FunctionalConfigurationFieldContract( + name="default_route", + description="Rota funcional padrao usada pelo canal.", + ), + FunctionalConfigurationFieldContract( + name="operation_window_ref", + description="Referencia da janela operacional aplicada ao canal.", + ), + ), + ), + FunctionalConfigurationContract( + config_key="published_runtime_state", + domain=FunctionalConfigurationDomain.CONFIG_PUBLICATION, + description="Estado efetivo publicado no produto para auditoria de versao e aplicacao runtime.", + source=FunctionalConfigurationSource.PRODUCT_EFFECTIVE_STATE, + write_permission=None, + mutability=FunctionalConfigurationMutability.READ_ONLY, + propagation=FunctionalConfigurationPropagation.OBSERVATION_ONLY, + fields=( + FunctionalConfigurationFieldContract( + name="config_scope", + description="Escopo funcional da configuracao publicada.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="active_version", + description="Versao funcional atualmente ativa no produto.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="published_by", + description="Identificador administrativo de quem publicou a configuracao.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="published_at", + description="Momento da ultima publicacao governada.", + writable=False, + ), + FunctionalConfigurationFieldContract( + name="applied_at", + description="Momento em que o produto aplicou a configuracao em runtime.", + writable=False, + ), + ), + ), +) + + +def get_functional_configuration(config_key: str) -> FunctionalConfigurationContract | None: + normalized = str(config_key or "").strip().lower() + for configuration in SYSTEM_FUNCTIONAL_CONFIGURATIONS: + if configuration.config_key == normalized: + return configuration + return None diff --git a/tests/test_shared_contracts.py b/tests/test_shared_contracts.py index 1a23921..f98a862 100644 --- a/tests/test_shared_contracts.py +++ b/tests/test_shared_contracts.py @@ -3,13 +3,41 @@ from datetime import datetime, timezone from shared.contracts import ( AdminPermission, + BOT_GOVERNED_SETTINGS, + MODEL_RUNTIME_PROFILES, + MODEL_RUNTIME_SEPARATION_RULES, + BotGovernanceArea, + BotGovernanceMutability, + FunctionalConfigurationDomain, + FunctionalConfigurationMutability, + FunctionalConfigurationPropagation, + FunctionalConfigurationSource, + ModelRuntimePurpose, + ModelRuntimeSeparationRule, + ModelRuntimeTarget, + OperationalConsistencyModel, + OperationalDataDomain, + OperationalDataSensitivity, + OperationalFreshnessTarget, + OperationalQuerySurface, + OperationalReadGranularity, + OperationalReadModel, + OperationalStorageShape, + OperationalSyncStrategy, + PRODUCT_OPERATIONAL_DATASETS, PublishedToolContract, ServiceName, StaffRole, + SYSTEM_FUNCTIONAL_CONFIGURATIONS, ToolLifecycleStatus, ToolParameterContract, ToolParameterType, ToolPublicationEnvelope, + get_bot_governed_setting, + get_functional_configuration, + get_model_runtime_contract, + get_operational_dataset, + normalize_staff_role, permissions_for_role, role_has_permission, role_includes, @@ -18,23 +46,36 @@ from shared.contracts import ( class AccessControlContractTests(unittest.TestCase): def test_role_hierarchy_is_ordered(self): - self.assertTrue(role_includes(StaffRole.ADMIN, StaffRole.STAFF)) - self.assertTrue(role_includes(StaffRole.STAFF, StaffRole.VIEWER)) - self.assertFalse(role_includes(StaffRole.VIEWER, StaffRole.ADMIN)) + self.assertTrue(role_includes(StaffRole.DIRETOR, StaffRole.COLABORADOR)) + self.assertFalse(role_includes(StaffRole.COLABORADOR, StaffRole.DIRETOR)) - def test_permissions_are_inherited_by_higher_roles(self): + def test_legacy_role_aliases_are_normalized_to_portuguese_roles(self): + self.assertEqual(normalize_staff_role("viewer"), StaffRole.COLABORADOR) + self.assertEqual(normalize_staff_role("staff"), StaffRole.COLABORADOR) + self.assertEqual(normalize_staff_role("admin"), StaffRole.DIRETOR) + + def test_permissions_are_inherited_by_director(self): self.assertIn( AdminPermission.VIEW_REPORTS, - permissions_for_role(StaffRole.VIEWER), + permissions_for_role(StaffRole.COLABORADOR), ) self.assertTrue( - role_has_permission(StaffRole.STAFF, AdminPermission.MANAGE_TOOL_DRAFTS) + role_has_permission(StaffRole.COLABORADOR, AdminPermission.MANAGE_TOOL_DRAFTS) ) - self.assertTrue( - role_has_permission(StaffRole.ADMIN, AdminPermission.MANAGE_SETTINGS) + self.assertFalse( + role_has_permission(StaffRole.COLABORADOR, AdminPermission.REVIEW_TOOL_GENERATIONS) + ) + self.assertFalse( + role_has_permission(StaffRole.COLABORADOR, AdminPermission.PUBLISH_TOOLS) ) self.assertFalse( - role_has_permission(StaffRole.VIEWER, AdminPermission.PUBLISH_TOOLS) + role_has_permission(StaffRole.COLABORADOR, AdminPermission.MANAGE_SETTINGS) + ) + self.assertTrue( + role_has_permission(StaffRole.DIRETOR, AdminPermission.MANAGE_SETTINGS) + ) + self.assertTrue( + role_has_permission(StaffRole.DIRETOR, AdminPermission.MANAGE_STAFF_ACCOUNTS) ) @@ -58,7 +99,7 @@ class ToolPublicationContractTests(unittest.TestCase): implementation_callable="run", checksum="sha256:abc123", published_at=datetime(2026, 3, 26, 12, 0, tzinfo=timezone.utc), - published_by="staff:1", + published_by="diretor:1", ) envelope = ToolPublicationEnvelope( @@ -78,5 +119,268 @@ class ToolPublicationContractTests(unittest.TestCase): ) +class ProductOperationalDataContractTests(unittest.TestCase): + def test_catalog_exposes_expected_operational_domains(self): + self.assertEqual(len(PRODUCT_OPERATIONAL_DATASETS), 8) + self.assertEqual( + {dataset.domain for dataset in PRODUCT_OPERATIONAL_DATASETS}, + { + OperationalDataDomain.INVENTORY, + OperationalDataDomain.SALES, + OperationalDataDomain.REVIEW, + OperationalDataDomain.RENTAL, + OperationalDataDomain.CONVERSATION, + OperationalDataDomain.INTEGRATION, + }, + ) + + def test_all_datasets_use_async_admin_read_model_without_direct_product_query(self): + for dataset in PRODUCT_OPERATIONAL_DATASETS: + self.assertEqual(dataset.read_permission, AdminPermission.VIEW_REPORTS) + self.assertEqual(dataset.report_read_model, OperationalReadModel.ADMIN_READ_MODEL) + self.assertEqual(dataset.consistency_model, OperationalConsistencyModel.EVENTUAL) + self.assertEqual(dataset.sync_strategy, OperationalSyncStrategy.ETL_INCREMENTAL) + self.assertEqual(dataset.storage_shape, OperationalStorageShape.SNAPSHOT_TABLE) + self.assertEqual(dataset.query_surface, OperationalQuerySurface.DEDICATED_VIEW) + self.assertFalse(dataset.uses_product_replica) + self.assertFalse(dataset.direct_product_query_allowed) + self.assertFalse(dataset.write_allowed) + + def test_sales_orders_dataset_is_read_only_and_blocks_customer_identity(self): + dataset = get_operational_dataset("sales_orders") + + self.assertIsNotNone(dataset) + self.assertEqual( + dataset.freshness_target, + OperationalFreshnessTarget.NEAR_REAL_TIME, + ) + self.assertEqual( + dataset.allowed_granularities, + ( + OperationalReadGranularity.AGGREGATE, + OperationalReadGranularity.RECORD, + ), + ) + self.assertIn("numero_pedido", [field.name for field in dataset.allowed_fields]) + + blocked_fields = {field.name: field.sensitivity for field in dataset.blocked_fields} + self.assertEqual( + blocked_fields["cpf"], + OperationalDataSensitivity.CUSTOMER_IDENTIFIER, + ) + self.assertEqual( + blocked_fields["user_id"], + OperationalDataSensitivity.INTERNAL_IDENTIFIER, + ) + + def test_conversation_turns_dataset_exposes_telemetry_without_raw_messages(self): + dataset = get_operational_dataset("conversation_turns") + + self.assertIsNotNone(dataset) + self.assertEqual( + dataset.freshness_target, + OperationalFreshnessTarget.INTRA_HOUR, + ) + self.assertIn("tool_name", [field.name for field in dataset.allowed_fields]) + + blocked_names = {field.name for field in dataset.blocked_fields} + self.assertIn("user_message", blocked_names) + self.assertIn("assistant_response", blocked_names) + self.assertIn("tool_arguments", blocked_names) + self.assertIn("error_detail", blocked_names) + + def test_rental_payment_dataset_blocks_receipt_identifiers(self): + dataset = get_operational_dataset("rental_payments") + + self.assertIsNotNone(dataset) + blocked_fields = {field.name: field.sensitivity for field in dataset.blocked_fields} + self.assertEqual( + blocked_fields["identificador_comprovante"], + OperationalDataSensitivity.SECRET, + ) + self.assertNotIn("identificador_comprovante", [field.name for field in dataset.allowed_fields]) + + def test_unknown_operational_dataset_returns_none(self): + self.assertIsNone(get_operational_dataset("customers")) + + +class SystemFunctionalConfigurationContractTests(unittest.TestCase): + def test_catalog_exposes_expected_configuration_domains(self): + self.assertEqual(len(SYSTEM_FUNCTIONAL_CONFIGURATIONS), 6) + self.assertEqual( + {configuration.domain for configuration in SYSTEM_FUNCTIONAL_CONFIGURATIONS}, + { + FunctionalConfigurationDomain.MODEL_CATALOG, + FunctionalConfigurationDomain.ATENDIMENTO_RUNTIME, + FunctionalConfigurationDomain.TOOL_GENERATION_RUNTIME, + FunctionalConfigurationDomain.BOT_POLICY, + FunctionalConfigurationDomain.CHANNEL_OPERATION, + FunctionalConfigurationDomain.CONFIG_PUBLICATION, + }, + ) + + def test_director_governed_configurations_require_manage_settings(self): + for configuration in SYSTEM_FUNCTIONAL_CONFIGURATIONS: + self.assertEqual(configuration.read_permission, AdminPermission.VIEW_SYSTEM) + self.assertFalse(configuration.direct_product_write_allowed) + + if configuration.mutability == FunctionalConfigurationMutability.DIRECTOR_GOVERNED: + self.assertEqual(configuration.write_permission, AdminPermission.MANAGE_SETTINGS) + self.assertEqual( + configuration.propagation, + FunctionalConfigurationPropagation.VERSIONED_PUBLICATION, + ) + else: + self.assertIsNone(configuration.write_permission) + self.assertEqual( + configuration.propagation, + FunctionalConfigurationPropagation.OBSERVATION_ONLY, + ) + + def test_atendimento_and_tool_generation_profiles_are_separated(self): + atendimento = get_functional_configuration("atendimento_runtime_profile") + tool_generation = get_functional_configuration("tool_generation_runtime_profile") + + self.assertIsNotNone(atendimento) + self.assertIsNotNone(tool_generation) + self.assertEqual( + atendimento.domain, + FunctionalConfigurationDomain.ATENDIMENTO_RUNTIME, + ) + self.assertEqual( + tool_generation.domain, + FunctionalConfigurationDomain.TOOL_GENERATION_RUNTIME, + ) + self.assertNotEqual(atendimento.config_key, tool_generation.config_key) + self.assertEqual(atendimento.source, FunctionalConfigurationSource.ADMIN_GOVERNED_STATE) + self.assertEqual(tool_generation.source, FunctionalConfigurationSource.ADMIN_GOVERNED_STATE) + + def test_read_only_configuration_surfaces_are_catalog_and_effective_runtime_state(self): + catalog = get_functional_configuration("allowed_model_catalog") + published_state = get_functional_configuration("published_runtime_state") + + self.assertIsNotNone(catalog) + self.assertIsNotNone(published_state) + self.assertEqual(catalog.mutability, FunctionalConfigurationMutability.READ_ONLY) + self.assertEqual(published_state.mutability, FunctionalConfigurationMutability.READ_ONLY) + self.assertEqual(catalog.source, FunctionalConfigurationSource.PLATFORM_CATALOG) + self.assertEqual( + published_state.source, + FunctionalConfigurationSource.PRODUCT_EFFECTIVE_STATE, + ) + self.assertTrue(all(not field.writable for field in catalog.fields)) + self.assertTrue(all(not field.writable for field in published_state.fields)) + + def test_unknown_functional_configuration_returns_none(self): + self.assertIsNone(get_functional_configuration("database_credentials")) + + +class BotGovernedConfigurationContractTests(unittest.TestCase): + def test_catalog_exposes_expected_bot_governance_areas(self): + self.assertEqual(len(BOT_GOVERNED_SETTINGS), 15) + self.assertEqual( + {setting.area for setting in BOT_GOVERNED_SETTINGS}, + { + BotGovernanceArea.MODEL_SELECTION, + BotGovernanceArea.RESPONSE_GENERATION, + BotGovernanceArea.TOOL_USAGE, + BotGovernanceArea.FALLBACK_AND_HANDOFF, + BotGovernanceArea.CHANNEL_OPERATION, + }, + ) + + def test_all_bot_settings_are_director_governed_and_versioned(self): + for setting in BOT_GOVERNED_SETTINGS: + self.assertEqual(setting.read_permission, AdminPermission.VIEW_SYSTEM) + self.assertEqual(setting.write_permission, AdminPermission.MANAGE_SETTINGS) + self.assertEqual( + setting.mutability, + BotGovernanceMutability.DIRECTOR_GOVERNED, + ) + self.assertTrue(setting.versioned_publication_required) + self.assertFalse(setting.direct_product_write_allowed) + + def test_bot_governance_excludes_tool_generation_runtime(self): + self.assertEqual( + {setting.parent_config_key for setting in BOT_GOVERNED_SETTINGS}, + { + "atendimento_runtime_profile", + "bot_behavior_policy", + "channel_operation_policy", + }, + ) + self.assertNotIn( + "tool_generation_runtime_profile", + {setting.parent_config_key for setting in BOT_GOVERNED_SETTINGS}, + ) + + def test_governed_settings_cover_model_policy_and_channel_controls(self): + governed_keys = {setting.setting_key for setting in BOT_GOVERNED_SETTINGS} + self.assertIn("bot_model_provider", governed_keys) + self.assertIn("bot_prompt_profile_ref", governed_keys) + self.assertIn("bot_tool_policy_ref", governed_keys) + self.assertIn("bot_handoff_enabled", governed_keys) + self.assertIn("channel_maintenance_mode", governed_keys) + + def test_unknown_bot_governed_setting_returns_none(self): + self.assertIsNone(get_bot_governed_setting("provider_api_key")) + + +class ModelRuntimeSeparationContractTests(unittest.TestCase): + def test_runtime_profiles_exist_for_attendimento_and_tool_generation(self): + self.assertEqual(len(MODEL_RUNTIME_PROFILES), 2) + self.assertEqual( + {runtime_contract.runtime_target for runtime_contract in MODEL_RUNTIME_PROFILES}, + { + ModelRuntimeTarget.ATENDIMENTO, + ModelRuntimeTarget.TOOL_GENERATION, + }, + ) + + def test_attendimento_and_generation_have_distinct_consumers_and_purposes(self): + atendimento = get_model_runtime_contract("atendimento") + tool_generation = get_model_runtime_contract("tool_generation") + + self.assertIsNotNone(atendimento) + self.assertIsNotNone(tool_generation) + self.assertEqual(atendimento.config_key, "atendimento_runtime_profile") + self.assertEqual(tool_generation.config_key, "tool_generation_runtime_profile") + self.assertEqual(atendimento.consumed_by_service, ServiceName.PRODUCT) + self.assertEqual(tool_generation.consumed_by_service, ServiceName.ADMIN) + self.assertEqual(atendimento.purpose, ModelRuntimePurpose.CUSTOMER_RESPONSE) + self.assertEqual( + tool_generation.purpose, + ModelRuntimePurpose.TOOL_GENERATION_AND_VALIDATION, + ) + self.assertTrue(atendimento.affects_customer_response) + self.assertFalse(tool_generation.affects_customer_response) + self.assertFalse(atendimento.can_generate_code) + self.assertTrue(tool_generation.can_generate_code) + + def test_runtime_profiles_require_independent_publication_and_rollback(self): + for runtime_contract in MODEL_RUNTIME_PROFILES: + self.assertEqual(runtime_contract.read_permission, AdminPermission.VIEW_SYSTEM) + self.assertEqual(runtime_contract.write_permission, AdminPermission.MANAGE_SETTINGS) + self.assertTrue(runtime_contract.published_independently) + self.assertTrue(runtime_contract.rollback_independently) + self.assertFalse(runtime_contract.cross_target_propagation_allowed) + self.assertEqual(runtime_contract.catalog_runtime_target, runtime_contract.runtime_target) + + def test_separation_rules_forbid_implicit_cross_runtime_propagation(self): + self.assertEqual( + set(MODEL_RUNTIME_SEPARATION_RULES), + { + ModelRuntimeSeparationRule.SEPARATE_CONFIG_KEYS, + ModelRuntimeSeparationRule.SEPARATE_CATALOG_TARGETS, + ModelRuntimeSeparationRule.INDEPENDENT_PUBLICATION, + ModelRuntimeSeparationRule.INDEPENDENT_ROLLBACK, + ModelRuntimeSeparationRule.NO_IMPLICIT_PROPAGATION, + }, + ) + + def test_unknown_model_runtime_returns_none(self): + self.assertIsNone(get_model_runtime_contract("shared_runtime")) + + if __name__ == "__main__": unittest.main()