Add the Open-WebUI tool that calls the retrieval bridge
Pasted into Open-WebUI rather than installed by a script: tools live in its own database, and writing there means guessing at the schema and access control of a live app. The secret is a Valve so this file stays committable.
This commit is contained in:
@@ -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://<host-lan-ip>: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)
|
||||
Reference in New Issue
Block a user