fix: bound broad tool workflows

This commit is contained in:
Mikei386 committed 2026-08-24 13:29:41 +02:00
1 parent 5db73d0a92
commit 308b8a4d1f
11 files changed
+186 -29

No files matched your search

+1 -1
View File
@@ -714,7 +714,7 @@ services:
build: build:
context: . context: .
dockerfile: platform/openwebui/Dockerfile dockerfile: platform/openwebui/Dockerfile
image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v6} image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v7}
container_name: mike-ai-open-webui container_name: mike-ai-open-webui
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+35 -8
View File
@@ -212,6 +212,14 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual( self.assertEqual(
result["tool_ids"], ["server:mcp:homeassistant-local"] result["tool_ids"], ["server:mcp:homeassistant-local"]
) )
self.assertIn("Never call ha_list_states merely", result["messages"][0]["content"])
self.assertIn("Never infer an automation entity_id", result["messages"][0]["content"])
async def test_hyphenated_homeassistant_and_tool_name_are_selected(self):
result = await self._select(
"Führe einen Home-Assistant-Test aus und nutze ha_list_states genau einmal."
)
self.assertEqual(result["tool_ids"], ["server:mcp:homeassistant-local"])
async def test_general_web_is_available_without_site_specific_rules(self): async def test_general_web_is_available_without_site_specific_rules(self):
result = await self._select("Erkläre mir kurz, wie ein Fahrrad funktioniert.") result = await self._select("Erkläre mir kurz, wie ein Fahrrad funktioniert.")
@@ -219,15 +227,15 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
self.assertTrue(result["metadata"]["features"]["web_search"]) self.assertTrue(result["metadata"]["features"]["web_search"])
self.assertNotIn("tool_ids", result) self.assertNotIn("tool_ids", result)
async def test_weather_uses_native_web_without_mcp(self): async def test_weather_gets_native_web_and_compact_fallback(self):
result = await self._select("Soll es heute in Rastatt regnen?") result = await self._select("Soll es heute in Rastatt regnen?")
self.assertNotIn("tool_ids", result) self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
async def test_youtube_channel_question_uses_native_web(self): async def test_youtube_channel_question_gets_general_web_fallback(self):
result = await self._select( result = await self._select(
"Welches Video steht aktuell oben auf dem YouTube-Kanal The Proper People?" "Welches Video steht aktuell oben auf dem YouTube-Kanal The Proper People?"
) )
self.assertNotIn("tool_ids", result) self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
async def test_unraid_uses_readonly_not_mua(self): async def test_unraid_uses_readonly_not_mua(self):
result = await self._select( result = await self._select(
@@ -272,7 +280,10 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
"aktuelle offizielle Liste online und prüfe jede fehlende Folge bei Deezer. " "aktuelle offizielle Liste online und prüfe jede fehlende Folge bei Deezer. "
"Nur lesen, ohne Downloads oder Änderungen." "Nur lesen, ohne Downloads oder Änderungen."
) )
self.assertEqual(result["tool_ids"], ["server:mcp:mua-readonly-local"]) self.assertEqual(
result["tool_ids"],
["server:mcp:mua-readonly-local", "server:mcp:web-general-local"],
)
self.assertTrue(result["features"]["web_search"]) self.assertTrue(result["features"]["web_search"])
self.assertTrue(result["metadata"]["features"]["web_search"]) self.assertTrue(result["metadata"]["features"]["web_search"])
self.assertIn("search_web/fetch_url", result["messages"][0]["content"]) self.assertIn("search_web/fetch_url", result["messages"][0]["content"])
@@ -329,20 +340,36 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
) )
self.assertIn("integrate or relay", result["messages"][0]["content"]) self.assertIn("integrate or relay", result["messages"][0]["content"])
async def test_github_and_explicit_web_only_adds_github_mcp(self): async def test_readonly_integration_plan_does_not_attach_unraid_admin(self):
result = await self._select(
"Analysiere https://github.com/foo/deemix und prüfe den auf Unraid "
"laufenden Container und entwirf einen Athena-MCP. Erstelle keinen zweiten Container, ändere nichts "
"und lies keine Secrets."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:github-local",
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
],
)
async def test_github_and_explicit_web_adds_compact_general_web(self):
result = await self._select( result = await self._select(
"Prüfe dieses GitHub Repository und suche zusätzlich im Netz nach Nutzerstimmen." "Prüfe dieses GitHub Repository und suche zusätzlich im Netz nach Nutzerstimmen."
) )
self.assertEqual( self.assertEqual(
result["tool_ids"], result["tool_ids"],
["server:mcp:github-local"], ["server:mcp:github-local", "server:mcp:web-general-local"],
) )
async def test_plain_public_web_request_does_not_attach_legacy_web_mcp(self): async def test_plain_public_web_request_attaches_general_not_legacy_web(self):
result = await self._select( result = await self._select(
"Suche im Netz auf MakerWorld einen Schlümpfe-Schlüsselanhänger." "Suche im Netz auf MakerWorld einen Schlümpfe-Schlüsselanhänger."
) )
self.assertNotIn("server:mcp:web-local", result.get("tool_ids", [])) self.assertNotIn("server:mcp:web-local", result.get("tool_ids", []))
self.assertIn("server:mcp:web-general-local", result.get("tool_ids", []))
async def test_manual_tool_is_preserved(self): async def test_manual_tool_is_preserved(self):
result = await self._select( result = await self._select(
+11 -2
View File
@@ -147,7 +147,7 @@ Der isolierte Eignungs- und Ausfalltest ist in
- Docker Compose - Docker Compose
- SearXNG, per Digest gepinnt - SearXNG, per Digest gepinnt
- TinySearch 0.5.1, per Digest gepinnt - TinySearch 0.6.1, per Digest gepinnt
- TinySearch ausschließlich im internen Docker-Netz, ohne Host-Port - TinySearch ausschließlich im internen Docker-Netz, ohne Host-Port
- lokale ONNX-Embeddings - lokale ONNX-Embeddings
- OpenWebUI-native allgemeine Suche und Seitenabruf in allen normalen Profilen - OpenWebUI-native allgemeine Suche und Seitenabruf in allen normalen Profilen
@@ -209,7 +209,16 @@ 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 dieselbe Signatur. Nach Ende des Budgets stehen zusätzliche interne Runden ausschließlich
für eine sichtbare werkzeugfreie Schlussantwort bereit. Das produktive für eine sichtbare werkzeugfreie Schlussantwort bereit. Das produktive
OpenWebUI-Derivat trägt den Tag OpenWebUI-Derivat trägt den Tag
`mike-ai/openwebui:main-01f4282-agent-loop-v6`. `mike-ai/openwebui:main-01f4282-agent-loop-v7`. V7 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.
Auto Tool Selector 4.2 hält native Websuche immer verfügbar, ergänzt bei
expliziter Webrecherche den TinySearch-Fallback und erkennt unter anderem
`Home Assistant`, `Home-Assistant` sowie direkte `ha_*`-Werkzeugbezüge. Für
Home Assistant verlangt er gezielte Zustandsabfragen und verbietet die
Ableitung einer `entity_id` aus einer YAML-`id`.
Der Platform Context MCP läuft ohne Docker-Socket, Shell, Egress oder Secrets. Der Platform Context MCP läuft ohne Docker-Socket, Shell, Egress oder Secrets.
Ein root-eigener Minutentimer erzeugt nur einen begrenzten Laufzeitsnapshot. Ein root-eigener Minutentimer erzeugt nur einen begrenzten Laufzeitsnapshot.
+16 -3
View File
@@ -39,8 +39,11 @@ benötigte deshalb kleinere, klarere Werkzeuge und harte Abbruchgrenzen.
erhält der Benutzer deshalb eine sichtbare Antwort aus den vorhandenen erhält der Benutzer deshalb eine sichtbare Antwort aus den vorhandenen
Befunden samt ehrlicher Angabe fehlender Belege. Inlet-Filter allein können Befunden samt ehrlicher Angabe fehlender Belege. Inlet-Filter allein können
dies nicht erzwingen, weil sie zwischen OpenWebUIs internen Werkzeugrunden dies nicht erzwingen, weil sie zwischen OpenWebUIs internen Werkzeugrunden
nicht erneut ausgeführt werden. Ein einzelnes Resultat ist auf 12.000, alle nicht erneut ausgeführt werden. Seit OpenWebUI-Derivat V7 sitzt die
Resultate zusammen auf 64.000 Zeichen begrenzt. Größenbegrenzung deshalb direkt in der internen Fortsetzungsschleife: ein
einzelnes Resultat ist auf 12.000, alle Resultate zusammen auf 64.000
Zeichen begrenzt. Zitate und sichtbarer Werkzeugstatus werden zuvor
verarbeitet; das Modell erhält eine markierte Kopf-/Ende-Verdichtung.
Die Basis ist unveränderlich auf OpenWebUI-Revision Die Basis ist unveränderlich auf OpenWebUI-Revision
`01f4282f1ffe0d6212f58d3afbeae21fffd0c4be` beziehungsweise Image-Digest `01f4282f1ffe0d6212f58d3afbeae21fffd0c4be` beziehungsweise Image-Digest
`sha256:6a773e5c3a246b65cbe74ce942b294292c0e5f81c138f703d111bc162f7d7c3d` `sha256:6a773e5c3a246b65cbe74ce942b294292c0e5f81c138f703d111bc162f7d7c3d`
@@ -65,7 +68,7 @@ benötigte deshalb kleinere, klarere Werkzeuge und harte Abbruchgrenzen.
## Abnahme ## Abnahme
- OpenWebUI-Filtertests: 34 - OpenWebUI-Filtertests: 36
- Web-MCP-Tests: 9 - Web-MCP-Tests: 9
- Athena-Operator-Tests: 13 - Athena-Operator-Tests: 13
- Platform-Context-Test: bestanden - Platform-Context-Test: bestanden
@@ -82,6 +85,16 @@ sudo /opt/mike-ai/stack/dev/verify_mcp_catalogs.sh
Er muss mit `MCP_CATALOG_SUITE_OK` enden. Er muss mit `MCP_CATALOG_SUITE_OK` enden.
## Reale Browserabnahme
Die angemeldete OpenWebUI-Sitzung bestand am 24. August 2026 folgende Läufe:
- unbekannte Website MakerWorld über native Suche plus TinySearch 0.6.1
- GitHub-Repository plus vorhandener Unraid-Container plus Athena-Planung
- mehrstufige Home-Assistant-YAML-Analyse ohne Vollinventar
- bewusst angeforderte 223-KB-Klasse einer Home-Assistant-Zustandsliste; V7
lieferte trotz 2.169 Zuständen nach 30,9 Sekunden eine sichtbare Kurzantwort
## Noch manuell zu prüfen ## Noch manuell zu prüfen
Ein echter Browsertest mit einer bewusst synthetischen CSV benötigt eine Ein echter Browsertest mit einer bewusst synthetischen CSV benötigt eine
+15 -2
View File
@@ -43,7 +43,9 @@ MCPs sind über feste WireGuard-Ports auch für Hermes und Pi erreichbar.
- bei ausgeschöpftem Budget folgt zwingend eine werkzeugfreie, sichtbare - bei ausgeschöpftem Budget folgt zwingend eine werkzeugfreie, sichtbare
Schlussantwort aus den bereits erhobenen Befunden Schlussantwort aus den bereits erhobenen Befunden
- Werkzeugausgaben bleiben kurz: 12.000 Zeichen je Ergebnis und 64.000 Zeichen - Werkzeugausgaben bleiben kurz: 12.000 Zeichen je Ergebnis und 64.000 Zeichen
über den Verlauf; ältere Resultate werden zuerst verdichtet über den Verlauf. Die Begrenzung sitzt in OpenWebUIs interner
Fortsetzungsschleife und greift daher auch auf Resultate, die erst nach dem
ersten Modellschritt entstehen.
Damit stoppt die Plattform bewiesene Schleifen, nicht normale lange Recherche. Damit stoppt die Plattform bewiesene Schleifen, nicht normale lange Recherche.
Die früheren Grenzen von zwölf Gesamtaufrufen und vier Aufrufen je Werkzeug Die früheren Grenzen von zwölf Gesamtaufrufen und vier Aufrufen je Werkzeug
@@ -54,7 +56,7 @@ waren für Qwen3.8-Agentenaufgaben zu klein.
| Client | Standardweg | | Client | Standardweg |
|---|---| |---|---|
| OpenWebUI | native `search_web` und `fetch_url`, immer verfügbar | | OpenWebUI | native `search_web` und `fetch_url`, immer verfügbar |
| Hermes/Pi/andere MCP-Clients | `http://192.168.1.212:8203/mcp` (TinySearch) | | Hermes/Pi/andere MCP-Clients | `http://192.168.1.212:8203/mcp` (TinySearch; laufender Alt-Gateway zusätzlich 8211 bis zum nächsten geplanten WireGuard-Neustart) |
| Spezial-/Rollbackbedarf | historischer `mcp-web` nur mit Compose-Profil `legacy-web` | | Spezial-/Rollbackbedarf | historischer `mcp-web` nur mit Compose-Profil `legacy-web` |
TinySearch stellt die vier Upstream-Werkzeuge `search`, `scrape_urls`, TinySearch stellt die vier Upstream-Werkzeuge `search`, `scrape_urls`,
@@ -81,3 +83,14 @@ Nach Änderungen müssen mindestens folgende Prüfungen erfolgreich sein:
6. Browserlauf mit einer unbekannten öffentlichen Website, GitHub plus 6. Browserlauf mit einer unbekannten öffentlichen Website, GitHub plus
Laufzeitprüfung sowie einer mehrstufigen Home-/Unraid-Aufgabe Laufzeitprüfung sowie einer mehrstufigen Home-/Unraid-Aufgabe
Produktive Abnahme am 24. August 2026:
- MakerWorld ohne Site-Adapter: 12 Werkzeugaufrufe, verifizierter Treffer mit
Downloadzahl und Direktlink, sichtbare Antwort nach 102,5 Sekunden
- GitHub + Unraid + Athena: 12 gezielte Repository-Leseaufrufe plus
Laufzeitprüfung; vorhandenen Deemix-Backendcontainer korrekt wiederverwendet
- Home Assistant YAML: drei auskommentierte Automatisierungen gefunden, ohne
vollständige Zustandsliste und mit sichtbarer Abschlussantwort nach 88,0 Sekunden
- absichtlicher Großausgabetest: 2.169 Home-Assistant-Zustände in einem
Werkzeugresultat; durch V7 intern verdichtet und nach 30,9 Sekunden korrekt
mit ausschließlich Anzahl und Testsatz beantwortet
+1 -1
View File
@@ -64,7 +64,7 @@ TinySearch bleibt als Ganzes read-only. Nur das flüchtige tmpfs-Verzeichnis
temporären Browser- und Sitzungszustand erzeugt. Es wird bei jedem temporären Browser- und Sitzungszustand erzeugt. Es wird bei jedem
Container-Neustart vollständig verworfen. Container-Neustart vollständig verworfen.
TinySearch ist der allgemeine portable Web-MCP. Auf VPN-Port 8203 können Hermes, TinySearch 0.6.1 ist der allgemeine portable Web-MCP. Auf VPN-Port 8203 können Hermes,
Pi und andere Clients seine vier Upstream-Werkzeuge direkt nutzen. SearXNG ist Pi und andere Clients seine vier Upstream-Werkzeuge direkt nutzen. SearXNG ist
der Such-Backenddienst. Die historische eigene Web-Fassade ist nur Rollback. der Such-Backenddienst. Die historische eigene Web-Fassade ist nur Rollback.
+3 -1
View File
@@ -56,7 +56,9 @@ services:
tinysearch: tinysearch:
<<: *tool-common <<: *tool-common
image: marcellm01/tinysearch@sha256:5a03d5a1f1b0fabe48f2a26e05db4a84bcb611106a57ec51e42db4549976aa9c # TinySearch v0.6.1. This release fixes the crawler behavior observed with
# v0.5.1 and adds the current search -> scrape_urls workflow.
image: marcellm01/tinysearch@sha256:7a7d0585f5000f462e699e42b97409715826a9e2edcd09a166afa93a4b7cba31
container_name: mike-ai-tools-tinysearch container_name: mike-ai-tools-tinysearch
dns: ["${AI_DNS:-1.1.1.1}"] dns: ["${AI_DNS:-1.1.1.1}"]
# Crawl4AI keeps transient browser/session state here. The container stays # Crawl4AI keeps transient browser/session state here. The container stays
+1 -1
View File
@@ -70,7 +70,7 @@ fi
# redundant download can hang indefinitely. Prepare only the persistent ONNX # redundant download can hang indefinitely. Prepare only the persistent ONNX
# bundle that is actually absent on a fresh installation. # bundle that is actually absent on a fresh installation.
docker volume create mike-ai-tools_tinysearch-models >/dev/null docker volume create mike-ai-tools_tinysearch-models >/dev/null
tiny_image="marcellm01/tinysearch@sha256:5a03d5a1f1b0fabe48f2a26e05db4a84bcb611106a57ec51e42db4549976aa9c" tiny_image="marcellm01/tinysearch@sha256:7a7d0585f5000f462e699e42b97409715826a9e2edcd09a166afa93a4b7cba31"
if ! docker run --rm --entrypoint test \ if ! docker run --rm --entrypoint test \
-v mike-ai-tools_tinysearch-models:/data/models "$tiny_image" \ -v mike-ai-tools_tinysearch-models:/data/models "$tiny_image" \
-f /data/models/all-minilm-l6-v2-onnx/model.onnx; then -f /data/models/all-minilm-l6-v2-onnx/model.onnx; then
@@ -1,7 +1,7 @@
""" """
title: MikeAI Auto Tool Selector title: MikeAI Auto Tool Selector
author: MikeAI author: MikeAI
version: 4.0.0 version: 4.2.0
description: Keeps broad web access available and adds relevant portable MCP domains without acting as a gate. description: Keeps broad web access available and adds relevant portable MCP domains without acting as a gate.
""" """
@@ -31,6 +31,7 @@ class Filter:
"navidrome": "server:mcp:navidrome-local", "navidrome": "server:mcp:navidrome-local",
"platform": "server:mcp:athena-platform", "platform": "server:mcp:athena-platform",
"operator": "server:mcp:athena-operator-local", "operator": "server:mcp:athena-operator-local",
"web": "server:mcp:web-general-local",
} }
LABELS = { LABELS = {
@@ -42,6 +43,7 @@ class Filter:
"navidrome": "Navidrome", "navidrome": "Navidrome",
"platform": "Athena-Plattformwissen", "platform": "Athena-Plattformwissen",
"operator": "Athena Operator", "operator": "Athena Operator",
"web": "allgemeine Websuche (TinySearch)",
} }
def __init__(self): def __init__(self):
@@ -83,6 +85,7 @@ class Filter:
r"\b(?:web|internet|online|websuche|webrecherche)\b", r"\b(?:web|internet|online|websuche|webrecherche)\b",
r"\b(?:such|recherchier|pr[uü]f|schau)\w*\b.{0,80}\b(?:netz|web|internet|online)\b", r"\b(?:such|recherchier|pr[uü]f|schau)\w*\b.{0,80}\b(?:netz|web|internet|online)\b",
r"\b(?:deezer|youtube|makerworld|wikipedia|docker\s*hub)\b", r"\b(?:deezer|youtube|makerworld|wikipedia|docker\s*hub)\b",
r"\b(?:wetter|regen|regnen|vorhersage)\b",
r"\b(?:aktuell|neueste|heute|bis heute)\w*\b.{0,100}\b(?:quelle|liste|verf[uü]gbar|erschienen|video|folge)\w*\b", r"\b(?:aktuell|neueste|heute|bis heute)\w*\b.{0,100}\b(?:quelle|liste|verf[uü]gbar|erschienen|video|folge)\w*\b",
), ),
) )
@@ -123,6 +126,20 @@ class Filter:
"contents. Use the read-only shell only when that purpose-built tool " "contents. Use the read-only shell only when that purpose-built tool "
"cannot answer." "cannot answer."
) )
if "homeassistant" in selected:
rule += (
" For Home Assistant, start with ha_search, ha_get_state, or an "
"exact entity/configuration query. Never call ha_list_states merely "
"to verify a few referenced entities or automations; use the full "
"state inventory only when the user explicitly requests a complete "
"inventory. For YAML work, inspect the exact file or section first "
"and keep any proposed edit separate from live state verification. "
"Never infer an automation entity_id from its YAML id or alias. After "
"a reload, resolve the actual entity_id through targeted search or the "
"entity registry before calling an automation service. Avoid repeating "
"the same YAML read, state lookup, or validation unless new evidence "
"or a changed configuration makes the repetition necessary."
)
messages = body.setdefault("messages", []) messages = body.setdefault("messages", [])
for message in messages: for message in messages:
if message.get("role") == "system" and isinstance(message.get("content"), str): if message.get("role") == "system" and isinstance(message.get("content"), str):
@@ -161,7 +178,8 @@ class Filter:
homeassistant = self._matches( homeassistant = self._matches(
text, text,
( (
r"\bhome\s*assist(?:ant|ent)s?\b", r"\bhomeassistants?\b", r"\bhass\b", r"\bhome[-\s]*assist(?:ant|ent)s?\b", r"\bhomeassistants?\b", r"\bhass\b",
r"\bha_[a-z0-9_]+\b",
r"\bautomatisierung(?:en)?\b", r"\bentit[aä]t(?:en)?\b", r"\bautomatisierung(?:en)?\b", r"\bentit[aä]t(?:en)?\b",
r"\bautomations?\.ya?ml\b", r"\bconfiguration\.ya?ml\b", r"\bautomations?\.ya?ml\b", r"\bconfiguration\.ya?ml\b",
r"\b(?:sensor|light|switch|climate|automation)\.[\w.-]+", r"\b(?:sensor|light|switch|climate|automation)\.[\w.-]+",
@@ -219,6 +237,7 @@ class Filter:
r"\b(?:keine|keinerlei|nichts?)\b.{0,40}\b(?:[aä]nder|update|aktualisier|durchf[uü]hr|installier|download|umbenenn)\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"\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(?: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",
), ),
): ):
unraid_write = False unraid_write = False
@@ -282,8 +301,14 @@ class Filter:
return body return body
latest_text = self._latest_user_text(body) latest_text = self._latest_user_text(body)
selected = self._classify(latest_text)
needs_native_web = self._needs_native_web(latest_text) needs_native_web = self._needs_native_web(latest_text)
selected = self._classify(latest_text)
# Web work gets two independent, general-purpose engines. Native
# OpenWebUI search has good result rendering but can return empty sets
# for an otherwise valid query. TinySearch adds only four compact
# tools and provides a portable fallback without site-specific MCPs.
if needs_native_web and "web" not in selected:
selected.append("web")
# General web search is a basic capability, not a site-specific MCP. # General web search is a basic capability, not a site-specific MCP.
# Keep it available on every normal request so an unfamiliar website # Keep it available on every normal request so an unfamiliar website
# (MakerWorld, eBay, a vendor page tomorrow) never requires another # (MakerWorld, eBay, a vendor page tomorrow) never requires another
@@ -298,8 +323,6 @@ class Filter:
metadata_features["web_search"] = True metadata_features["web_search"] = True
if not selected: if not selected:
if needs_native_web:
await self._notify(__event_emitter__, ["native Websuche"])
return body return body
existing = body.get("tool_ids") existing = body.get("tool_ids")
@@ -335,10 +358,12 @@ class Filter:
for message in body.get("messages", []): for message in body.get("messages", []):
if message.get("role") == "system" and isinstance(message.get("content"), str): if message.get("role") == "system" and isinstance(message.get("content"), str):
message["content"] += ( message["content"] += (
" Native Open WebUI web search is also enabled for this request. " " Two broad web engines are available: native Open WebUI "
"After collecting local or specialist-tool evidence, continue with " "search_web/fetch_url and TinySearch search/scrape_urls/research. "
"search_web/fetch_url for current public evidence; do not stop merely " "Start with one focused query. If it returns no results, an access "
"because no separate web MCP appears in the MCP tool list." "block, or weak evidence, switch once to the other engine with a "
"reformulated query. Do not repeat many near-identical searches and "
"do not request or wait for a site-specific MCP."
) )
break break
await self._notify(__event_emitter__, labels) await self._notify(__event_emitter__, labels)
@@ -33,6 +33,12 @@ replacement = """ tool_call_iterations = 0
tool_call_executions = 0 tool_call_executions = 0
max_tool_call_executions = 40 max_tool_call_executions = 40
max_executions_per_tool = 12 max_executions_per_tool = 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.
max_single_tool_result_chars = 12000
max_total_tool_result_chars = 64000
tool_result_chars_used = 0
tool_execution_counts = {} tool_execution_counts = {}
tool_signature_counts = {} tool_signature_counts = {}
max_tool_call_iterations = getattr( max_tool_call_iterations = getattr(
@@ -96,6 +102,64 @@ if source.count(needle) != 1:
) )
source = source.replace(needle, replacement) source = source.replace(needle, replacement)
needle = """ results.append(
{
'tool_call_id': tool_call_id,
'content': str(tool_result) if tool_result else '',
**({'files': tool_result_files} if tool_result_files else {}),
**({'embeds': tool_result_embeds} if tool_result_embeds else {}),
}
)
"""
replacement = """ # Citations and the visible terminal event above use the
# complete result. Only the model continuation is compacted.
tool_result_text = str(tool_result) if tool_result else ''
remaining_tool_chars = max(
0, max_total_tool_result_chars - tool_result_chars_used
)
allowed_tool_chars = min(
max_single_tool_result_chars, remaining_tool_chars
)
if len(tool_result_text) > allowed_tool_chars:
original_tool_chars = len(tool_result_text)
if allowed_tool_chars >= 800:
marker = (
'\\n\\n[Tool result compacted by Open WebUI: '
f'{original_tool_chars} characters total; middle omitted. '
'Refine the next tool call instead of requesting the same '
'broad inventory again.]\\n\\n'
)
tail_chars = min(2000, allowed_tool_chars // 4)
head_chars = max(
0, allowed_tool_chars - len(marker) - tail_chars
)
tool_result_text = (
tool_result_text[:head_chars]
+ marker
+ tool_result_text[-tail_chars:]
)
else:
tool_result_text = (
'[Tool-result context budget exhausted. Use a more targeted '
'tool call or answer from existing evidence.]'
)[:allowed_tool_chars]
tool_result_chars_used += len(tool_result_text)
results.append(
{
'tool_call_id': tool_call_id,
'content': tool_result_text,
**({'files': tool_result_files} if tool_result_files else {}),
**({'embeds': tool_result_embeds} if tool_result_embeds else {}),
}
)
"""
if source.count(needle) != 1:
raise SystemExit(
f"expected exactly one Open WebUI tool-result anchor, found {source.count(needle)}"
)
source = source.replace(needle, replacement)
needle = """ res = await generate_chat_completion( needle = """ res = await generate_chat_completion(
request, request,
new_form_data, new_form_data,
+5 -1
View File
@@ -19,6 +19,10 @@ install -d -m 0700 /data/mike-ai-operator/state /data/mike-ai-operator/repositor
install -m 0755 "$ROOT/platform/operator/athena_operatord.py" /usr/local/libexec/mike-ai-athena-operatord install -m 0755 "$ROOT/platform/operator/athena_operatord.py" /usr/local/libexec/mike-ai-athena-operatord
install -m 0644 "$ROOT/platform/operator/athena-operator.service" /etc/systemd/system/mike-ai-athena-operator.service install -m 0644 "$ROOT/platform/operator/athena-operator.service" /etc/systemd/system/mike-ai-athena-operator.service
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now mike-ai-athena-operator.service systemctl enable mike-ai-athena-operator.service
# The unit may already be active during an in-place upgrade. `enable --now`
# does not reload a running process after its executable was replaced, which
# would leave the MCP facade and executor on different protocol versions.
systemctl restart mike-ai-athena-operator.service
systemctl is-active --quiet mike-ai-athena-operator.service systemctl is-active --quiet mike-ai-athena-operator.service
echo ATHENA_OPERATOR_EXECUTOR_OK echo ATHENA_OPERATOR_EXECUTOR_OK