From 17583236a66d178370b12510439eb7c5d76144ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Thu, 26 Mar 2026 12:04:25 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20feat(admin):=20criar=20?= =?UTF-8?q?scaffold=20inicial=20do=20orquestrador-admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin_app/__init__.py | 1 + admin_app/__main__.py | 3 + admin_app/api/__init__.py | 1 + admin_app/api/dependencies.py | 10 + admin_app/api/router.py | 6 + admin_app/api/routes/__init__.py | 1 + admin_app/api/routes/system.py | 27 ++ admin_app/api/schemas.py | 23 ++ admin_app/app_factory.py | 17 ++ admin_app/core/__init__.py | 1 + admin_app/core/settings.py | 48 ++++ admin_app/db/__init__.py | 1 + admin_app/db/database.py | 32 +++ admin_app/db/models/__init__.py | 3 + admin_app/db/models/base.py | 17 ++ admin_app/main.py | 3 + admin_app/repositories/__init__.py | 3 + admin_app/repositories/base_repository.py | 6 + admin_app/services/__init__.py | 3 + admin_app/services/system_service.py | 31 +++ ...01-separate-admin-and-customer-identity.md | 139 ++++++++++ .../0002-split-product-and-admin-services.md | 246 ++++++++++++++++++ tests/test_admin_app_bootstrap.py | 67 +++++ 23 files changed, 689 insertions(+) create mode 100644 admin_app/__init__.py create mode 100644 admin_app/__main__.py create mode 100644 admin_app/api/__init__.py create mode 100644 admin_app/api/dependencies.py create mode 100644 admin_app/api/router.py create mode 100644 admin_app/api/routes/__init__.py create mode 100644 admin_app/api/routes/system.py create mode 100644 admin_app/api/schemas.py create mode 100644 admin_app/app_factory.py create mode 100644 admin_app/core/__init__.py create mode 100644 admin_app/core/settings.py create mode 100644 admin_app/db/__init__.py create mode 100644 admin_app/db/database.py create mode 100644 admin_app/db/models/__init__.py create mode 100644 admin_app/db/models/base.py create mode 100644 admin_app/main.py create mode 100644 admin_app/repositories/__init__.py create mode 100644 admin_app/repositories/base_repository.py create mode 100644 admin_app/services/__init__.py create mode 100644 admin_app/services/system_service.py create mode 100644 docs/adr/0001-separate-admin-and-customer-identity.md create mode 100644 docs/adr/0002-split-product-and-admin-services.md create mode 100644 tests/test_admin_app_bootstrap.py diff --git a/admin_app/__init__.py b/admin_app/__init__.py new file mode 100644 index 0000000..4bdb988 --- /dev/null +++ b/admin_app/__init__.py @@ -0,0 +1 @@ +"""Runtime administrativo do orquestrador.""" diff --git a/admin_app/__main__.py b/admin_app/__main__.py new file mode 100644 index 0000000..23bcbec --- /dev/null +++ b/admin_app/__main__.py @@ -0,0 +1,3 @@ +from admin_app.main import app + +__all__ = ["app"] diff --git a/admin_app/api/__init__.py b/admin_app/api/__init__.py new file mode 100644 index 0000000..f265b6a --- /dev/null +++ b/admin_app/api/__init__.py @@ -0,0 +1 @@ +"""Camada HTTP do servico administrativo.""" diff --git a/admin_app/api/dependencies.py b/admin_app/api/dependencies.py new file mode 100644 index 0000000..264ebeb --- /dev/null +++ b/admin_app/api/dependencies.py @@ -0,0 +1,10 @@ +from fastapi import Request + +from admin_app.core.settings import AdminSettings, get_admin_settings + + +def get_settings(request: Request) -> AdminSettings: + app_settings = getattr(request.app.state, "admin_settings", None) + if isinstance(app_settings, AdminSettings): + return app_settings + return get_admin_settings() diff --git a/admin_app/api/router.py b/admin_app/api/router.py new file mode 100644 index 0000000..41463ba --- /dev/null +++ b/admin_app/api/router.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +from admin_app.api.routes.system import router as system_router + +api_router = APIRouter() +api_router.include_router(system_router) diff --git a/admin_app/api/routes/__init__.py b/admin_app/api/routes/__init__.py new file mode 100644 index 0000000..69e3bbc --- /dev/null +++ b/admin_app/api/routes/__init__.py @@ -0,0 +1 @@ +"""Rotas administrativas do servico interno.""" diff --git a/admin_app/api/routes/system.py b/admin_app/api/routes/system.py new file mode 100644 index 0000000..1c01d62 --- /dev/null +++ b/admin_app/api/routes/system.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Depends + +from admin_app.api.dependencies import get_settings +from admin_app.api.schemas import AdminHealthResponse, AdminRootResponse, AdminSystemInfoResponse +from admin_app.core.settings import AdminSettings +from admin_app.services.system_service import SystemService + +router = APIRouter(tags=["system"]) + + +def _build_service(settings: AdminSettings) -> SystemService: + return SystemService(settings=settings) + + +@router.get("/", response_model=AdminRootResponse) +def root(settings: AdminSettings = Depends(get_settings)): + return _build_service(settings).build_root_payload() + + +@router.get("/health", response_model=AdminHealthResponse) +def health_check(settings: AdminSettings = Depends(get_settings)): + return _build_service(settings).build_health_payload() + + +@router.get("/system/info", response_model=AdminSystemInfoResponse) +def system_info(settings: AdminSettings = Depends(get_settings)): + return _build_service(settings).build_system_info_payload() diff --git a/admin_app/api/schemas.py b/admin_app/api/schemas.py new file mode 100644 index 0000000..26939e7 --- /dev/null +++ b/admin_app/api/schemas.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + + +class AdminRootResponse(BaseModel): + service: str + status: str + message: str + environment: str + + +class AdminHealthResponse(BaseModel): + service: str + status: str + version: str + + +class AdminSystemInfoResponse(BaseModel): + service: str + app_name: str + environment: str + version: str + api_prefix: str + debug: bool diff --git a/admin_app/app_factory.py b/admin_app/app_factory.py new file mode 100644 index 0000000..06b2b12 --- /dev/null +++ b/admin_app/app_factory.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI + +from admin_app.api.router import api_router +from admin_app.core.settings import AdminSettings, get_admin_settings + + +# Fabrica explicita do runtime administrativo para facilitar testes e futura configuracao. +def create_app(settings: AdminSettings | None = None) -> FastAPI: + resolved_settings = settings or get_admin_settings() + app = FastAPI( + title=resolved_settings.admin_app_name, + version=resolved_settings.admin_version, + debug=resolved_settings.admin_debug, + ) + app.state.admin_settings = resolved_settings + app.include_router(api_router, prefix=resolved_settings.admin_api_prefix) + return app diff --git a/admin_app/core/__init__.py b/admin_app/core/__init__.py new file mode 100644 index 0000000..46b00f0 --- /dev/null +++ b/admin_app/core/__init__.py @@ -0,0 +1 @@ +"""Configuracoes centrais do servico administrativo.""" diff --git a/admin_app/core/settings.py b/admin_app/core/settings.py new file mode 100644 index 0000000..f6fb0a4 --- /dev/null +++ b/admin_app/core/settings.py @@ -0,0 +1,48 @@ +from functools import lru_cache + +from pydantic import field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class AdminSettings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + extra="ignore", + ) + + admin_app_name: str = "Orquestrador Admin" + admin_environment: str = "production" + admin_debug: bool = False + admin_version: str = "0.1.0" + admin_api_prefix: str = "" + + # Banco administrativo do runtime interno. + admin_db_host: str = "127.0.0.1" + admin_db_port: int = 3306 + admin_db_user: str = "root" + admin_db_password: str = "" + admin_db_name: str = "orquestrador_admin" + admin_db_cloud_sql_connection_name: str | None = None + + @field_validator("admin_debug", mode="before") + @classmethod + def parse_debug_aliases(cls, value): + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"debug", "development", "dev"}: + return True + if normalized in {"release", "production", "prod"}: + return False + return value + + @field_validator("admin_environment", "admin_api_prefix", mode="before") + @classmethod + def normalize_text_settings(cls, value): + if isinstance(value, str): + return value.strip() + return value + + +@lru_cache(maxsize=1) +def get_admin_settings() -> AdminSettings: + return AdminSettings() diff --git a/admin_app/db/__init__.py b/admin_app/db/__init__.py new file mode 100644 index 0000000..9a201cb --- /dev/null +++ b/admin_app/db/__init__.py @@ -0,0 +1 @@ +"""Persistencia do servico administrativo.""" diff --git a/admin_app/db/database.py b/admin_app/db/database.py new file mode 100644 index 0000000..fa939cb --- /dev/null +++ b/admin_app/db/database.py @@ -0,0 +1,32 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +from admin_app.core.settings import get_admin_settings + +settings = get_admin_settings() +admin_cloud_sql = settings.admin_db_cloud_sql_connection_name + +if admin_cloud_sql: + ADMIN_DATABASE_URL = ( + f"mysql+pymysql://{settings.admin_db_user}:{settings.admin_db_password}@/{settings.admin_db_name}" + f"?unix_socket=/cloudsql/{admin_cloud_sql}" + ) +else: + ADMIN_DATABASE_URL = ( + f"mysql+pymysql://{settings.admin_db_user}:{settings.admin_db_password}@" + f"{settings.admin_db_host}:{settings.admin_db_port}/{settings.admin_db_name}" + ) + +admin_engine = create_engine( + ADMIN_DATABASE_URL, + pool_pre_ping=True, + connect_args={"connect_timeout": 5}, +) + +AdminSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=admin_engine, +) + +AdminBase = declarative_base() diff --git a/admin_app/db/models/__init__.py b/admin_app/db/models/__init__.py new file mode 100644 index 0000000..93e1e84 --- /dev/null +++ b/admin_app/db/models/__init__.py @@ -0,0 +1,3 @@ +from admin_app.db.models.base import AdminTimestampedModel + +__all__ = ["AdminTimestampedModel"] diff --git a/admin_app/db/models/base.py b/admin_app/db/models/base.py new file mode 100644 index 0000000..bc8c3c5 --- /dev/null +++ b/admin_app/db/models/base.py @@ -0,0 +1,17 @@ +from sqlalchemy import DateTime +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.sql import func + +from admin_app.db.database import AdminBase + + +# Base abstrata com timestamps para futuras entidades administrativas. +class AdminTimestampedModel(AdminBase): + __abstract__ = True + + created_at: Mapped[object] = mapped_column(DateTime, server_default=func.current_timestamp()) + updated_at: Mapped[object] = mapped_column( + DateTime, + server_default=func.current_timestamp(), + onupdate=func.current_timestamp(), + ) diff --git a/admin_app/main.py b/admin_app/main.py new file mode 100644 index 0000000..4b481a2 --- /dev/null +++ b/admin_app/main.py @@ -0,0 +1,3 @@ +from admin_app.app_factory import create_app + +app = create_app() diff --git a/admin_app/repositories/__init__.py b/admin_app/repositories/__init__.py new file mode 100644 index 0000000..60eb35b --- /dev/null +++ b/admin_app/repositories/__init__.py @@ -0,0 +1,3 @@ +from admin_app.repositories.base_repository import BaseRepository + +__all__ = ["BaseRepository"] diff --git a/admin_app/repositories/base_repository.py b/admin_app/repositories/base_repository.py new file mode 100644 index 0000000..dccbb5b --- /dev/null +++ b/admin_app/repositories/base_repository.py @@ -0,0 +1,6 @@ +from sqlalchemy.orm import Session + + +class BaseRepository: + def __init__(self, db: Session): + self.db = db diff --git a/admin_app/services/__init__.py b/admin_app/services/__init__.py new file mode 100644 index 0000000..b211bd7 --- /dev/null +++ b/admin_app/services/__init__.py @@ -0,0 +1,3 @@ +from admin_app.services.system_service import SystemService + +__all__ = ["SystemService"] diff --git a/admin_app/services/system_service.py b/admin_app/services/system_service.py new file mode 100644 index 0000000..858f11c --- /dev/null +++ b/admin_app/services/system_service.py @@ -0,0 +1,31 @@ +from admin_app.core.settings import AdminSettings + + +class SystemService: + def __init__(self, settings: AdminSettings): + self.settings = settings + + def build_root_payload(self) -> dict: + return { + "service": "orquestrador-admin", + "status": "ok", + "message": "Servico administrativo inicializado.", + "environment": self.settings.admin_environment, + } + + def build_health_payload(self) -> dict: + return { + "service": "orquestrador-admin", + "status": "ok", + "version": self.settings.admin_version, + } + + def build_system_info_payload(self) -> dict: + return { + "service": "orquestrador-admin", + "app_name": self.settings.admin_app_name, + "environment": self.settings.admin_environment, + "version": self.settings.admin_version, + "api_prefix": self.settings.admin_api_prefix, + "debug": self.settings.admin_debug, + } diff --git a/docs/adr/0001-separate-admin-and-customer-identity.md b/docs/adr/0001-separate-admin-and-customer-identity.md new file mode 100644 index 0000000..dd52e49 --- /dev/null +++ b/docs/adr/0001-separate-admin-and-customer-identity.md @@ -0,0 +1,139 @@ +# ADR 0001 - Separar usuario de atendimento de conta administrativa interna + +## Status +Accepted + +## Contexto +Hoje o sistema possui um conceito principal de usuario em `app/db/mock_models.py` (`User`). +Esse registro representa a identidade operacional do atendimento e nasce a partir de canais externos, como Telegram. +Ele serve para vincular conversas, pedidos, locacoes, revisoes e contexto transacional do usuario final. + +Para a frente de auto-incremento de tools, precisaremos de uma area interna com login, permissao, auditoria e publicacao controlada. +Misturar essa conta interna com o `User` atual criaria problemas de seguranca, modelagem e isolamento de dominio. + +## Decisao +Vamos separar explicitamente dois dominios de identidade: + +1. `AtendimentoUser` + - Continua sendo o `User` atual do banco operacional/mock. + - Representa clientes e pessoas atendidas por canais externos. + - Continua vinculado a conversa, pedido, revisao, locacao e historico operacional. + +2. `StaffAccount` + - Sera uma nova entidade para acesso administrativo interno. + - Representa funcionarios e administradores da empresa. + - Sera usada para login no painel interno, configuracao do sistema, criacao/aprovacao de tools e auditoria. + +## Fronteira entre os dois tipos de conta + +### AtendimentoUser +- Banco: operacional/mock (`MockBase`) +- Origem: canal externo (`channel`, `external_id`) +- Autenticacao: indireta, via canal de atendimento +- Responsabilidade: atendimento ao cliente e contexto de negocio +- Nao deve receber credenciais de painel interno + +### StaffAccount +- Banco: administrativo/tools (`Base`) +- Origem: cadastro interno controlado +- Autenticacao: login web proprio +- Responsabilidade: administracao, configuracao e governanca de tools +- Nao deve ser usado para identificar cliente do atendimento + +## Racional para usar o banco administrativo/tools para StaffAccount +O projeto ja possui um banco administrativo ligado a `Base`, hoje usado para `tools`. +Como a nova frente trata de governanca do sistema e nao de jornada do cliente final, o lugar mais coerente para `StaffAccount` e para os metadados de geracao/publicacao e esse mesmo dominio administrativo. + +Isso reduz acoplamento com o banco operacional e evita misturar seguranca interna com dados de atendimento. + +## Entidades alvo derivadas desta decisao +As proximas fases devem introduzir, no banco administrativo, entidades como: + +- `StaffAccount` +- `StaffSession` ou estrategia equivalente de token +- `ToolDraft` +- `ToolGenerationJob` +- `ToolValidationRun` +- `ToolPublication` +- `AuditLog` + +O banco operacional continua com entidades como: + +- `User` +- `Order` +- `ReviewSchedule` +- `RentalContract` +- `ConversationTurn` + +## Regras arquiteturais obrigatorias + +1. Nenhuma rota administrativa deve reutilizar `User` do atendimento como identidade autenticada. +2. Nenhuma regra de atendimento deve depender de `StaffAccount` para funcionar. +3. O pipeline de geracao/publicacao de tools deve operar fora do caminho critico do atendimento. +4. Toda ativacao de tool gerada deve ser auditavel e vinculada a um `StaffAccount`. +5. O atendimento continua decidindo execucao com base no modelo; o painel administrativo apenas governa cadastro, validacao e publicacao. + +## Papel inicial de permissao +A primeira versao deve prever ao menos estes papeis: + +- `admin`: gerencia contas internas, aprova e publica tools, altera configuracoes sensiveis +- `staff`: cria drafts, acompanha geracao, revisa resultados e solicita aprovacao +- `viewer`: consulta status e auditoria, sem publicar + +## Estrutura tecnica sugerida + +### Banco administrativo (`Base`) +- `app/db/models/staff_account.py` +- `app/db/models/tool_draft.py` +- `app/db/models/tool_generation_job.py` +- `app/db/models/audit_log.py` + +### Repositorios +- `app/repositories/staff_account_repository.py` +- `app/repositories/tool_draft_repository.py` +- `app/repositories/tool_generation_job_repository.py` + +### Servicos +- `app/services/admin/auth_service.py` +- `app/services/admin/tool_draft_service.py` +- `app/services/admin/tool_generation_service.py` +- `app/services/admin/audit_service.py` + +### API interna +- `app/api/routes/admin_auth.py` +- `app/api/routes/admin_tools.py` +- `app/api/routes/admin_audit.py` + +## Fluxo alvo de alto nivel +1. `StaffAccount` faz login no painel interno. +2. O usuario interno cria um `ToolDraft` com nome, descricao e parametros. +3. Um job isolado gera a implementacao e executa validacoes. +4. O resultado fica disponivel para revisao humana. +5. Um `admin` aprova e publica. +6. A tool publicada passa a integrar o registry ativo sem afetar o dominio de identidade do atendimento. + +## Impacto nas proximas etapas +A partir desta decisao, as proximas implementacoes devem seguir esta ordem: + +1. Criar `StaffAccount` e autenticacao administrativa. +2. Criar autorizacao por papel. +3. Criar entidades de draft/versionamento/validacao. +4. Criar pipeline isolado de geracao. +5. Criar painel e rotas administrativas. + +## Consequencias +### Positivas +- Isola seguranca interna do atendimento ao cliente. +- Facilita auditoria e governanca. +- Evita acoplamento indevido entre canal externo e painel interno. +- Deixa clara a separacao entre operacao e administracao do sistema. + +### Custos +- Introduz novo conjunto de entidades e rotas. +- Exige autenticacao e autorizacao dedicadas. +- Aumenta a complexidade de bootstrap e persistencia do dominio administrativo. + +## Fora do escopo desta ADR +- Escolha definitiva do modelo para geracao de codigo. +- Implementacao do frontend administrativo. +- Definicao detalhada do sandbox de execucao das tools geradas. diff --git a/docs/adr/0002-split-product-and-admin-services.md b/docs/adr/0002-split-product-and-admin-services.md new file mode 100644 index 0000000..1cf4142 --- /dev/null +++ b/docs/adr/0002-split-product-and-admin-services.md @@ -0,0 +1,246 @@ +# ADR 0002 - Separar o runtime de produto do serviço administrativo + +## Status +Accepted + +## Relacao com ADRs anteriores +Esta decisao complementa a ADR 0001. +A ADR 0001 separa identidade de atendimento e identidade administrativa. +A ADR 0002 amplia essa separacao para o nivel de servicos e runtime. + +## Contexto +O sistema atual nasceu como um unico runtime orientado ao atendimento. +Hoje ele concentra no mesmo projeto e no mesmo ciclo operacional: + +- atendimento conversacional +- orquestracao de tools +- integracao com Telegram +- estado conversacional +- regras operacionais de vendas, revisao e locacao +- administracao futura do sistema +- geracao futura de novas tools +- relatorios e configuracoes internas + +A nova frente de evolucao exige um modulo administrativo mais robusto, com: + +- login interno de funcionarios e administradores +- configuracao do sistema +- relatorios de vendas, arrecadacao e operacao +- cadastro, geracao, validacao e publicacao de novas tools +- auditoria de alteracoes e aprovacoes + +Se tudo isso continuar no mesmo runtime do atendimento, teremos aumento de risco em quatro eixos: + +1. Performance + - jobs pesados de geracao e validacao podem concorrer com o atendimento. + +2. Seguranca + - login administrativo, aprovacoes e publicacao de codigo ficariam expostos no mesmo servico do produto. + +3. Operacao + - qualquer falha ou deploy administrativo pode impactar diretamente o atendimento. + +4. Evolucao + - o painel e a automacao interna possuem cadencia, dependencias e necessidades diferentes do runtime conversacional. + +## Decisao +Vamos separar a solucao em dois servicos distintos, inicialmente no mesmo repositorio. + +### 1. Servico de produto +Nome conceitual: `orquestrador-product` + +Responsabilidades: +- atendimento conversacional +- integracao com Telegram e futuros canais de atendimento +- orquestracao de tools em tempo de execucao +- fluxos operacionais de vendas, revisao e locacao +- leitura apenas de tools publicadas e configuracoes ativas + +Esse servico continua sendo o runtime critico do produto. +Ele deve permanecer leve, previsivel e protegido de cargas administrativas. + +### 2. Servico administrativo +Nome conceitual: `orquestrador-admin` + +Responsabilidades: +- autenticacao e autorizacao interna +- painel administrativo +- configuracoes do sistema +- relatorios de vendas, arrecadacao e operacao +- cadastro de drafts de tools +- geracao de implementacoes +- validacao automatica +- aprovacao humana +- publicacao controlada +- auditoria de mudancas + +Esse servico nao participa do hot path do atendimento. +Ele governa o sistema, mas nao executa atendimento em tempo real. + +## Decisao sobre repositorio +Neste primeiro momento, os dois servicos permanecem no mesmo repositorio. + +Motivos: +- menor custo operacional inicial +- versionamento conjunto das fronteiras compartilhadas +- mais facilidade para evoluir contratos internos +- menos atrito no inicio da iniciativa + +No futuro, se a operacao justificar, eles podem ser separados em repositorios diferentes. +Essa separacao nao e obrigatoria agora. + +## Fronteira entre os servicos + +### O que pertence ao servico de produto +- LLM do atendimento +- orquestrador +- registry de tools ativas +- execucao de tools aprovadas +- fluxo de conversa +- integracoes com canais externos de atendimento +- persistencia operacional do usuario final + +### O que pertence ao servico administrativo +- `StaffAccount` +- permissao por papel +- painel interno +- configuracao administrativa +- relatorios e dashboards +- pipeline de geracao de tools +- versionamento de tools +- aprovacao/publicacao +- trilha de auditoria + +## Principio de integracao entre os servicos +A integracao entre `product` e `admin` deve ser preferencialmente assincrona ou orientada a publicacao de estado. +O runtime de produto nao deve depender de uma chamada online ao servico administrativo para responder ao cliente. + +Regra obrigatoria: +- o atendimento deve continuar funcionando mesmo se o servico administrativo estiver indisponivel. + +## Modelo de acoplamento permitido + +### Permitido +- leitura de tools publicadas +- leitura de configuracoes marcadas como ativas +- leitura de versoes aprovadas +- sincronizacao de metadados publicados +- consumo de eventos ou snapshots administrativos + +### Nao permitido no hot path do atendimento +- gerar tool sob demanda durante o atendimento +- validar codigo em tempo real no runtime do produto +- depender de login administrativo para executar atendimento +- bloquear resposta ao usuario aguardando operacao do servico administrativo + +## Estrategia de dados + +### Banco do servico de produto +Responsavel por: +- usuarios de atendimento +- pedidos +- revisoes +- locacoes +- conversas +- estado operacional +- referencias de tools ativas necessarias ao runtime + +### Banco do servico administrativo +Responsavel por: +- contas internas (`StaffAccount`) +- sessoes e credenciais administrativas +- configuracoes do sistema +- relatorios consolidados +- drafts de tools +- jobs de geracao +- execucoes de validacao +- publicacoes +- auditoria + +## Conexao entre dados dos dois servicos +Existem duas estrategias validas para as proximas fases: + +1. Banco administrativo consulta dados consolidados do produto por replicacao, ETL ou views dedicadas para relatorios. +2. Banco administrativo recebe snapshots/eventos do produto para alimentar relatorios e auditoria operacional. + +Decisao inicial recomendada: +- manter o produto como fonte operacional +- usar o servico administrativo para leitura consolidada, auditoria e governanca +- evitar escrita administrativa direta nas tabelas operacionais do atendimento, salvo casos explicitamente versionados e controlados + +## Estrutura tecnica sugerida no monorepo + +### Produto +- `app/` permanece como nucleo do runtime de atendimento +- entrypoints de atendimento e integracoes continuam aqui + +### Administrativo +Criar uma nova arvore dedicada, por exemplo: +- `admin_app/` + - `api/` + - `services/` + - `repositories/` + - `models/` + - `main.py` + +Ou, se quisermos maximizar reaproveitamento de convencao atual: +- `app_admin/` + +A escolha do nome pode ser definida na fase de scaffold. +O importante nesta ADR e a separacao de runtime e responsabilidade. + +## Deploy esperado +No medio prazo, o deploy deve prever dois servicos distintos: + +- `orquestrador-product` +- `orquestrador-admin` + +Cada um com: +- variaveis de ambiente proprias +- processo/servico dedicado +- observabilidade propria +- escala independente + +## Implicacoes para modelo de IA +A geracao de tools e automacao administrativa podem usar um modelo diferente do atendimento. +Essa escolha fica facilitada pela separacao de servicos, pois: +- evita disputa de recurso e custo com o chat principal +- permite tuning de latencia e qualidade por caso de uso +- reduz risco de sobrecarregar o atendimento + +## Regras obrigatorias decorrentes desta ADR +1. O runtime de produto nao executa pipeline de geracao de tools. +2. O servico administrativo nao participa do hot path de resposta ao cliente. +3. Toda tool nova nasce no servico administrativo e so chega ao produto depois de publicada. +4. Relatorios e configuracoes internas pertencem ao servico administrativo. +5. O produto so consome estado publicado e aprovado. +6. Deploys do servico administrativo nao devem exigir redeploy simultaneo do produto, salvo mudanca de contrato compartilhado. + +## Sequencia recomendada de implementacao +1. Formalizar esta arquitetura em documentacao. +2. Criar fundacao do servico administrativo no monorepo. +3. Implementar `StaffAccount`, auth e papeis. +4. Criar area de configuracao e relatorios basicos. +5. Criar entidades de draft/publicacao de tools. +6. Implementar pipeline isolado de geracao e validacao. +7. Integrar publicacao de tools com o runtime de produto. + +## Consequencias +### Positivas +- isola o atendimento das cargas administrativas +- melhora seguranca +- facilita escalabilidade independente +- prepara o sistema para governanca e auditoria reais +- reduz risco operacional no produto + +### Custos +- aumenta a complexidade arquitetural +- exige contratos claros entre servicos +- traz mais trabalho de deploy, observabilidade e configuracao +- exige estrategia de compartilhamento de dados para relatorios + +## Fora do escopo desta ADR +- implementar o scaffold real do segundo servico +- escolher o modelo definitivo de geracao +- definir o formato final de sincronizacao de dados analiticos +- definir a UI final do painel administrativo diff --git a/tests/test_admin_app_bootstrap.py b/tests/test_admin_app_bootstrap.py new file mode 100644 index 0000000..8df6f43 --- /dev/null +++ b/tests/test_admin_app_bootstrap.py @@ -0,0 +1,67 @@ +import unittest + +from fastapi.testclient import TestClient + +from admin_app.app_factory import create_app +from admin_app.core.settings import AdminSettings + + +class AdminAppBootstrapTests(unittest.TestCase): + def test_admin_app_root_endpoint(self): + app = create_app(AdminSettings(admin_environment="staging")) + client = TestClient(app) + + response = client.get("/") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "service": "orquestrador-admin", + "status": "ok", + "message": "Servico administrativo inicializado.", + "environment": "staging", + }, + ) + + def test_admin_app_health_endpoint(self): + app = create_app(AdminSettings(admin_version="1.2.3")) + client = TestClient(app) + + response = client.get("/health") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + {"service": "orquestrador-admin", "status": "ok", "version": "1.2.3"}, + ) + + def test_admin_app_system_info_endpoint(self): + settings = AdminSettings( + admin_app_name="Admin Interno", + admin_environment="development", + admin_version="0.9.0", + admin_api_prefix="/admin", + admin_debug=True, + ) + app = create_app(settings) + client = TestClient(app) + + response = client.get("/admin/system/info") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "service": "orquestrador-admin", + "app_name": "Admin Interno", + "environment": "development", + "version": "0.9.0", + "api_prefix": "/admin", + "debug": True, + }, + ) + + +if __name__ == "__main__": + unittest.main()