🏗️ feat(admin): criar scaffold inicial do orquestrador-admin

feat/self-evolving-tools-foundation
parent 498836fd38
commit 17583236a6

@ -0,0 +1 @@
"""Runtime administrativo do orquestrador."""

@ -0,0 +1,3 @@
from admin_app.main import app
__all__ = ["app"]

@ -0,0 +1 @@
"""Camada HTTP do servico administrativo."""

@ -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()

@ -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)

@ -0,0 +1 @@
"""Rotas administrativas do servico interno."""

@ -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()

@ -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

@ -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

@ -0,0 +1 @@
"""Configuracoes centrais do servico administrativo."""

@ -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()

@ -0,0 +1 @@
"""Persistencia do servico administrativo."""

@ -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()

@ -0,0 +1,3 @@
from admin_app.db.models.base import AdminTimestampedModel
__all__ = ["AdminTimestampedModel"]

@ -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(),
)

@ -0,0 +1,3 @@
from admin_app.app_factory import create_app
app = create_app()

@ -0,0 +1,3 @@
from admin_app.repositories.base_repository import BaseRepository
__all__ = ["BaseRepository"]

@ -0,0 +1,6 @@
from sqlalchemy.orm import Session
class BaseRepository:
def __init__(self, db: Session):
self.db = db

@ -0,0 +1,3 @@
from admin_app.services.system_service import SystemService
__all__ = ["SystemService"]

@ -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,
}

@ -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.

@ -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

@ -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()
Loading…
Cancel
Save