feat: standardize long-running tool workflows

This commit is contained in:
Mikei386
2026-08-24 15:02:25 +02:00
parent 0a5da6e5f1
commit 7978a4ee56
9 changed files with 284 additions and 31 deletions
+1 -1
View File
@@ -714,7 +714,7 @@ services:
build:
context: .
dockerfile: platform/openwebui/Dockerfile
image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v8}
image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v9}
container_name: mike-ai-open-webui
restart: unless-stopped
volumes:
+21
View File
@@ -0,0 +1,21 @@
# Merge this block into ~/.hermes/config.yaml on any VPN-connected client.
# Hermes discovers the tools from each HTTP MCP server automatically at startup.
mcp_servers:
athena_operator:
url: "http://192.168.1.212:8202/mcp"
timeout: 3600
connect_timeout: 30
enabled: true
supports_parallel_tool_calls: false
web:
url: "http://192.168.1.212:8203/mcp"
timeout: 180
connect_timeout: 30
enabled: true
supports_parallel_tool_calls: true
athena_context:
url: "http://192.168.1.212:8201/mcp"
timeout: 120
connect_timeout: 30
enabled: true
supports_parallel_tool_calls: true
+50
View File
@@ -289,6 +289,56 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("search_web/fetch_url", result["messages"][0]["content"])
self.assertEqual(result["reasoning_effort"], "medium")
async def test_unraid_media_write_gets_portable_operator_and_web(self):
result = await self._select(
"Ermittle online das neueste Video, lade es mit yt-dlp auf dem Unraid-Host "
"in einen temporären Ordner, konvertiere es mit ffmpeg und lege die fertige "
"Datei im Share Transfer ab."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
"server:mcp:mua",
"server:mcp:web-general-local",
],
)
self.assertIn("direct MUA management or shell tool", result["messages"][0]["content"])
self.assertIn("Do not ask the user to enable another tool", result["messages"][0]["content"])
self.assertIn("start-status-result pattern", result["messages"][0]["content"])
self.assertTrue(result["metadata"]["mikeai_long_operator_task"])
async def test_generic_remote_host_file_operation_gets_operator(self):
result = await self._select(
"Führe auf dem Server ein vorhandenes Skript aus und speichere die neue Datei unter /data/export."
)
self.assertEqual(result["tool_ids"], ["server:mcp:athena-operator-local"])
self.assertTrue(result["metadata"]["mikeai_long_operator_task"])
async def test_readonly_diagnostics_do_not_get_long_operator_budget(self):
result = await self._select("Prüfe nur lesend den Zustand von Unraid.")
self.assertNotIn("mikeai_long_operator_task", result["metadata"])
async def test_narrow_safety_clause_does_not_cancel_authorized_unraid_write(self):
result = await self._select(
"Lade das Video auf dem Unraid-Host herunter, konvertiere es und lege es "
"im Transfer-Share ab. Installiere dabei kein Paket dauerhaft und ändere "
"keine Unraid-Systemkonfiguration."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
"server:mcp:mua",
],
)
async def test_plain_download_advice_does_not_attach_operator(self):
result = await self._select("Erkläre mir, wie ein Browser einen Download technisch durchführt.")
self.assertNotIn("tool_ids", result)
async def test_voice_transcription_variants_select_unraid(self):
result = await self._select(
"Welche Dacher Contäner laufen aktuell auf dem Anrate Server?"
+64
View File
@@ -0,0 +1,64 @@
# Client-unabhängiger Werkzeugstandard
## Ziel
Eine Aufgabe darf nicht nur deshalb scheitern, weil sie in Hermes oder Pi statt
in OpenWebUI gestellt wurde. OpenWebUI-Filter sind eine Komfort- und
Kontextoptimierung, aber niemals die eigentliche Berechtigungs- oder
Fähigkeitsschicht.
## Kernfähigkeiten für jeden vertrauenswürdigen VPN-Client
| Fähigkeit | MCP-Endpunkt | Zweck |
|---|---|---|
| Breiter Operator | `http://192.168.1.212:8202/mcp` | Terminal, Dateien, Docker, Git, Downloads, Konvertierung, APIs und SSH zu konfigurierten Systemen |
| Allgemeines Web | `http://192.168.1.212:8203/mcp` | Site-unabhängige Suche und Seitenabruf |
| Plattformwissen | `http://192.168.1.212:8201/mcp` | Aufbau, Ist-Zustand, Quellen und Änderungsablauf von Athena |
Diese drei Server bilden das tragfähige Minimum. Fach-MCPs wie Home Assistant,
MUA, ARR, Navidrome und GitHub ergänzen kurze strukturierte Operationen. Sie
sind der bevorzugte Weg, aber keine Voraussetzung: Fehlt eine Spezialfunktion,
bleiben allgemeines Web und Operator verfügbar.
## Client-Verhalten
- **OpenWebUI:** Native Websuche bleibt grundsätzlich verfügbar. Der Auto Tool
Selector hängt Fach-MCPs und den Operator anhand allgemeiner
Fähigkeitsklassen an. Allgemeine Hostarbeit wird anhand von Ausführungs- oder
Änderungsabsicht plus Host-, Datei-, Kommando- oder Dienstkontext erkannt;
nicht anhand einzelner Programme oder Websites.
- **Hermes:** Die drei Kernendpunkte werden in `~/.hermes/config.yaml`
eingetragen. Hermes verbindet sie beim Start, ruft MCP `tools/list` auf und
stellt die entdeckten Werkzeuge in jedem Gespräch bereit. Vorlage:
`config/hermes-mcp-core.yaml.example`.
- **Pi und weitere MCP-Clients:** Dieselben Streamable-HTTP-Endpunkte direkt
konfigurieren. Es ist kein OpenWebUI-Filter und kein zusätzlicher Proxy
erforderlich.
## Verbindliche Sicherheitsgrenze
Toolbeschreibungen und Systemprompts helfen dem Modell bei der Wahl, sind aber
keine Sicherheitsgrenze. Unverzichtbare Verbote, Ausgabelimits,
Schreibabläufe und Schutz vor dem Verlust der Remote-Erreichbarkeit werden im
MCP beziehungsweise im Athena-Operator-Dienst erzwungen. Dadurch gelten sie
identisch für OpenWebUI, Hermes, Pi und zukünftige Clients.
## Lange operative Aufgaben
Lange Arbeiten verwenden clientunabhängig das Muster
`Start -> kompakter Status -> Ergebnis/Verifikation`. Recherche wird vor der
Mutation abgeschlossen; Hilfsmittel werden gebündelt vorbereitet und lange
Kommandos asynchron gestartet, wenn ein synchroner Werkzeugaufruf in ein
Zeitlimit laufen könnte. OpenWebUI gewährt dieser allgemeinen Aufgabenklasse
ein höheres, aber weiterhin endliches Ausführungsbudget: 64 Aufrufe insgesamt
und 24 je Werkzeug. Normale Recherche bleibt bei 40 beziehungsweise 12. Das
ist keine Sonderregel für YouTube, ffmpeg, MUA oder einen bestimmten Server.
## Community-Bezug
Das folgt dem MCP-Modell: Ein Client verbindet einen vertrauenswürdigen
Streamable-HTTP-Server und entdeckt dessen Werkzeuge über `tools/list`.
Hermes registriert konfigurierte HTTP-MCPs beim Start als normale Werkzeuge;
OpenWebUI registriert Remote-MCPs global und kann sie pro Anfrage über
`tool_ids` aktivieren. Clientseitige Auswahl optimiert Kontext und Bedienung,
serverseitige Regeln bleiben maßgeblich.
+1 -1
View File
@@ -16,7 +16,7 @@
| Platform Context MCP | Athena-/MikeAI-Wissen, begrenzter Laufzeitsnapshot und kontrollierte Dokumentationspflege | eigener Container ohne Docker-Socket, Shell, Egress oder Secrets | Kern |
| Athena Operator MCP | Entwicklung und vollständiger Betrieb der KI-Plattform | strukturierte Operationen plus breites, begrenztes Terminal; Erreichbarkeitsänderungen blockiert | Kern |
| Operator-Kontext | `docs/QWEN_OPERATOR_CONTEXT.md` plus `config/operator-system-prompt.txt` | versionierte Selbstbeschreibung und Sicherheitsregeln für Qwen | Kern |
| Unraid/MUA | MUA r020+ auf dem HomeServer, direkter MCP-Endpunkt | read-only Automatik; begrenzte Datei-/Medieninventare; Verwaltung bei explizitem Änderungsauftrag; idempotente Batch-Updates | Kern |
| Unraid/MUA | MUA r022+ auf dem HomeServer, direkter MCP-Endpunkt | read-only Automatik; begrenzte Datei-/Medieninventare; Verwaltung bei explizitem Änderungsauftrag; idempotente Batch-Updates; asynchrone Jobs mit Start/Status/Aufräumen für lange Arbeiten | Kern |
| Whisper | ggml-org/whisper.cpp | Service im Router-Deploy | optional |
| XTTS-v2 | Coqui, offizielles CUDA-12.1-Image per Digest | RTX-3060-Container, Stimme `Annmarie Nele`, CPML | Kern |
| TTS-Gateway | `platform/docker/tts-gateway/` | interne Queue, stabile deutsche Satzblöcke, automatische Erkennung rein englischer Texte und Piper-Fallback | Kern |
+39 -7
View File
@@ -197,6 +197,21 @@ aktiviert er zusätzlich die native Websuche. Der Ablauf steht in
`docs/UNRAID_MEDIA_AUDIT_WORKFLOW.md`. Der frühere GraphQL-basierte Unraid-MCP wurde
entfernt und gehört weder zum Start noch zum Recovery.
Ab MUA r022 stehen für lange, explizit autorisierte Arbeiten zusätzlich
`unraid_system_job_start`, `unraid_system_job_status` und
`unraid_system_job_cleanup` bereit. Der Start kehrt sofort mit einer Job-ID
zurück; Statusabfragen liefern nur kompakte, redigierte Ausgaben. Damit blockiert
ein Download, Transcode oder vergleichbarer Auftrag weder Open WebUI noch Hermes
oder Pi bis zum Prozessende. Die drei Werkzeuge erben dieselbe ausdrücklich
erteilte Berechtigung wie die uneingeschränkte Shell.
Auto Tool Selector 4.5 kennzeichnet allgemeine lange Operator-Aufgaben
produktunabhängig. Der OpenWebUI-Agentenloop stellt dafür bis zu 64
Werkzeugausführungen insgesamt und 24 je Werkzeug bereit; normale Aufgaben
bleiben bei 40/12. Zusätzlich verlangt der Systemhinweis das generische
Start-Status-Ergebnis-Muster und reserviert den Abschluss für Verifikation,
Zielablage und Aufräumen.
Der GitHub-Container läuft produktiv. Token-Datei, interner
Streamable-HTTP-Handshake, fehlende Host-Portfreigabe und exakt drei
read-only Werkzeuge wurden am 24. August 2026 verifiziert.
@@ -204,14 +219,15 @@ Die Transportbrücke verwendet den OpenWebUI-kompatiblen `mcp-proxy` 0.12.0 im
stateless Betrieb. Supergateway wurde nach reproduzierbaren HTTP-400-Fehlern
bei `notifications/initialized` aus diesem Pfad entfernt.
Die agentische OpenWebUI-Schleife führt höchstens 40 einzelne Werkzeuge aus,
höchstens zwölf Aufrufe desselben Werkzeugnamens und höchstens zweimal exakt
dieselbe Signatur. Nach Ende des Budgets stehen zusätzliche interne Runden ausschließlich
für eine sichtbare werkzeugfreie Schlussantwort bereit. Das produktive
OpenWebUI-Derivat trägt den Tag
`mike-ai/openwebui:main-01f4282-agent-loop-v8`.
Die agentische OpenWebUI-Schleife führt bei normalen Aufgaben höchstens 40
einzelne Werkzeuge und höchstens zwölf Aufrufe desselben Werkzeugnamens aus.
Allgemeine lange Operator-Aufgaben erhalten 64 beziehungsweise 24. Exakt
dieselbe Signatur bleibt stets auf zwei Wiederholungen begrenzt. Nach Ende des
Budgets stehen zusätzliche interne Runden ausschließlich für eine sichtbare
werkzeugfreie Schlussantwort bereit. Das produktive OpenWebUI-Derivat trägt
den Tag `mike-ai/openwebui:main-01f4282-agent-loop-v9`.
Das in V8 enthaltene Verhalten aus V7 begrenzt zusätzlich die
Das in V9 enthaltene Verhalten aus V8 begrenzt zusätzlich die
Werkzeugantworten in OpenWebUIs internen Fortsetzungsrunden auf 12.000 Zeichen
je Ergebnis und 64.000 Zeichen pro Antwortlauf. Damit greift die Begrenzung
auch bei Ergebnissen, die erst nach dem ersten Request entstehen.
@@ -238,6 +254,22 @@ Angebote und sortierte Notebook, Zubehör sowie ein Ersatzteilgerät aus. Das
belegt sowohl die technische Grenze als auch eine brauchbare Synthese; ein
eBay-spezifischer MCP war nicht erforderlich.
Auto Tool Selector 4.5 erkennt zusätzlich allgemeine operative Arbeit über
Fähigkeitsklassen: Eine ausdrückliche Ausführungs- oder Änderungsabsicht in
Verbindung mit Host, Dateisystem, Kommando, Dienst, Pfad oder typischen
Kommandozeilenwerkzeugen stellt den Athena Operator bereit. Dadurch benötigen
neue Programme wie Download- oder Medienwerkzeuge keine eigene Selector-Regel.
Bei Unraid-Arbeit werden MUA für kompakte Bestandsaufnahme und der Operator für
die ausdrücklich verlangte allgemeine Schreibarbeit gemeinsam angeboten.
Lange Operator-Aufgaben werden dabei produktunabhängig markiert und folgen dem
Start-Status-Ergebnis-Muster, damit Recherche und Vorbereitung nicht das
Budget für Ausführung, Verifikation und Aufräumen verbrauchen.
Für andere Clients gilt `docs/CLIENT_TOOL_STANDARD.md`. Hermes lädt die drei
Kern-MCPs Web, Operator und Plattformwissen direkt per Streamable HTTP; die
Vorlage liegt unter `config/hermes-mcp-core.yaml.example`. Damit hängt die
Fähigkeit nicht vom OpenWebUI-Filter ab.
Der Platform Context MCP läuft ohne Docker-Socket, Shell, Egress oder Secrets.
Ein root-eigener Minutentimer erzeugt nur einen begrenzten Laufzeitsnapshot.
Der Schreibpfad ist auf `docs/*.md`, Vorschau, ausdrückliche Freigabe, atomare
+7
View File
@@ -31,6 +31,13 @@ bereit und hängt anhand der Anfrage passende Fachkataloge an. Er verweigert
keine Fähigkeit und erfordert keine Site-spezifischen Regeln. Alle zentralen
MCPs sind über feste WireGuard-Ports auch für Hermes und Pi erreichbar.
Der verbindliche client-unabhängige Mindeststandard ist in
`docs/CLIENT_TOOL_STANDARD.md` festgelegt: allgemeines Web, Athena Operator und
Plattformwissen werden in jedem vertrauenswürdigen VPN-Client konfiguriert.
OpenWebUI darf diese Grundfähigkeiten zur Kontextoptimierung automatisch
auswählen; Hermes und Pi entdecken sie direkt über die MCP-Endpunkte. Die
Sicherheitsgrenze liegt immer serverseitig und hängt nicht von einem Filter ab.
Marketplace-Recherche bleibt ebenfalls allgemein: Der Selector erkennt
Kauf-, Angebots-, Preis- und Versandsuchen unabhängig von einer einzelnen
Website. Qwen beginnt mit einer fokussierten Suche, nutzt höchstens zwei
@@ -1,7 +1,7 @@
"""
title: MikeAI Auto Tool Selector
author: MikeAI
version: 4.3.0
version: 4.5.0
description: Keeps broad web access available and adds relevant portable MCP domains without acting as a gate.
"""
@@ -109,6 +109,48 @@ class Filter:
)
return search_intent and marketplace_context
def _is_explicitly_readonly(self, raw_text: str) -> bool:
"""Recognise a global read-only request, not a narrow safety exception."""
text = self._normalise(raw_text)
return self._matches(
text,
(
r"\b(?:nur|ausschlie(?:ß|ss)lich|lediglich)\s+(?:lesen|pr[uü]fen|anzeigen|auflisten)\b",
r"\bread[- ]only\b",
r"\bohne\s+(?:downloads?|umbenennungen?|schreibzugriff|[aä]nderungen?)\b",
r"\bohne\s+downloads?\b.{0,80}\b(?:[aä]nderungen?|umbenennungen?)\b",
r"\b(?:nichts|keinerlei)\s+(?:ver[aä]ndern|[aä]ndern|schreiben)\b",
r"\b(?:[aä]ndere|ver[aä]ndere)\s+nichts\b",
),
)
def _needs_general_operator(self, raw_text: str) -> bool:
"""Detect broad host work by capability, not product or website name."""
text = self._normalise(raw_text)
if self._is_explicitly_readonly(raw_text):
return False
action_intent = self._matches(
text,
(
r"\b(?:ausf[uü]hr|starte?|stoppe?|restart|installier|deploy|bau|erstell|"
r"schreib|speicher|leg|kopier|verschieb|umbenenn|l[oö]sch|entfern|"
r"download|herunterlad|konvertier|transkodier|komprimier|extrahier|"
r"bearbeit|[aä]nder|modifizier|aktualisier|reparier|fix)\w*\b",
r"\b(?:f[uü]hre|lass)\b.{0,80}\b(?:befehl|kommando|skript|command)\b",
),
)
operational_context = self._matches(
text,
(
r"\b(?:host|server|unraid|unreid|unrate|anrate|nas|linux|debian|docker|"
r"container|terminal|shell|kommando|befehl|datei(?:system)?|ordner|"
r"verzeichnis|pfad|share|freigabe|repository|repo|mcp|api)\b",
r"\b(?:ffmpeg|ffprobe|yt-dlp|youtube-dl|curl|wget|git|docker\s+compose)\b",
r"(?:^|\s)/(?:tmp|data|mnt|opt|srv|var|etc|home)(?:/|\b)",
),
)
return action_intent and operational_context
@staticmethod
def _add_system_rule(
body: dict,
@@ -129,7 +171,25 @@ class Filter:
"the Athena Operator terminal when it can answer the unresolved question. Never "
"duplicate an existing backend; integrate or relay it and report any remaining gap."
)
if "unraid" in selected and "unraid_admin" in selected:
if "unraid" in selected and "unraid_admin" in selected and "operator" in selected:
rule += (
" This request explicitly authorizes general work on Unraid. Use "
"read-only MUA first for compact evidence, then use the direct MUA "
"management or shell tool for commands and writes on Unraid. Use "
"the Athena Operator only for orchestration on Athena or as a "
"fallback when the target MCP cannot perform the required operation; "
"do not spend repeated calls inventing a second SSH path. Do not ask "
"the user to enable another tool: all three capabilities are attached. "
"Do not install persistent packages unless explicitly requested; "
"prefer an existing binary or task-local temporary copy. Avoid "
"overwrites unless authorized and verify the final artifact."
" For long-running work, finish discovery before mutation, then use "
"a generic start-status-result pattern: prepare prerequisites in as few "
"calls as possible, start the long command asynchronously when the tool "
"timeout might be exceeded, poll only compact progress, and reserve "
"enough calls for verification, final placement, and cleanup."
)
elif "unraid" in selected and "unraid_admin" in selected:
rule += (
" This request explicitly asks for an Unraid state change. Use the "
"read-only MUA tools to establish evidence, then the narrowest MUA "
@@ -140,6 +200,19 @@ class Filter:
"Do not ask the user to enable another tool: both required MUA "
"capabilities are already attached to this request."
)
elif "unraid" in selected and "operator" in selected:
rule += (
" This request requires general filesystem, command, download, "
"conversion or other host work on Unraid. Use read-only MUA first "
"for compact inventory and runtime evidence, then use the Athena "
"Operator terminal for the exact user-authorized command sequence. "
"Do not ask the user to enable another tool: both capabilities are "
"already attached. Do not install persistent packages unless the "
"user explicitly requested installation; prefer an existing binary "
"or a task-local temporary copy. Write only to the requested target, "
"avoid overwrites unless explicitly authorized, and verify the final "
"artifact independently before reporting success."
)
elif "unraid" in selected:
rule += (
" For bounded file or media-library inventory on Unraid, prefer "
@@ -193,7 +266,8 @@ class Filter:
selected: list[str] = []
operator = self._matches(
general_operator = self._needs_general_operator(raw_text)
operator = general_operator or self._matches(
text,
(
r"\bathena[- ]operator\b",
@@ -267,18 +341,12 @@ class Filter:
r"\b(?:installier|entfern|l[oö]sch|[aä]nder|modifizier)\w*\b.*\b(?:unraid|docker|cont[aä]iner)\b",
r"\b(?:unraid|docker|cont[aä]iner)\b.*\b(?:installier|entfern|l[oö]sch|[aä]nder|modifizier)\w*\b",
r"\bf[uü]hr\w*\b.*\bupdates?\b.*\b(?:durch|aus)\b",
r"\b(?:lad(?:e|en)?|download|herunterlad|konvertier|transkodier|kopier|verschieb|speicher|schreib|leg|umbenenn|l[oö]sch)\w*\b.{0,160}\b(?:unraid|unreid|unrate|anrate|nas|share|freigabe)\b",
r"\b(?:unraid|unreid|unrate|anrate|nas|share|freigabe)\b.{0,160}\b(?:lad(?:e|en)?|download|herunterlad|konvertier|transkodier|kopier|verschieb|speicher|schreib|leg|umbenenn|l[oö]sch)\w*\b",
r"\b(?:kommando|befehl|shell[- ]befehl)\w*\b.{0,100}\b(?:auf|in)\b.{0,30}\b(?:unraid|unreid|unrate|anrate|nas)\b",
),
)
if unraid_write and self._matches(
text,
(
r"\b(?:nur|lediglich|ausschlie(?:ß|ss)lich)\b.{0,30}\b(?:pr[uü]f|anzeig|auflist|status|nachseh)\w*\b",
r"\b(?:keine|keinerlei|nichts?)\b.{0,40}\b(?:[aä]nder|update|aktualisier|durchf[uü]hr|installier|download|umbenenn)\w*\b",
r"\bohne\b.{0,50}\b(?:[aä]nder|update|aktualisier|durchf[uü]hr|installier|download|umbenenn)\w*\b",
r"\b(?:nicht|nichts?)\b.{0,20}\b(?:durchf[uü]hr|ausf[uü]hr|aktualisier|installier)\w*\b",
r"\b(?:[aä]nder|erstell|installier|l[oö]sch|entfern|download|umbenenn)\w*\b.{0,25}\b(?:nichts?|keine[nrms]?|keinerlei)\b",
),
):
if unraid_write and self._is_explicitly_readonly(raw_text):
unraid_write = False
github = self._matches(
text,
@@ -365,6 +433,11 @@ class Filter:
# Consumed by the versioned Open WebUI continuation patch.
# This is a request-local execution policy, not a site rule.
metadata["mikeai_marketplace_research"] = True
# Long operational tasks need more executions than bounded research.
# This is a capability-class policy, not a rule for any product, site,
# command or MCP. The patched Open WebUI loop consumes this marker.
if self._needs_general_operator(latest_text):
metadata["mikeai_long_operator_task"] = True
if not selected:
return body
@@ -31,8 +31,14 @@ replacement = """ tool_call_iterations = 0
# Per-tool and exact-repeat limits below prevent one low-level MCP
# operation from consuming the whole turn.
tool_call_executions = 0
max_tool_call_executions = 40
max_executions_per_tool = 12
long_operator_task = bool(
(metadata or {}).get('mikeai_long_operator_task', False)
)
# Research stays tightly bounded. Explicit long-running host work
# receives a larger, still finite budget so discovery cannot consume
# the calls needed for execution, verification and cleanup.
max_tool_call_executions = 64 if long_operator_task else 40
max_executions_per_tool = 24 if long_operator_task else 12
# Inlet filters only see the first request, not later internal
# tool continuations. Bound the evidence injected into those
# continuations so one broad inventory cannot consume context.