"""Registro de herramientas para agentes de IA. Provee un sistema de definición, validación y ejecución de herramientas compatible con los formatos de OpenAI y Anthropic. Incluye herramientas de ejemplo para búsqueda, fecha/hora y cálculo seguro. """ from __future__ import annotations import ast import datetime import logging import operator from dataclasses import dataclass from typing import Any, Callable logger = logging.getLogger("orchestration.tools") # Operadores permitidos para la calculadora segura _ALLOWED_OPERATORS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, ast.Pow: operator.pow, ast.USub: operator.neg, ast.UAdd: operator.pos, } _JSON_TYPE_MAP: dict[str, type | tuple[type, ...]] = { "string": str, "integer": int, "number": (int, float), "boolean": bool, "array": list, "object": dict, } @dataclass class ToolDefinition: """Definición de una herramienta para agentes. Args: name: Nombre único de la herramienta. description: Descripción de lo que hace la herramienta. parameters: Esquema JSON Schema de los parámetros. function: Función que implementa la herramienta. requires_confirmation: Si requiere confirmación del usuario. timeout_seconds: Tiempo máximo de ejecución. """ name: str description: str parameters: dict function: Callable requires_confirmation: bool = False timeout_seconds: float = 30.0 def validate_params(self, params: dict) -> tuple[bool, str]: """Valida parámetros contra el JSON Schema (validación manual). Args: params: Diccionario de parámetros a validar. Returns: Tupla ``(válido, mensaje_error)``. Si es válido, el mensaje es una cadena vacía. """ schema = self.parameters # Verificar campos requeridos required = schema.get("required", []) for field_name in required: if field_name not in params: return False, f"Missing required parameter: '{field_name}'" # Verificar tipos y enums properties = schema.get("properties", {}) for param_name, value in params.items(): if param_name not in properties: continue prop_schema = properties[param_name] # Verificar tipo expected_type_str = prop_schema.get("type") if expected_type_str and expected_type_str in _JSON_TYPE_MAP: expected_type = _JSON_TYPE_MAP[expected_type_str] # bool es subclase de int en Python, tratar como caso especial if expected_type_str == "integer" and isinstance(value, bool): return ( False, f"Parameter '{param_name}' expected type " f"'{expected_type_str}', got 'boolean'", ) if not isinstance(value, expected_type): actual = type(value).__name__ return ( False, f"Parameter '{param_name}' expected type " f"'{expected_type_str}', got '{actual}'", ) # Verificar enum enum_values = prop_schema.get("enum") if enum_values is not None and value not in enum_values: return ( False, f"Parameter '{param_name}' must be one of {enum_values}, " f"got '{value}'", ) return True, "" def execute(self, params: dict) -> str: """Ejecuta la herramienta con los parámetros dados. Args: params: Diccionario de parámetros. Returns: Resultado como cadena, o mensaje de error. """ valid, error_msg = self.validate_params(params) if not valid: return f"Validation error: {error_msg}" try: result = self.function(**params) return str(result) except Exception as exc: return f"Execution error: {type(exc).__name__}: {exc}" class ToolRegistry: """Registro centralizado de herramientas disponibles para agentes. Permite registrar, consultar y ejecutar herramientas, y exportar sus definiciones en formatos compatibles con OpenAI y Anthropic. """ def __init__(self) -> None: self._tools: dict[str, ToolDefinition] = {} def register(self, tool: ToolDefinition) -> None: """Registra una herramienta. Raises: ValueError: Si ya existe una herramienta con el mismo nombre. """ if tool.name in self._tools: raise ValueError(f"Tool '{tool.name}' is already registered") self._tools[tool.name] = tool logger.info("Registered tool: %s", tool.name) def register_function( self, name: str, description: str, parameters: dict, function: Callable, **kwargs: Any, ) -> None: """Atajo para registrar una función como herramienta. Args: name: Nombre de la herramienta. description: Descripción. parameters: JSON Schema de parámetros. function: Función implementadora. **kwargs: Parámetros extra para ``ToolDefinition``. """ tool = ToolDefinition( name=name, description=description, parameters=parameters, function=function, **kwargs, ) self.register(tool) def get(self, name: str) -> ToolDefinition: """Obtiene una herramienta por nombre. Raises: KeyError: Si la herramienta no existe. """ if name not in self._tools: raise KeyError( f"Tool '{name}' not found. " f"Available: {list(self._tools.keys())}" ) return self._tools[name] def remove(self, name: str) -> None: """Elimina una herramienta del registro. Raises: KeyError: Si la herramienta no existe. """ if name not in self._tools: raise KeyError(f"Tool '{name}' not found") del self._tools[name] logger.info("Removed tool: %s", name) def list_tools(self) -> list[str]: """Retorna la lista de nombres de herramientas registradas.""" return list(self._tools.keys()) def to_openai_format(self) -> list[dict]: """Exporta las herramientas en formato OpenAI function calling.""" return [ { "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.parameters, }, } for tool in self._tools.values() ] def to_anthropic_format(self) -> list[dict]: """Exporta las herramientas en formato Anthropic tool use.""" return [ { "name": tool.name, "description": tool.description, "input_schema": tool.parameters, } for tool in self._tools.values() ] def execute_tool(self, name: str, params: dict) -> str: """Ejecuta una herramienta por nombre con los parámetros dados. Args: name: Nombre de la herramienta. params: Diccionario de parámetros. Returns: Resultado como cadena, o mensaje de error. """ try: tool = self.get(name) return tool.execute(params) except Exception as exc: return f"Error: {type(exc).__name__}: {exc}" # --------------------------------------------------------------------------- # Herramientas de ejemplo # --------------------------------------------------------------------------- def search_documents(query: str, top_k: int = 5) -> str: """Busca documentos relevantes (mock). Args: query: Consulta de búsqueda. top_k: Número de resultados a retornar. Returns: Texto con resultados placeholder. """ results = [ f"Document {i + 1}: Result for '{query}' (relevance: {0.9 - i * 0.1:.1f})" for i in range(top_k) ] return "\n".join(results) def get_current_datetime() -> str: """Retorna la fecha y hora actual en formato ISO.""" return datetime.datetime.now().isoformat() def _safe_eval_node(node: ast.AST) -> int | float: """Evalúa un nodo AST aritmético de forma segura.""" if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): return node.value if isinstance(node, ast.BinOp): op_type = type(node.op) if op_type not in _ALLOWED_OPERATORS: raise ValueError(f"Unsupported operator: {op_type.__name__}") left = _safe_eval_node(node.left) right = _safe_eval_node(node.right) return _ALLOWED_OPERATORS[op_type](left, right) if isinstance(node, ast.UnaryOp): op_type = type(node.op) if op_type not in _ALLOWED_OPERATORS: raise ValueError(f"Unsupported operator: {op_type.__name__}") operand = _safe_eval_node(node.operand) return _ALLOWED_OPERATORS[op_type](operand) raise ValueError(f"Unsupported expression node: {type(node).__name__}") def calculate(expression: str) -> str: """Evalúa una expresión aritmética de forma segura usando AST. Solo permite constantes numéricas y operadores aritméticos básicos. Nunca usa ``eval()`` directamente. Args: expression: Expresión aritmética (e.g., ``"2 + 3 * 4"``). Returns: Resultado como cadena. Raises: ValueError: Si la expresión contiene elementos no permitidos. """ tree = ast.parse(expression, mode="eval") result = _safe_eval_node(tree.body) return str(result) if __name__ == "__main__": # Demo de herramientas registry = ToolRegistry() registry.register( ToolDefinition( name="search", description="Search documents", parameters={ "type": "object", "properties": { "query": {"type": "string", "description": "Search query"}, "top_k": {"type": "integer", "description": "Results count"}, }, "required": ["query"], }, function=search_documents, ) ) registry.register( ToolDefinition( name="datetime", description="Get current date and time", parameters={"type": "object", "properties": {}}, function=get_current_datetime, ) ) registry.register( ToolDefinition( name="calculate", description="Evaluate arithmetic expression", parameters={ "type": "object", "properties": { "expression": { "type": "string", "description": "Arithmetic expression", }, }, "required": ["expression"], }, function=calculate, ) ) print("Tools:", registry.list_tools()) print("\nOpenAI format:", registry.to_openai_format()) print("\nSearch result:", registry.execute_tool("search", {"query": "AI agents"})) print("\nDatetime:", registry.execute_tool("datetime", {})) print("\nCalculate:", registry.execute_tool("calculate", {"expression": "2 + 3 * 4"}))