diff --git a/AI/openwebui_tool.py b/AI/openwebui_tool.py new file mode 100644 index 0000000..2c6d0f3 --- /dev/null +++ b/AI/openwebui_tool.py @@ -0,0 +1,129 @@ +""" +title: Varaverk Docs +description: Search Varaverk's own documentation and return grounding context. +author: Varaverk +version: 1.0.0 +required_open_webui_version: 0.4.0 +""" + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# PURPOSE +# Open-WebUI tool that lets the chat model search Varaverk's documentation index and answer +# from it, instead of from whatever it happens to remember about a private project it has +# never seen. +# +# OPERATIONAL MODEL +# Not installed by any Varaverk script. Open-WebUI stores tools in its own database, and +# writing there directly would mean guessing at its schema, IDs and access control on a live +# app. Paste this into Open-WebUI → Workspace → Tools → +, then set the two Valves. +# +# Calls AI/ai_serve.js over HTTP because Open-WebUI runs in a container that cannot see +# Varaverk's filesystem and has no WebGUI session, so the plugin's PHP API is unreachable to +# it. The bridge returns chunks; the model already loaded in Open-WebUI does the generating. +# +# CONFIGURATION (Valves — set in the Open-WebUI tool editor, not here) +# base_url http://:7822 — AI_HTTP_PORT from master.conf +# secret AI_HTTP_SECRET from master.conf +# +# The secret is a Valve rather than a constant so this file stays committable. Do not paste +# it into the code — this path is git-tracked and pushed to a remote. +# +# OPERATIONAL SAFEGUARDS +# Retrieval only. The bridge exposes one read-only verb over documentation already in git; +# this tool cannot write conf, run a script, or change anything. +# +# Returns "no relevant documentation found" rather than an empty string on a miss, so the +# model states that plainly instead of filling the silence from memory — the failure this +# whole retrieval path exists to prevent. +# +# Every failure is returned as readable text, never raised. An exception inside a tool call +# surfaces to the user as an opaque error; a sentence explaining that the bridge is +# unreachable is something they can act on. +# +# Time-boxed at 30s. A hung retrieval must not hold the chat turn open indefinitely. +# ═══════════════════════════════════════════════════════════════════════════════════════════════ + +import json +import urllib.parse +import urllib.request + +from pydantic import BaseModel, Field + + +class Tools: + class Valves(BaseModel): + base_url: str = Field( + default="http://192.168.50.2:7822", + description="AI retrieval bridge — host LAN IP and AI_HTTP_PORT. Not localhost: " + "Open-WebUI is a container and localhost is itself.", + ) + secret: str = Field( + default="", + description="AI_HTTP_SECRET from Configurations/master.conf", + ) + results: int = Field( + default=8, + description="Chunks to retrieve per query (1-25)", + ) + + def __init__(self): + self.valves = self.Valves() + + def search_varaverk_docs(self, query: str, kind: str = "") -> str: + """ + Search the Varaverk documentation index for passages relevant to a question about + this specific home-media system: its scripts, configuration variables, safeguards, + orchestrators, rsync behaviour, watchdogs, fallback logic or plugin internals. + + Always use this before answering any question about Varaverk. Varaverk is a private + project and is not in your training data; without this tool you do not know what it + is and must not guess. + + :param query: The question or topic to search for, in natural language. + :param kind: Optional filter on where the text comes from. Use "readme" for + definitional or narrative questions such as "what is Varaverk" or "why does this + exist" — otherwise per-script header sections outrank the top-level prose and the + answer will look absent when it is not. Leave empty for specific technical + questions. One of: header, readme, manual, template, doc. + :return: Numbered passages with their source paths, or a message saying nothing matched. + """ + if not self.valves.secret: + return ("The Varaverk docs tool is not configured: its 'secret' Valve is empty. " + "Set it to AI_HTTP_SECRET from Configurations/master.conf.") + + params = { + "key": self.valves.secret, + "q": query, + "k": max(1, min(int(self.valves.results), 25)), + } + if kind: + params["kind"] = kind + + url = f"{self.valves.base_url.rstrip('/')}/search?" + urllib.parse.urlencode(params) + + try: + with urllib.request.urlopen(url, timeout=30) as r: + data = json.load(r) + except Exception as e: + return (f"Could not reach the Varaverk retrieval bridge at " + f"{self.valves.base_url} ({e}). It is started by AI/start_ai_server.sh; " + f"check that AI_ENABLED and AI_HTTP_PORT are set and the index exists.") + + if not data.get("ok"): + return f"Varaverk retrieval failed: {data.get('error', 'unknown error')}" + + results = data.get("results") or [] + if not results: + return (f"No relevant documentation found for '{query}'. Say so plainly rather " + f"than answering from general knowledge — Varaverk is private and is not " + f"in your training data.") + + out = [f"{len(results)} passage(s) from the Varaverk documentation index:", ""] + for i, r in enumerate(results, 1): + label = " › ".join(x for x in (r.get("path"), r.get("section"), r.get("heading")) if x) + out.append(f"[{i}] {label} (score {r.get('score')})") + out.append(r.get("content", "").strip()) + out.append("") + out.append("Answer only from the passages above, and cite them by their [n] markers. " + "If they do not contain the answer, say so and name what is missing.") + return "\n".join(out)