""" title: Qortal API Toolkit author: crowetic (with assistance from chatgpt) git_url: https://gitea.qortal.link/crowetic/AI-Dev version: 0.3.0 license: MIT description: Query any Qortal Core HTTP endpoint directly from Open WebUI. HOW-TO ------ 1. Add this in 'tools' undder 'workspaces' to create the tool function 2. Add the tool to your model 3. In any model/preset: *Chat → ⚙️ Tools* → enable **Qortal API Toolkit**. 4. Ask something like: “Get the balance of address Q...” The LLM will auto-call `qortal_get`. ________ __ .__ _____ __________.___ _____ __ .__ \_____ \ ____________/ |______ | | / _ \\______ \ |/ ____\_ __ ____ _____/ |_|__| ____ ____ / / \ \ / _ \_ __ \ __\__ \ | | / /_\ \| ___/ \ __\ | \/ \_/ ___\ __\ |/ _ \ / \ / \_/. ( <_> ) | \/| | / __ \| |_/ | \ | | || | | | / | \ \___| | | ( <_> ) | \ \_____\ \_/\____/|__| |__| (____ /____|____|__ /____| |___||__| |____/|___| /\___ >__| |__|\____/|___| / \__> \/ \/ \/ \/ \/ ___. __ .__ \_ |__ ___.__. /\ ___________ ______ _ __ _____/ |_|__| ____ | __ < | | \/ _/ ___\_ __ \/ _ \ \/ \/ // __ \ __\ |/ ___\ | \_\ \___ | /\ \ \___| | \( <_> ) /\ ___/| | | \ \___ |___ / ____| \/ \___ >__| \____/ \/\_/ \___ >__| |__|\___ > \/\/ \/ \/ \/ """ import os, json, requests from typing import Optional from pydantic import BaseModel, Field, ConfigDict # ──────────────────────────────────────────────────────────── helpers ────── class _EventEmitter: def __init__(self, cb): # cb is Open WebUI’s internal emitter self.cb = cb async def emit(self, msg, status="in_progress", done=False): if self.cb: await self.cb( { "type": "status", "data": { "status": status, "description": msg, "done": done, }, } ) # ──────────────────────────────────────────────────────────── TOOL ───────── class Tools: # user-tweakable knobs appear in Settings → Tools class Valves(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) QORTAL_URL: str = Field( default=os.getenv("QORTAL_API_URL", "https://api.qortal.org"), description="Base URL of your Qortal node", ) HTTP_TIMEOUT: int = Field(default=30, description="Request timeout in seconds") ALLOW_POST: bool = Field( default=False, description="Enable POST endpoints (node must allow them)", ) def __init__(self): self.valves = self.Valves() self.headers = {"User-Agent": "Open-WebUI Qortal Toolkit (+https://qortal.org)"} # ─────────────────────────── single public function ──────────────────── async def qortal_call( self, endpoint: str, method: str = "GET", path_params_json: str = "{}", query_params_json: str = "{}", json_body_json: str = "{}", __event_emitter__=None, ) -> str: """ Generic Qortal HTTP call. • endpoint (str) – e.g. "/addresses/balance/{address}" • method (str) – "GET" or "POST" • path_params_json (str) – JSON dict of tokens to replace • query_params_json (str) – JSON dict for URL ?key=value • json_body_json (str) – JSON dict for POST body """ emitter = _EventEmitter(__event_emitter__) await emitter.emit(f"{method.upper()} {endpoint}") # parse the JSON strings the LLM supplies try: path_params_json = path_params_json or "{}" query_params_json = query_params_json or "{}" json_body_json = json_body_json or "{}" except json.JSONDecodeError as e: await emitter.emit("Bad JSON in arguments", "error", True) return json.dumps({"success": False, "error": str(e)}) # build full URL url = self.valves.QORTAL_URL.rstrip("/") + "/" + endpoint.lstrip("/") for k, v in path_params.items(): url = url.replace("{" + k + "}", str(v)) # block POST if valve is off if method.upper() == "POST" and not self.valves.ALLOW_POST: await emitter.emit("POST disabled", "error", True) return json.dumps({"success": False, "error": "post_disabled"}) # make the request try: r = requests.request( method.upper(), url, params=query_params, json=json_body if method.upper() == "POST" else None, headers=self.headers, timeout=self.valves.HTTP_TIMEOUT, ) try: data = r.json() except ValueError: data = r.text await emitter.emit(f"→ {r.status_code}", "complete", True) return json.dumps( { "success": r.ok, "status_code": r.status_code, "url": r.url, "data": data, }, ensure_ascii=False, ) except Exception as e: await emitter.emit(str(e), "error", True) return json.dumps({"success": False, "error": str(e)})