You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
orquestrador/admin_app/services/tool_management_service.py

491 lines
20 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from datetime import UTC, datetime
from admin_app.core.settings import AdminSettings
from shared.contracts import ServiceName, ToolLifecycleStatus, ToolParameterType
@dataclass(frozen=True)
class BootstrapToolCatalogEntry:
tool_name: str
display_name: str
description: str
domain: str
parameter_count: int
@dataclass(frozen=True)
class ToolIntakeDomainOption:
value: str
label: str
description: str
_BOOTSTRAP_TOOL_CATALOG: tuple[BootstrapToolCatalogEntry, ...] = (
BootstrapToolCatalogEntry(
tool_name="consultar_estoque",
display_name="Consultar estoque",
description="Consulta veiculos disponiveis no estoque comercial.",
domain="vendas",
parameter_count=4,
),
BootstrapToolCatalogEntry(
tool_name="validar_cliente_venda",
display_name="Validar cliente para venda",
description="Avalia elegibilidade de credito para operacoes de venda.",
domain="vendas",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="avaliar_veiculo_troca",
display_name="Avaliar veiculo de troca",
description="Estima o valor de entrada de um veiculo usado.",
domain="vendas",
parameter_count=3,
),
BootstrapToolCatalogEntry(
tool_name="agendar_revisao",
display_name="Agendar revisao",
description="Abre um agendamento de revisao ou manutencao.",
domain="revisao",
parameter_count=6,
),
BootstrapToolCatalogEntry(
tool_name="listar_agendamentos_revisao",
display_name="Listar agendamentos de revisao",
description="Consulta a fila de agendamentos de revisao do cliente.",
domain="revisao",
parameter_count=3,
),
BootstrapToolCatalogEntry(
tool_name="cancelar_agendamento_revisao",
display_name="Cancelar agendamento de revisao",
description="Cancela um agendamento existente por protocolo.",
domain="revisao",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="editar_data_revisao",
display_name="Editar data de revisao",
description="Remarca uma revisao para um novo horario.",
domain="revisao",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="realizar_pedido",
display_name="Realizar pedido",
description="Efetiva um pedido de compra com o veiculo escolhido.",
domain="vendas",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="listar_pedidos",
display_name="Listar pedidos",
description="Consulta pedidos ja abertos pelo cliente.",
domain="vendas",
parameter_count=3,
),
BootstrapToolCatalogEntry(
tool_name="cancelar_pedido",
display_name="Cancelar pedido",
description="Cancela um pedido existente com motivo registrado.",
domain="vendas",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="consultar_frota_aluguel",
display_name="Consultar frota de aluguel",
description="Lista veiculos disponiveis para locacao.",
domain="locacao",
parameter_count=6,
),
BootstrapToolCatalogEntry(
tool_name="abrir_locacao_aluguel",
display_name="Abrir locacao de aluguel",
description="Inicia um contrato de locacao de veiculo.",
domain="locacao",
parameter_count=7,
),
BootstrapToolCatalogEntry(
tool_name="registrar_devolucao_aluguel",
display_name="Registrar devolucao de aluguel",
description="Fecha uma locacao e devolve o veiculo para a frota.",
domain="locacao",
parameter_count=4,
),
BootstrapToolCatalogEntry(
tool_name="registrar_pagamento_aluguel",
display_name="Registrar pagamento de aluguel",
description="Registra comprovantes e pagamentos de contratos de locacao.",
domain="locacao",
parameter_count=7,
),
BootstrapToolCatalogEntry(
tool_name="limpar_contexto_conversa",
display_name="Limpar contexto de conversa",
description="Reinicia o contexto operacional atual do atendimento.",
domain="orquestracao",
parameter_count=1,
),
BootstrapToolCatalogEntry(
tool_name="continuar_proximo_pedido",
display_name="Continuar proximo pedido",
description="Retoma o proximo pedido pendente do fluxo atual.",
domain="orquestracao",
parameter_count=0,
),
BootstrapToolCatalogEntry(
tool_name="descartar_pedidos_pendentes",
display_name="Descartar pedidos pendentes",
description="Descarta apenas a fila pendente de pedidos do contexto.",
domain="orquestracao",
parameter_count=1,
),
BootstrapToolCatalogEntry(
tool_name="cancelar_fluxo_atual",
display_name="Cancelar fluxo atual",
description="Interrompe o fluxo corrente sem apagar todo o contexto.",
domain="orquestracao",
parameter_count=1,
),
)
_INTAKE_DOMAIN_OPTIONS: tuple[ToolIntakeDomainOption, ...] = (
ToolIntakeDomainOption(
value="vendas",
label="Vendas",
description="Ferramentas para estoque, negociacao, pedido e conversao comercial.",
),
ToolIntakeDomainOption(
value="revisao",
label="Revisao",
description="Ferramentas para agendamento, remarcacao e operacao da oficina.",
),
ToolIntakeDomainOption(
value="locacao",
label="Locacao",
description="Ferramentas para frota, contratos, devolucao e arrecadacao de aluguel.",
),
ToolIntakeDomainOption(
value="orquestracao",
label="Orquestracao",
description="Ferramentas internas para fluxo conversacional, contexto e decisao do bot.",
),
)
_LIFECYCLE_DESCRIPTIONS = {
ToolLifecycleStatus.DRAFT: "Estado inicial de uma tool ainda em definicao.",
ToolLifecycleStatus.GENERATED: "Implementacao gerada e pronta para analise tecnica.",
ToolLifecycleStatus.VALIDATED: "Tool validada automaticamente com verificacoes basicas.",
ToolLifecycleStatus.APPROVED: "Versao revisada e aprovada para publicacao controlada.",
ToolLifecycleStatus.ACTIVE: "Tool publicada e apta a abastecer o runtime de produto.",
ToolLifecycleStatus.FAILED: "Falha registrada na geracao, validacao ou ativacao.",
ToolLifecycleStatus.ARCHIVED: "Versao retirada de circulacao e mantida apenas para historico.",
}
_PARAMETER_TYPE_DESCRIPTIONS = {
ToolParameterType.STRING: "Texto livre, codigos e identificadores.",
ToolParameterType.INTEGER: "Valores inteiros para limites, anos e contagens.",
ToolParameterType.NUMBER: "Valores numericos decimais, como preco e diaria.",
ToolParameterType.BOOLEAN: "Marcadores verdadeiro ou falso para decisoes operacionais.",
ToolParameterType.OBJECT: "Estruturas compostas para payloads complexos.",
ToolParameterType.ARRAY: "Colecoes ordenadas de valores.",
}
_TOOL_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
_PARAMETER_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
class ToolManagementService:
def __init__(self, settings: AdminSettings):
self.settings = settings
def build_overview_payload(self) -> dict:
catalog = self.list_publication_catalog()
return {
"mode": "bootstrap_catalog",
"metrics": [
{
"key": "active_catalog",
"label": "Tools mapeadas",
"value": str(len(catalog)),
"description": "Catalogo bootstrap refletindo a base de tools conhecida no monorepo.",
},
{
"key": "lifecycle_stages",
"label": "Etapas de lifecycle",
"value": str(len(ToolLifecycleStatus)),
"description": "Estados compartilhados entre governanca administrativa e publicacao.",
},
{
"key": "parameter_types",
"label": "Tipos de parametro",
"value": str(len(ToolParameterType)),
"description": "Tipos aceitos pelo contrato inicial de publicacao de tools.",
},
{
"key": "draft_persistence",
"label": "Persistencia de drafts",
"value": "pendente",
"description": "A fase atual entrega uma tela real de cadastro com validacao; a persistencia entra na fase seguinte.",
},
],
"workflow": self.build_lifecycle_payload(),
"next_steps": [
"Criar entidades administrativas para ToolDraft, ToolValidationRun e ToolPublication.",
"Persistir o pre-cadastro validado da nova tela em armazenamento proprio do admin.",
"Abrir filas de revisao, aprovacao e ativacao com auditoria ponta a ponta.",
],
}
def build_contracts_payload(self) -> dict:
return {
"publication_source_service": ServiceName.ADMIN,
"publication_target_service": ServiceName.PRODUCT,
"lifecycle_statuses": self.build_lifecycle_payload(),
"parameter_types": [
{
"code": parameter_type,
"label": parameter_type.value.upper(),
"description": _PARAMETER_TYPE_DESCRIPTIONS[parameter_type],
}
for parameter_type in ToolParameterType
],
"publication_fields": [
"source_service",
"target_service",
"publication_id",
"published_tool",
"emitted_at",
],
"published_tool_fields": [
"tool_name",
"display_name",
"description",
"version",
"status",
"parameters",
"implementation_module",
"implementation_callable",
"checksum",
"published_at",
"published_by",
],
}
def build_draft_form_payload(self) -> dict:
return {
"mode": "validated_preview",
"domain_options": [
{
"value": option.value,
"label": option.label,
"description": option.description,
}
for option in _INTAKE_DOMAIN_OPTIONS
],
"parameter_types": [
{
"code": parameter_type,
"label": parameter_type.value.upper(),
"description": _PARAMETER_TYPE_DESCRIPTIONS[parameter_type],
}
for parameter_type in ToolParameterType
],
"naming_rules": [
"tool_name deve usar snake_case minusculo, sem espacos, com 3 a 64 caracteres.",
"display_name deve explicar claramente a acao operacional que o bot vai executar.",
"Cada parametro precisa de nome, tipo, descricao e marcador de obrigatoriedade.",
],
"submission_notes": [
"O colaborador pode preencher e validar o pre-cadastro da tool no painel.",
"Toda tool nova segue para revisao e aprovacao de um diretor antes de qualquer publicacao.",
"Nesta fase a tela valida o cadastro e monta o preview, enquanto a persistencia definitiva entra na fase 5.",
],
"approval_notes": [
"Diretor revisa objetivo, parametros e aderencia ao contrato compartilhado.",
"A publicacao para o runtime de produto so pode acontecer apos aprovacao humana.",
"Campos livres e payloads complexos exigem criterio maior na etapa de revisao.",
],
}
def build_drafts_payload(self) -> dict:
return {
"storage_status": "pending_persistence",
"message": (
"A nova tela de cadastro ja valida o pre-cadastro da tool no painel, mas a persistencia de ToolDraft ainda sera criada nas proximas etapas."
),
"drafts": [],
"supported_statuses": [ToolLifecycleStatus.DRAFT],
}
def build_review_queue_payload(self) -> dict:
return {
"queue_mode": "bootstrap_empty_state",
"message": (
"A fila de revisao ainda opera em estado vazio ate a criacao das entidades de geracao e validacao."
),
"items": [],
"supported_statuses": [
ToolLifecycleStatus.GENERATED,
ToolLifecycleStatus.VALIDATED,
ToolLifecycleStatus.APPROVED,
ToolLifecycleStatus.FAILED,
],
}
def build_publications_payload(self) -> dict:
return {
"source": "bootstrap_catalog",
"target_service": ServiceName.PRODUCT,
"publications": self.list_publication_catalog(),
}
def preview_draft_submission(self, payload: dict, *, owner_name: str | None = None) -> dict:
normalized = self._normalize_draft_payload(payload)
warnings = self._build_intake_warnings(normalized)
required_parameter_count = sum(1 for parameter in normalized["parameters"] if parameter["required"])
summary = (
f"{normalized['display_name']} pronta para seguir como draft com {len(normalized['parameters'])} parametro(s) e revisao obrigatoria de diretor."
)
return {
"storage_status": "validated_preview",
"message": "Pre-cadastro validado no painel. A persistencia definitiva entra na fase de governanca de tools.",
"draft_preview": {
"draft_id": f"preview::{normalized['tool_name']}",
"tool_name": normalized["tool_name"],
"display_name": normalized["display_name"],
"domain": normalized["domain"],
"status": ToolLifecycleStatus.DRAFT,
"summary": summary,
"business_goal": normalized["business_goal"],
"parameter_count": len(normalized["parameters"]),
"required_parameter_count": required_parameter_count,
"requires_director_approval": True,
"owner_name": owner_name,
"parameters": normalized["parameters"],
},
"warnings": warnings,
"next_steps": [
"Persistir o draft administrativo em armazenamento proprio do admin na fase 5.",
"Encaminhar a tool para revisao e aprovacao de um diretor.",
"Executar pipeline de geracao, validacao e publicacao antes da ativacao no produto.",
],
}
def build_lifecycle_payload(self) -> list[dict]:
return [
{
"code": status,
"label": status.value.replace("_", " ").title(),
"description": _LIFECYCLE_DESCRIPTIONS[status],
}
for status in ToolLifecycleStatus
]
def list_publication_catalog(self) -> list[dict]:
published_at = datetime.now(UTC)
return [
{
"publication_id": f"bootstrap::{entry.tool_name}::v1",
"tool_name": entry.tool_name,
"display_name": entry.display_name,
"description": entry.description,
"domain": entry.domain,
"version": 1,
"status": ToolLifecycleStatus.ACTIVE,
"parameter_count": entry.parameter_count,
"implementation_module": "app.services.tools.handlers",
"implementation_callable": entry.tool_name,
"published_by": "bootstrap_catalog",
"published_at": published_at,
}
for entry in _BOOTSTRAP_TOOL_CATALOG
]
def _normalize_draft_payload(self, payload: dict) -> dict:
tool_name = str(payload.get("tool_name") or "").strip().lower()
if not _TOOL_NAME_PATTERN.fullmatch(tool_name):
raise ValueError("tool_name deve usar snake_case minusculo com 3 a 64 caracteres.")
display_name = str(payload.get("display_name") or "").strip()
if len(display_name) < 4:
raise ValueError("display_name precisa ter pelo menos 4 caracteres.")
domain = str(payload.get("domain") or "").strip().lower()
valid_domains = {option.value for option in _INTAKE_DOMAIN_OPTIONS}
if domain not in valid_domains:
raise ValueError("Selecione um dominio valido para a nova tool.")
description = str(payload.get("description") or "").strip()
if len(description) < 16:
raise ValueError("A descricao precisa ter pelo menos 16 caracteres para contextualizar a tool.")
business_goal = str(payload.get("business_goal") or "").strip()
if len(business_goal) < 12:
raise ValueError("Explique o objetivo operacional da tool com pelo menos 12 caracteres.")
raw_parameters = payload.get("parameters") or []
if not isinstance(raw_parameters, list):
raise ValueError("Os parametros enviados para a tool sao invalidos.")
seen_parameter_names: set[str] = set()
parameters: list[dict] = []
for raw_parameter in raw_parameters:
name = str((raw_parameter or {}).get("name") or "").strip().lower()
if not name:
continue
if not _PARAMETER_NAME_PATTERN.fullmatch(name):
raise ValueError("Cada parametro deve usar snake_case minusculo com pelo menos 2 caracteres.")
if name in seen_parameter_names:
raise ValueError("Nao e permitido repetir nomes de parametro na mesma tool.")
seen_parameter_names.add(name)
raw_parameter_type = (raw_parameter or {}).get("parameter_type") or ""
parameter_type = (
raw_parameter_type
if isinstance(raw_parameter_type, ToolParameterType)
else ToolParameterType(str(raw_parameter_type).strip().lower())
)
parameter_description = str((raw_parameter or {}).get("description") or "").strip()
if len(parameter_description) < 8:
raise ValueError("Cada parametro precisa de uma descricao com pelo menos 8 caracteres.")
parameters.append(
{
"name": name,
"parameter_type": parameter_type,
"description": parameter_description,
"required": bool((raw_parameter or {}).get("required", True)),
}
)
if len(parameters) > 10:
raise ValueError("A fase inicial do painel aceita no maximo 10 parametros por tool.")
return {
"tool_name": tool_name,
"display_name": display_name,
"domain": domain,
"description": description,
"business_goal": business_goal,
"parameters": parameters,
}
def _build_intake_warnings(self, payload: dict) -> list[str]:
warnings: list[str] = []
parameters = payload["parameters"]
if not parameters:
warnings.append("A tool foi cadastrada sem parametros. Confirme se a acao realmente nao exige entrada contextual.")
if len(parameters) >= 6:
warnings.append("A quantidade de parametros ja pede uma revisao mais cuidadosa antes da aprovacao de diretor.")
if any(parameter["parameter_type"] in {ToolParameterType.OBJECT, ToolParameterType.ARRAY} for parameter in parameters):
warnings.append("Parametros compostos exigem atencao extra na revisao porque podem esconder payloads mais sensiveis.")
if payload["domain"] == "orquestracao":
warnings.append("Tools de orquestracao precisam confirmar claramente como afetam o fluxo do bot antes da ativacao.")
return warnings