import inspect from typing import Callable, Dict, List from fastapi import HTTPException from shared.contracts import GENERATED_TOOL_ENTRYPOINT, GENERATED_TOOLS_PACKAGE from sqlalchemy.orm import Session from app.models.tool_model import ToolDefinition from app.repositories.tool_repository import ToolRepository from app.services.tools.handlers import ( agendar_revisao, avaliar_veiculo_troca, cancelar_agendamento_revisao, cancelar_pedido, editar_data_revisao, listar_agendamentos_revisao, listar_pedidos, consultar_estoque, consultar_frota_aluguel, abrir_locacao_aluguel, registrar_devolucao_aluguel, registrar_pagamento_aluguel, realizar_pedido, validar_cliente_venda, ) HANDLERS: Dict[str, Callable] = { "consultar_estoque": consultar_estoque, "consultar_frota_aluguel": consultar_frota_aluguel, "validar_cliente_venda": validar_cliente_venda, "avaliar_veiculo_troca": avaliar_veiculo_troca, "agendar_revisao": agendar_revisao, "listar_agendamentos_revisao": listar_agendamentos_revisao, "cancelar_agendamento_revisao": cancelar_agendamento_revisao, "editar_data_revisao": editar_data_revisao, "cancelar_pedido": cancelar_pedido, "listar_pedidos": listar_pedidos, "realizar_pedido": realizar_pedido, "abrir_locacao_aluguel": abrir_locacao_aluguel, "registrar_devolucao_aluguel": registrar_devolucao_aluguel, "registrar_pagamento_aluguel": registrar_pagamento_aluguel, } class GeneratedToolCoreBoundaryViolation(RuntimeError): """Raised when a generated tool attempts to reuse or point at core runtime code.""" # Registry em memoria das tools disponiveis para o orquestrador. class ToolRegistry: def __init__(self, db: Session, extra_handlers: Dict[str, Callable] | None = None): """Carrega tools do banco e registra apenas as que possuem handler conhecido.""" self._tools = [] available_handlers = dict(HANDLERS) if extra_handlers: available_handlers.update(extra_handlers) repo = ToolRepository(db) db_tools = repo.get_all() for db_tool in db_tools: handler = available_handlers.get(db_tool.name) if not handler: continue self.register_tool( name=db_tool.name, description=db_tool.description, parameters=db_tool.parameters, handler=handler, ) def register_tool(self, name, description, parameters, handler): """Registra uma tool em memoria para uso pelo orquestrador.""" if self._is_generated_handler(handler): self._ensure_generated_tool_boundary(name=name, handler=handler) self._append_tool_definition( name=name, description=description, parameters=parameters, handler=handler, ) def register_generated_tool(self, name, description, parameters, handler): """Registra uma tool gerada apenas quando ela respeita o pacote isolado do runtime.""" self._ensure_generated_tool_boundary(name=name, handler=handler) self._append_tool_definition( name=name, description=description, parameters=parameters, handler=handler, ) def _append_tool_definition(self, *, name, description, parameters, handler): self._tools.append( ToolDefinition( name=name, description=description, parameters=parameters, handler=handler, ) ) @staticmethod def _is_generated_handler(handler: Callable) -> bool: module_name = str(getattr(handler, "__module__", "") or "").strip() return module_name.startswith(f"{GENERATED_TOOLS_PACKAGE}.") def _ensure_generated_tool_boundary(self, *, name: str, handler: Callable) -> None: normalized_name = str(name or "").strip().lower() if normalized_name in HANDLERS: raise GeneratedToolCoreBoundaryViolation( f"Tool gerada '{normalized_name}' nao pode sobrescrever um handler do catalogo core." ) if any(str(tool.name or "").strip().lower() == normalized_name for tool in self._tools): raise GeneratedToolCoreBoundaryViolation( f"Tool gerada '{normalized_name}' nao pode sobrescrever uma tool ja registrada no runtime." ) module_name = str(getattr(handler, "__module__", "") or "").strip() if not module_name.startswith(f"{GENERATED_TOOLS_PACKAGE}."): raise GeneratedToolCoreBoundaryViolation( f"Tools geradas so podem ser carregadas do pacote isolado '{GENERATED_TOOLS_PACKAGE}.*'." ) handler_name = str(getattr(handler, "__name__", "") or "").strip() if handler_name != GENERATED_TOOL_ENTRYPOINT: raise GeneratedToolCoreBoundaryViolation( f"Tools geradas precisam expor o entrypoint governado '{GENERATED_TOOL_ENTRYPOINT}'." ) def get_tools(self) -> List[ToolDefinition]: """Retorna a lista atual de tools registradas.""" return self._tools async def execute(self, name: str, arguments: dict, user_id: int | None = None): """Executa a tool solicitada pelo modelo com os argumentos extraidos.""" tool = next((t for t in self._tools if t.name == name), None) if not tool: raise HTTPException( status_code=400, detail={ "code": "tool_not_found", "message": f"Tool {name} nao encontrada.", "retryable": False, }, ) call_args = dict(arguments or {}) signature = inspect.signature(tool.handler) if user_id is not None and "user_id" in signature.parameters: call_args["user_id"] = user_id supported_args = { key: value for key, value in call_args.items() if key in signature.parameters } missing_required = [ parameter.name for parameter in signature.parameters.values() if parameter.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY) and parameter.default is inspect._empty and parameter.name not in supported_args ] if missing_required: raise HTTPException( status_code=400, detail={ "code": "invalid_tool_arguments", "message": f"Argumentos obrigatorios ausentes para a tool {name}: {', '.join(missing_required)}.", "retryable": True, "field": missing_required[0], }, ) try: return await tool.handler(**supported_args) except TypeError as exc: raise HTTPException( status_code=400, detail={ "code": "invalid_tool_arguments", "message": f"Argumentos invalidos para a tool {name}.", "retryable": True, }, ) from exc