from __future__ import annotations import re from dataclasses import dataclass from datetime import UTC, datetime from admin_app.core.settings import AdminSettings from admin_app.db.models import ToolDraft, ToolMetadata, ToolVersion from admin_app.db.models.tool_artifact import ( ToolArtifactKind, ToolArtifactStage, ToolArtifactStatus, ) from admin_app.repositories.tool_artifact_repository import ToolArtifactRepository from admin_app.repositories.tool_draft_repository import ToolDraftRepository from admin_app.repositories.tool_metadata_repository import ToolMetadataRepository from admin_app.repositories.tool_version_repository import ToolVersionRepository from shared.contracts import ( GENERATED_TOOL_ENTRYPOINT, GENERATED_TOOLS_PACKAGE, ServiceName, TOOL_LIFECYCLE_STAGES, ToolLifecycleStatus, ToolParameterType, build_generated_tool_module_name, build_generated_tool_module_path, ) @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.", ), ) _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}$") _RESERVED_CORE_TOOL_NAMES = frozenset(entry.tool_name for entry in _BOOTSTRAP_TOOL_CATALOG) class ToolManagementService: def __init__( self, settings: AdminSettings, draft_repository: ToolDraftRepository | None = None, version_repository: ToolVersionRepository | None = None, metadata_repository: ToolMetadataRepository | None = None, artifact_repository: ToolArtifactRepository | None = None, ): self.settings = settings self.draft_repository = draft_repository self.version_repository = version_repository self.metadata_repository = metadata_repository self.artifact_repository = artifact_repository def build_overview_payload(self) -> dict: catalog_payload = self.build_publications_payload() catalog = catalog_payload["publications"] persisted_draft_count = len(self.draft_repository.list_drafts()) if self.draft_repository else 0 persisted_version_count = 0 if self.version_repository is not None: persisted_version_count = len(self.version_repository.list_versions()) elif self.draft_repository is not None: persisted_version_count = sum(draft.version_count for draft in self.draft_repository.list_drafts()) persisted_metadata_count = len(self.metadata_repository.list_metadata()) if self.metadata_repository else 0 persisted_artifact_count = len(self.artifact_repository.list_artifacts()) if self.artifact_repository else 0 return { "mode": "admin_tool_draft_governance", "metrics": [ { "key": "active_catalog", "label": "Tools mapeadas", "value": str(len(catalog)), "description": "Catalogo governado persistido quando disponivel, com fallback bootstrap enquanto o admin ainda nao tiver metadados proprios.", }, { "key": "lifecycle_stages", "label": "Etapas de lifecycle", "value": str(len(TOOL_LIFECYCLE_STAGES)), "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": "persisted_drafts", "label": "Drafts persistidos", "value": str(persisted_draft_count), "description": "Pre-cadastros administrativos ja gravados no armazenamento proprio do admin.", }, { "key": "persisted_versions", "label": "Versoes administrativas", "value": str(persisted_version_count), "description": "Historico versionado das iteracoes de cada tool governada pelo admin.", }, { "key": "persisted_metadata", "label": "Metadados persistidos", "value": str(persisted_metadata_count), "description": "Snapshots canonicos por versao com nome, descricao, parametros, status e autor da tool.", }, { "key": "persisted_artifacts", "label": "Artefatos auditaveis", "value": str(persisted_artifact_count), "description": "Manifestos de geracao e relatorios de validacao gravados por versao para trilha administrativa.", }, ], "workflow": self.build_lifecycle_payload(), "next_steps": [ "Persistir artefatos de geracao e validacao por versao sem perder o historico administrativo.", "Abrir filas de revisao, aprovacao e ativacao com auditoria ponta a ponta.", "Conectar publicacoes versionadas ao runtime de produto com rollback controlado.", ], } 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.", "tool_name nao pode reutilizar nomes reservados pelo catalogo core ja publicado.", "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, validar e persistir o draft da tool no painel.", "Toda tool nova segue para revisao e aprovacao de um diretor antes de qualquer publicacao.", "Reenvios da mesma tool reaproveitam o draft raiz e geram uma nova versao administrativa.", ], "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: if self.draft_repository is None: 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 nao foi conectada neste runtime." ), "drafts": [], "supported_statuses": [ToolLifecycleStatus.DRAFT], } drafts = self.draft_repository.list_drafts(statuses=(ToolLifecycleStatus.DRAFT,)) message = ( "Nenhum draft administrativo salvo ainda." if not drafts else f"{len(drafts)} draft(s) administrativo(s) salvo(s) no admin com historico versionado." ) return { "storage_status": "admin_database", "message": message, "drafts": [self._serialize_draft_summary(draft) for draft in 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 conectadas as versoes persistidas de cada draft." ), "items": [], "supported_statuses": [ ToolLifecycleStatus.GENERATED, ToolLifecycleStatus.VALIDATED, ToolLifecycleStatus.APPROVED, ToolLifecycleStatus.FAILED, ], } def build_publications_payload(self) -> dict: metadata_entries = self._list_latest_metadata_entries() if metadata_entries: return { "source": "admin_metadata_catalog", "target_service": ServiceName.PRODUCT, "publications": [ self._serialize_metadata_publication(metadata) for metadata in metadata_entries ], } return { "source": "bootstrap_catalog", "target_service": ServiceName.PRODUCT, "publications": self.list_publication_catalog(), } def create_draft_submission( self, payload: dict, *, owner_staff_account_id: int | None = None, 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 = self._build_draft_summary(normalized) stored_parameters = self._serialize_parameters_for_storage(normalized["parameters"]) if self.draft_repository is None: version_number = 1 version_count = 1 version_id = self._build_preview_version_id(normalized["tool_name"], version_number) 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']}", "version_id": version_id, "tool_name": normalized["tool_name"], "display_name": normalized["display_name"], "domain": normalized["domain"], "status": ToolLifecycleStatus.DRAFT, "summary": summary, "business_goal": normalized["business_goal"], "version_number": version_number, "version_count": version_count, "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.", ], } if owner_staff_account_id is None: raise ValueError("owner_staff_account_id e obrigatorio para persistir o draft.") existing_draft = self.draft_repository.get_by_tool_name(normalized["tool_name"]) next_version_number = self._resolve_next_version_number(normalized["tool_name"], existing_draft) next_version_count = next_version_number if existing_draft is None else max(existing_draft.version_count + 1, next_version_number) if existing_draft is None: draft = self.draft_repository.create( tool_name=normalized["tool_name"], display_name=normalized["display_name"], domain=normalized["domain"], description=normalized["description"], business_goal=normalized["business_goal"], summary=summary, parameters_json=stored_parameters, required_parameter_count=required_parameter_count, current_version_number=next_version_number, version_count=next_version_count, owner_staff_account_id=owner_staff_account_id, owner_display_name=owner_name or "Autor administrativo", requires_director_approval=True, ) else: draft = self.draft_repository.update_submission( existing_draft, display_name=normalized["display_name"], domain=normalized["domain"], description=normalized["description"], business_goal=normalized["business_goal"], summary=summary, parameters_json=stored_parameters, required_parameter_count=required_parameter_count, current_version_number=next_version_number, version_count=next_version_count, owner_staff_account_id=owner_staff_account_id, owner_display_name=owner_name or "Autor administrativo", requires_director_approval=True, ) version = None if self.version_repository is not None: version = self.version_repository.create( draft_id=draft.id, tool_name=draft.tool_name, version_number=next_version_number, summary=summary, description=normalized["description"], business_goal=normalized["business_goal"], parameters_json=stored_parameters, required_parameter_count=required_parameter_count, owner_staff_account_id=owner_staff_account_id, owner_display_name=owner_name or "Autor administrativo", status=ToolLifecycleStatus.DRAFT, requires_director_approval=True, ) if version is not None and self.metadata_repository is not None: self.metadata_repository.upsert_version_metadata( draft_id=draft.id, tool_version_id=version.id, tool_name=draft.tool_name, display_name=draft.display_name, domain=draft.domain, description=draft.description, parameters_json=stored_parameters, version_number=version.version_number, status=version.status, author_staff_account_id=version.owner_staff_account_id, author_display_name=version.owner_display_name, ) if version is not None and self.artifact_repository is not None: self._persist_initial_version_artifacts( draft=draft, version=version, summary=summary, warnings=warnings, stored_parameters=stored_parameters, required_parameter_count=required_parameter_count, owner_staff_account_id=owner_staff_account_id, owner_name=owner_name or "Autor administrativo", ) return { "storage_status": "admin_database", "message": "Draft administrativo persistido com sucesso em fluxo versionado.", "draft_preview": self._serialize_draft_preview(draft, version), "warnings": warnings, "next_steps": [ f"Encaminhar a versao v{draft.current_version_number} para revisao e aprovacao de um diretor.", "Conectar a versao persistida ao pipeline de geracao e validacao automatica da tool.", "Persistir artefatos e publicacoes associados a cada versao governada.", ], } 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 = self._build_draft_summary(normalized) existing_draft = None if self.draft_repository is not None: existing_draft = self.draft_repository.get_by_tool_name(normalized["tool_name"]) version_number = self._resolve_next_version_number(normalized["tool_name"], existing_draft) version_count = version_number if existing_draft is None else max(existing_draft.version_count + 1, version_number) return { "storage_status": "validated_preview", "message": "Pre-cadastro validado no painel com numeracao de versao reservada para a tool.", "draft_preview": { "draft_id": existing_draft.draft_id if existing_draft is not None else f"preview::{normalized['tool_name']}", "version_id": self._build_preview_version_id(normalized["tool_name"], version_number), "tool_name": normalized["tool_name"], "display_name": normalized["display_name"], "domain": normalized["domain"], "status": ToolLifecycleStatus.DRAFT, "summary": summary, "business_goal": normalized["business_goal"], "version_number": version_number, "version_count": version_count, "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 a nova versao administrativa para consolidar o historico da tool.", "Encaminhar a versao 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": stage.code, "label": stage.label, "description": stage.description, "order": stage.order, "terminal": stage.terminal, } for stage in TOOL_LIFECYCLE_STAGES ] 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 _persist_initial_version_artifacts( self, *, draft: ToolDraft, version: ToolVersion, summary: str, warnings: list[str], stored_parameters: list[dict], required_parameter_count: int, owner_staff_account_id: int, owner_name: str, ) -> None: if self.artifact_repository is None: return generation_payload = self._build_generation_artifact_payload( draft=draft, version=version, summary=summary, stored_parameters=stored_parameters, ) validation_payload = self._build_validation_artifact_payload( draft=draft, version=version, warnings=warnings, stored_parameters=stored_parameters, required_parameter_count=required_parameter_count, ) self.artifact_repository.upsert_version_artifact( draft_id=draft.id, tool_version_id=version.id, tool_name=draft.tool_name, version_number=version.version_number, artifact_stage=ToolArtifactStage.GENERATION, artifact_kind=ToolArtifactKind.GENERATION_REQUEST, artifact_status=ToolArtifactStatus.PENDING, summary="Manifesto inicial de geracao persistido para auditoria da versao.", payload_json=generation_payload, author_staff_account_id=owner_staff_account_id, author_display_name=owner_name, ) self.artifact_repository.upsert_version_artifact( draft_id=draft.id, tool_version_id=version.id, tool_name=draft.tool_name, version_number=version.version_number, artifact_stage=ToolArtifactStage.VALIDATION, artifact_kind=ToolArtifactKind.VALIDATION_REPORT, artifact_status=ToolArtifactStatus.SUCCEEDED, summary="Relatorio de validacao do pre-cadastro persistido para auditoria da versao.", payload_json=validation_payload, author_staff_account_id=owner_staff_account_id, author_display_name=owner_name, ) @staticmethod def _build_generation_artifact_payload( *, draft: ToolDraft, version: ToolVersion, summary: str, stored_parameters: list[dict], ) -> dict: return { "source": "admin_draft_intake", "tool_name": draft.tool_name, "display_name": draft.display_name, "domain": draft.domain, "version_number": version.version_number, "draft_id": draft.draft_id, "version_id": version.version_id, "business_goal": draft.business_goal, "description": draft.description, "summary": summary, "parameters": list(stored_parameters), "requires_director_approval": draft.requires_director_approval, "target_package": GENERATED_TOOLS_PACKAGE, "target_module": build_generated_tool_module_name(draft.tool_name), "target_file_path": build_generated_tool_module_path(draft.tool_name), "target_callable": GENERATED_TOOL_ENTRYPOINT, "reserved_lifecycle_target": ToolLifecycleStatus.GENERATED.value, } @staticmethod def _build_validation_artifact_payload( *, draft: ToolDraft, version: ToolVersion, warnings: list[str], stored_parameters: list[dict], required_parameter_count: int, ) -> dict: return { "source": "admin_draft_intake", "tool_name": draft.tool_name, "version_number": version.version_number, "draft_id": draft.draft_id, "version_id": version.version_id, "validation_status": "passed", "warnings": list(warnings), "parameter_count": len(stored_parameters), "required_parameter_count": required_parameter_count, "checked_rules": [ "tool_name_snake_case", "display_name_min_length", "domain_catalog", "description_min_length", "business_goal_min_length", "parameter_contracts", ], } def _list_latest_metadata_entries(self) -> list[ToolMetadata]: if self.metadata_repository is None: return [] latest_by_tool_name: dict[str, ToolMetadata] = {} for metadata in self.metadata_repository.list_metadata(): normalized_tool_name = str(metadata.tool_name or "").strip().lower() if normalized_tool_name in latest_by_tool_name: continue latest_by_tool_name[normalized_tool_name] = metadata return list(latest_by_tool_name.values()) def _serialize_metadata_publication(self, metadata: ToolMetadata) -> dict: parameters = self._serialize_parameters_for_response(metadata.parameters_json) return { "publication_id": metadata.metadata_id, "tool_name": metadata.tool_name, "display_name": metadata.display_name, "description": metadata.description, "domain": metadata.domain, "version": metadata.version_number, "status": metadata.status, "parameter_count": len(parameters), "parameters": parameters, "author_name": metadata.author_display_name, "implementation_module": build_generated_tool_module_name(metadata.tool_name), "implementation_callable": GENERATED_TOOL_ENTRYPOINT, "published_by": metadata.author_display_name, "published_at": metadata.updated_at or metadata.created_at, } def _serialize_draft_summary(self, draft: ToolDraft) -> dict: return { "draft_id": draft.draft_id, "tool_name": draft.tool_name, "display_name": draft.display_name, "status": draft.status, "summary": draft.summary, "current_version_number": draft.current_version_number, "version_count": draft.version_count, "owner_name": draft.owner_display_name, "updated_at": draft.updated_at, } def _serialize_draft_preview( self, draft: ToolDraft, version: ToolVersion | None = None, ) -> dict: parameters = self._serialize_parameters_for_response(draft.parameters_json) version_id = version.version_id if version is not None else self._build_preview_version_id( draft.tool_name, draft.current_version_number, ) version_number = version.version_number if version is not None else draft.current_version_number return { "draft_id": draft.draft_id, "version_id": version_id, "tool_name": draft.tool_name, "display_name": draft.display_name, "domain": draft.domain, "status": draft.status, "summary": draft.summary, "business_goal": draft.business_goal, "version_number": version_number, "version_count": draft.version_count, "parameter_count": len(parameters), "required_parameter_count": draft.required_parameter_count, "requires_director_approval": draft.requires_director_approval, "owner_name": draft.owner_display_name, "parameters": parameters, } @staticmethod def _serialize_parameters_for_storage(parameters: list[dict]) -> list[dict]: return [ { "name": parameter["name"], "parameter_type": parameter["parameter_type"].value, "description": parameter["description"], "required": parameter["required"], } for parameter in parameters ] @staticmethod def _serialize_parameters_for_response(parameters_json: list[dict] | None) -> list[dict]: return [ { "name": str((parameter or {}).get("name") or "").strip().lower(), "parameter_type": ToolParameterType(str((parameter or {}).get("parameter_type") or "string").strip().lower()), "description": str((parameter or {}).get("description") or "").strip(), "required": bool((parameter or {}).get("required", True)), } for parameter in (parameters_json or []) ] @staticmethod def _build_draft_summary(payload: dict) -> str: return ( f"{payload['display_name']} pronta para seguir como draft com {len(payload['parameters'])} parametro(s) e revisao obrigatoria de diretor." ) @staticmethod def _build_preview_version_id(tool_name: str, version_number: int) -> str: return f"tool_version::{str(tool_name or '').strip().lower()}::v{int(version_number)}" def _resolve_next_version_number( self, tool_name: str, existing_draft: ToolDraft | None, ) -> int: repository_version = ( self.version_repository.get_next_version_number(tool_name) if self.version_repository is not None else 1 ) if existing_draft is None: return repository_version return max(repository_version, existing_draft.current_version_number + 1) 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.") if tool_name in _RESERVED_CORE_TOOL_NAMES: raise ValueError( "tool_name reservado pelo catalogo core do sistema. Gere uma nova tool sem sobrescrever uma capability interna." ) 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