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