fix: bound broad tool workflows

This commit is contained in:
Mikei386
2026-08-24 13:29:41 +02:00
parent 5db73d0a92
commit 308b8a4d1f
11 changed files with 186 additions and 29 deletions
+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
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
der Such-Backenddienst. Die historische eigene Web-Fassade ist nur Rollback.
+3 -1
View File
@@ -56,7 +56,9 @@ services:
tinysearch:
<<: *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
dns: ["${AI_DNS:-1.1.1.1}"]
# 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
# bundle that is actually absent on a fresh installation.
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 \
-v mike-ai-tools_tinysearch-models:/data/models "$tiny_image" \
-f /data/models/all-minilm-l6-v2-onnx/model.onnx; then
@@ -1,7 +1,7 @@
"""
title: MikeAI Auto Tool Selector
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.
"""
@@ -31,6 +31,7 @@ class Filter:
"navidrome": "server:mcp:navidrome-local",
"platform": "server:mcp:athena-platform",
"operator": "server:mcp:athena-operator-local",
"web": "server:mcp:web-general-local",
}
LABELS = {
@@ -42,6 +43,7 @@ class Filter:
"navidrome": "Navidrome",
"platform": "Athena-Plattformwissen",
"operator": "Athena Operator",
"web": "allgemeine Websuche (TinySearch)",
}
def __init__(self):
@@ -83,6 +85,7 @@ class Filter:
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(?: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",
),
)
@@ -123,6 +126,20 @@ class Filter:
"contents. Use the read-only shell only when that purpose-built tool "
"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", [])
for message in messages:
if message.get("role") == "system" and isinstance(message.get("content"), str):
@@ -161,7 +178,8 @@ class Filter:
homeassistant = self._matches(
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"\bautomations?\.ya?ml\b", r"\bconfiguration\.ya?ml\b",
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"\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",
),
):
unraid_write = False
@@ -282,8 +301,14 @@ class Filter:
return body
latest_text = self._latest_user_text(body)
selected = self._classify(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.
# Keep it available on every normal request so an unfamiliar website
# (MakerWorld, eBay, a vendor page tomorrow) never requires another
@@ -298,8 +323,6 @@ class Filter:
metadata_features["web_search"] = True
if not selected:
if needs_native_web:
await self._notify(__event_emitter__, ["native Websuche"])
return body
existing = body.get("tool_ids")
@@ -335,10 +358,12 @@ class Filter:
for message in body.get("messages", []):
if message.get("role") == "system" and isinstance(message.get("content"), str):
message["content"] += (
" Native Open WebUI web search is also enabled for this request. "
"After collecting local or specialist-tool evidence, continue with "
"search_web/fetch_url for current public evidence; do not stop merely "
"because no separate web MCP appears in the MCP tool list."
" Two broad web engines are available: native Open WebUI "
"search_web/fetch_url and TinySearch search/scrape_urls/research. "
"Start with one focused query. If it returns no results, an access "
"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
await self._notify(__event_emitter__, labels)
@@ -33,6 +33,12 @@ replacement = """ tool_call_iterations = 0
tool_call_executions = 0
max_tool_call_executions = 40
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_signature_counts = {}
max_tool_call_iterations = getattr(
@@ -96,6 +102,64 @@ if source.count(needle) != 1:
)
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(
request,
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 0644 "$ROOT/platform/operator/athena-operator.service" /etc/systemd/system/mike-ai-athena-operator.service
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
echo ATHENA_OPERATOR_EXECUTOR_OK