"""Custom tool execution for user-defined HTTP API tools.""" import re from typing import Any, Dict, Optional import httpx from loguru import logger from api.db import db_client from api.utils.credential_auth import build_auth_header # Map tool parameter types to JSON schema types TYPE_MAP = { "string": "string", "number": "number", "boolean": "boolean", } def tool_to_function_schema(tool: Any) -> Dict[str, Any]: """Convert a ToolModel to an LLM function schema. Args: tool: ToolModel instance with name, description, and definition Returns: Function schema dict compatible with OpenAI/Anthropic function calling """ definition = tool.definition or {} config = definition.get("config", {}) parameters = config.get("parameters", []) or [] # Build properties and required list from parameters properties = {} required = [] for param in parameters: param_name = param.get("name", "") param_type = param.get("type", "string") param_desc = param.get("description", "") param_required = param.get("required", True) if not param_name: continue properties[param_name] = { "type": TYPE_MAP.get(param_type, "string"), "description": param_desc, } if param_required: required.append(param_name) # Sanitize tool name for function name (lowercase, underscores only) function_name = re.sub(r"[^a-z0-9_]", "_", tool.name.lower()) # Remove consecutive underscores and trim function_name = re.sub(r"_+", "_", function_name).strip("_") return { "type": "function", "function": { "name": function_name, "description": tool.description or f"Execute {tool.name} tool", "parameters": { "type": "object", "properties": properties, "required": required, }, }, "_tool_uuid": tool.tool_uuid, } async def execute_http_tool( tool: Any, arguments: Dict[str, Any], call_context_vars: Optional[Dict[str, Any]] = None, organization_id: Optional[int] = None, ) -> Dict[str, Any]: """Execute an HTTP API tool. Args: tool: ToolModel instance arguments: Arguments passed by the LLM (parameter name -> value) call_context_vars: Additional context variables from the call (unused for now) organization_id: Organization ID for credential lookup Returns: Result dict with response data or error """ definition = tool.definition or {} config = definition.get("config", {}) # Get HTTP method and URL method = config.get("method", "POST").upper() url = config.get("url", "") # Get headers from config headers = dict(config.get("headers", {}) or {}) # Add auth header if credential is configured credential_uuid = config.get("credential_uuid") if credential_uuid and organization_id: try: credential = await db_client.get_credential_by_uuid( credential_uuid, organization_id ) if credential: auth_header = build_auth_header(credential) headers.update(auth_header) logger.debug(f"Applied credential '{credential.name}' to tool request") else: logger.warning( f"Credential {credential_uuid} not found for tool '{tool.name}'" ) except Exception as e: logger.error(f"Failed to fetch credential for tool '{tool.name}': {e}") # Get timeout timeout_ms = config.get("timeout_ms", 5000) timeout_seconds = timeout_ms / 1000 # Build request: JSON body for POST/PUT/PATCH, query params for GET/DELETE body = None params = None if method in ("POST", "PUT", "PATCH"): body = arguments elif method in ("GET", "DELETE") and arguments: params = arguments logger.info( f"Executing custom tool '{tool.name}' ({tool.tool_uuid}): {method} {url}" ) logger.debug(f"Request body: {body}, params: {params}") try: async with httpx.AsyncClient(timeout=timeout_seconds) as client: response = await client.request( method=method, url=url, headers=headers, json=body, params=params, ) # Try to parse JSON response try: response_data = response.json() except Exception: response_data = {"raw_response": response.text} result = { "status": "success", "status_code": response.status_code, "data": response_data, } logger.debug( f"Custom tool '{tool.name}' completed with status {response.status_code}" ) return result except httpx.TimeoutException: logger.error(f"Custom tool '{tool.name}' timed out after {timeout_seconds}s") return { "status": "error", "error": f"Request timed out after {timeout_seconds} seconds", } except httpx.RequestError as e: logger.error(f"Custom tool '{tool.name}' request failed: {e}") return { "status": "error", "error": f"Request failed: {str(e)}", } except Exception as e: logger.error(f"Custom tool '{tool.name}' execution failed: {e}") return { "status": "error", "error": f"Tool execution failed: {str(e)}", }