""" title: Qortal API Pipeline author: crowe & ChatGPT git_url: https://github.com/crowetic/qortal-api-tool description: Native Open WebUI tool that lets the LLM call any Qortal Core HTTP endpoint, with automatic discovery and validation against the node's openapi.json. required_open_webui_version: 0.5.0 version: 0.2.0 licence: MIT """ import os import json import requests from typing import Any, Dict, Optional, List # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- DEFAULT_QORTAL_URL = os.getenv("QORTAL_API_URL", "https://api.qortal.org").rstrip("/") OPENAPI_URL = os.getenv("QORTAL_OPENAPI_URL", f"{DEFAULT_QORTAL_URL}/openapi.json") # --------------------------------------------------------------------------- class Tools: """ ⚙️ Qortal API Toolkit for Open WebUI ------------------------------------------------- Generic wrappers + dynamic endpoint list pulled from openapi.json. Methods exposed to the LLM: • list_get_endpoints() – list of all available GET endpoints • qortal_get() – perform validated HTTP GET • qortal_post() – perform HTTP POST """ # ---------------------- internal helpers ------------------------------ # _openapi_cache: Optional[Dict[str, Any]] = None def _build_url(self, endpoint: str, path_params: Optional[Dict[str, str]] = None) -> str: """Replace {tokens} in endpoint and join with base URL.""" cleaned = f"/{endpoint.lstrip('/')}" # ensure single leading slash if path_params: for k, v in path_params.items(): cleaned = cleaned.replace("{" + k + "}", str(v)) return DEFAULT_QORTAL_URL + cleaned # ---------------------- OpenAPI helpers ------------------------------- # def _load_openapi(self) -> Dict[str, Any]: if self._openapi_cache is not None: return self._openapi_cache try: resp = requests.get(OPENAPI_URL, timeout=30) resp.raise_for_status() self._openapi_cache = resp.json() except Exception as e: self._openapi_cache = {} return self._openapi_cache def list_get_endpoints(self) -> List[str]: """Return all GET endpoints available on this Qortal node.""" spec = self._load_openapi() paths = spec.get("paths", {}) return [p for p, verbs in paths.items() if "get" in verbs] def _is_valid_get(self, endpoint: str) -> bool: endpoint = "/" + endpoint.lstrip("/") return endpoint in self.list_get_endpoints() # -------------------------- request core ------------------------------ # def _request( self, method: str, endpoint: str, path_params: Optional[Dict[str, Any]] = None, query_params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None, validate_get: bool = True, ) -> Dict[str, Any]: if method.upper() == "GET" and validate_get and not self._is_valid_get(endpoint): return { "success": False, "error": f"Endpoint '{endpoint}' is not listed as GET in node's OpenAPI spec", "url": None, } url = self._build_url(endpoint, path_params) try: resp = requests.request( method=method.upper(), url=url, params=query_params, json=json_body, timeout=30, ) try: data = resp.json() except ValueError: data = resp.text return { "status_code": resp.status_code, "success": resp.ok, "url": resp.url, "data": data, } except Exception as e: return {"success": False, "error": str(e), "url": url} # -------------------------- PUBLIC TOOLS ----------------------------- # def qortal_get( self, endpoint: str, path_params: Optional[dict] = None, query_params: Optional[dict] = None, ) -> dict: """ Generic HTTP GET to Qortal Core. Parameters ---------- endpoint : str Endpoint path (e.g. "/addresses/balance/{address}") path_params : dict, optional Dict for replacement of {tokens} in endpoint. query_params : dict, optional Dict for URL query parameters. """ return self._request("GET", endpoint, path_params, query_params) def qortal_post( self, endpoint: str, path_params: Optional[dict] = None, json_body: Optional[dict] = None, ) -> dict: """ Generic HTTP POST to Qortal Core. Parameters ---------- endpoint : str Endpoint path (e.g. "/arbitrary/publish") path_params : dict, optional Dict for replacement of {tokens} in endpoint. json_body : dict, optional Dict to send as JSON body. """ return self._request("POST", endpoint, path_params, None, json_body, validate_get=False)