fix(openwebui): guarantee answer after tool budget

This commit is contained in:
Mikei386
2026-08-24 08:06:34 +02:00
parent c402ea79f7
commit 81a563372d
7 changed files with 133 additions and 17 deletions
+8 -4
View File
@@ -705,7 +705,10 @@ services:
start_period: 10s start_period: 10s
open-webui: open-webui:
image: ${OPENWEBUI_IMAGE:-ghcr.io/open-webui/open-webui:v0.9.5} build:
context: .
dockerfile: platform/openwebui/Dockerfile
image: ${OPENWEBUI_IMAGE:-mike-ai/open-webui:v0.9.5-tool-final-v1}
container_name: mike-ai-open-webui container_name: mike-ai-open-webui
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -746,9 +749,10 @@ services:
AUDIO_TTS_VOICE: alloy AUDIO_TTS_VOICE: alloy
ENABLE_SIGNUP: ${OPENWEBUI_ENABLE_SIGNUP:-false} ENABLE_SIGNUP: ${OPENWEBUI_ENABLE_SIGNUP:-false}
ENABLE_FOLLOW_UP_GENERATION: ${OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION:-false} ENABLE_FOLLOW_UP_GENERATION: ${OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION:-false}
# A model must synthesize an answer instead of spending hundreds of # The derived image reserves the last round for a tool-free synthesis.
# iterations retrying an empty/dynamic page or traversing a repository. # Eight rounds leave room for real operator work without allowing a
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS: "12" # simple repository lookup to fan out into dozens of calls.
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS: "8"
USER_AGENT: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" USER_AGENT: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
# Seed native MCP connections on a fresh Open WebUI database. Secrets # Seed native MCP connections on a fresh Open WebUI database. Secrets
# stay inside the tool containers, so these internal URLs need no keys. # stay inside the tool containers, so these internal URLs need no keys.
+13 -7
View File
@@ -26,17 +26,23 @@ benötigte deshalb kleinere, klarere Werkzeuge und harte Abbruchgrenzen.
Trennzeichen, Kopfzeile, Metadatenzeilen, Dezimal- und Datumsformat erkannt; Trennzeichen, Kopfzeile, Metadatenzeilen, Dezimal- und Datumsformat erkannt;
damit führen deutsche Bankexporte nicht mehr unnötig zuerst zu einem damit führen deutsche Bankexporte nicht mehr unnötig zuerst zu einem
ParserError wegen einer falschen Spaltenzahl. Tabellenanalysen sollen im ParserError wegen einer falschen Spaltenzahl. Tabellenanalysen sollen im
Regelfall mit einer Erkennungs- und einer Auswertungsrunde auskommen. Nach Regelfall mit einer Erkennungs- und einer Auswertungsrunde auskommen.
sechs lokalen Dateiaufrufen erzwingt der Guard eine werkzeugfreie 4. Pro Antwort sind höchstens acht Werkzeugrunden erlaubt. Das abgeleitete,
Abschlussantwort, bevor OpenWebUI bei seiner harten Zwölf-Runden-Grenze reproduzierbar gebaute OpenWebUI-Image verwendet die letzte Runde zwingend
ohne sichtbares Ergebnis abbricht. als werkzeugfreie Synthese. Statt `Tool-call limit reached` ohne Ergebnis
4. Pro Antwort sind höchstens zwölf Werkzeugrunden erlaubt. Der zweite erhält der Benutzer deshalb eine sichtbare Antwort aus den vorhandenen
Befunden samt ehrlicher Angabe fehlender Belege. Inlet-Filter allein können
dies nicht erzwingen, weil sie zwischen OpenWebUIs internen Werkzeugrunden
nicht erneut ausgeführt werden. Der zweite
identische Aufruf wird gestoppt. Ein einzelnes Resultat ist auf 10.000, alle identische Aufruf wird gestoppt. Ein einzelnes Resultat ist auf 10.000, alle
Resultate zusammen auf 36.000 Zeichen begrenzt. Resultate zusammen auf 36.000 Zeichen begrenzt.
5. Der Home-Assistant-MCP behält den TLS-Namen `ha.casaderoll.de`, routet ihn 5. Repository-Prüfungen beginnen mit README/Wurzel, verwenden anschließend
höchstens drei gezielte Code-Suchen und öffnen nur relevante Treffer. Eine
konkrete Laufzeitinstanz wird genau einmal über ihr Fachwerkzeug geprüft.
6. Der Home-Assistant-MCP behält den TLS-Namen `ha.casaderoll.de`, routet ihn
im Container aber auf `HOME_LAN_PROXY_IP` im Heimnetz. Dadurch funktioniert im Container aber auf `HOME_LAN_PROXY_IP` im Heimnetz. Dadurch funktioniert
er auch vom Außenstandort über WireGuard. er auch vom Außenstandort über WireGuard.
6. Task-Management ist keine Faktenquelle und wird nicht für einzelne Fragen, 7. Task-Management ist keine Faktenquelle und wird nicht für einzelne Fragen,
Nachschlageaufgaben oder Dateianalysen verwendet. Nachschlageaufgaben oder Dateianalysen verwendet.
## Abnahme ## Abnahme
+7
View File
@@ -0,0 +1,7 @@
FROM ghcr.io/open-webui/open-webui:v0.9.5
COPY platform/openwebui/patch_tool_finalization.py /tmp/patch_tool_finalization.py
RUN python /tmp/patch_tool_finalization.py \
/app/backend/open_webui/utils/middleware.py \
&& rm /tmp/patch_tool_finalization.py
@@ -1,7 +1,7 @@
""" """
title: MikeAI Stability Guard title: MikeAI Stability Guard
author: MikeAI author: MikeAI
version: 2.1.0 version: 2.2.0
description: Bounds tool output and context use and breaks repeated tool-call loops. description: Bounds tool output and context use and breaks repeated tool-call loops.
""" """
@@ -29,9 +29,9 @@ class Filter:
max_total_tool_chars: int = 36000 max_total_tool_chars: int = 36000
compacted_tool_chars: int = 2000 compacted_tool_chars: int = 2000
duplicate_tool_call_limit: int = 2 duplicate_tool_call_limit: int = 2
# OpenWebUI itself stops after twelve rounds without giving the model # Secondary protection for histories that re-enter the filter. The
# another completion. Stay below that ceiling so the model still gets # live internal tool loop is bounded and finalized by the derived
# a final, tool-free turn. # OpenWebUI image because inlet filters do not run between its rounds.
max_tool_calls_per_turn: int = 10 max_tool_calls_per_turn: int = 10
max_private_table_tool_calls: int = 6 max_private_table_tool_calls: int = 6
+3 -1
View File
@@ -237,7 +237,9 @@ with con:
"Für Repository-Suche, echte Datei-Inhalte und gezielte " "Für Repository-Suche, echte Datei-Inhalte und gezielte "
"Code-Suche auf GitHub. Bei Fragen zu Implementierung, README, API-Routen oder " "Code-Suche auf GitHub. Bei Fragen zu Implementierung, README, API-Routen oder "
"Quellcode dieses Werkzeug statt allgemeiner Websuche verwenden. Keine Issues, " "Quellcode dieses Werkzeug statt allgemeiner Websuche verwenden. Keine Issues, "
"Pull Requests, Actions, rekursiven Komplettbäume oder Schreibzugriffe.", "Pull Requests, Actions, rekursiven Komplettbäume oder Schreibzugriffe. Bei einem "
"konkreten Repository zuerst README beziehungsweise Wurzel einmal lesen, danach "
"höchstens drei gezielte Code-Suchen und nur relevante Trefferdateien öffnen.",
), ),
"athena-operator-local": ( "athena-operator-local": (
"Athena Operator", "Athena Operator",
+7 -1
View File
@@ -213,7 +213,13 @@ params = {
"For GitHub repository implementation details, README files, source trees, " "For GitHub repository implementation details, README files, source trees, "
"API routes, or code search, use the dedicated official GitHub repository " "API routes, or code search, use the dedicated official GitHub repository "
"tool instead of guessing from ordinary web results. Use general web search " "tool instead of guessing from ordinary web results. Use general web search "
"for wider public discussion and non-repository sources. " "for wider public discussion and non-repository sources. For one named repository, "
"do not enumerate files recursively. Read the root README or one root listing once, "
"then use one to three targeted code searches for terms such as route, API, CLI, "
"endpoint, command or the relevant framework, and open only the few matching files "
"needed for evidence. If a live deployment is also mentioned, inspect that service "
"once with its domain tool. Normally finish within six GitHub calls and always "
"synthesize an answer from the evidence already obtained. "
"If a specialist tool returns an authentication, authorization, connection, " "If a specialist tool returns an authentication, authorization, connection, "
"or configuration error, do not repeat the same call. Report the error. For " "or configuration error, do not repeat the same call. Report the error. For "
"public information you may make at most one focused fallback attempt with " "public information you may make at most one focused fallback attempt with "
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Make Open WebUI finish with an answer when its internal tool budget is used.
Open WebUI 0.9.5 otherwise ends the request with only
``Tool-call limit reached``. The patch is deliberately assertion-based: an
upstream source change makes the image build fail instead of silently applying
the modification at the wrong location.
"""
from pathlib import Path
import sys
if len(sys.argv) != 2:
raise SystemExit("usage: patch_tool_finalization.py MIDDLEWARE_PY")
path = Path(sys.argv[1])
source = path.read_text()
patch_marker = "The tool budget for this turn is exhausted. Do not call"
if patch_marker in source:
raise SystemExit("Open WebUI tool-finalization patch is already present")
needle = """ res = await generate_chat_completion(
request,
new_form_data,
user,
bypass_system_prompt=True,
)
"""
replacement = """ # The upstream loop otherwise stops with only
# `Tool-call limit reached` after executing this batch. Reserve
# the final completion for synthesis: keep every accumulated
# result, but expose no tools to the model and explicitly require
# a useful, evidence-bounded answer.
force_final_response = (
max_tool_call_iterations is not None
and tool_call_iterations >= max_tool_call_iterations
)
if force_final_response:
new_form_data.pop('tools', None)
new_form_data.pop('tool_ids', None)
final_metadata = dict(metadata)
final_metadata['tools'] = {}
final_metadata['tool_ids'] = []
final_metadata['tool_servers'] = []
new_form_data['metadata'] = final_metadata
new_form_data['messages'] = add_or_update_system_message(
'The tool budget for this turn is exhausted. Do not call '
'or imitate any more tools. Give the user a concise final '
'answer now using only the tool results already present. '
'Explicitly distinguish verified findings from inference, '
'and state what could not be verified. Never end without a '
'visible answer.',
new_form_data['messages'],
append=True,
)
res = await generate_chat_completion(
request,
new_form_data,
user,
bypass_system_prompt=True,
)
"""
if source.count(needle) != 1:
raise SystemExit(
f"expected exactly one Open WebUI continuation anchor, found {source.count(needle)}"
)
source = source.replace(needle, replacement)
needle = """ await stream_body_handler(res, new_form_data)
output[:0] = prior_output
"""
replacement = """ await stream_body_handler(res, new_form_data)
if force_final_response:
# A model may still print tool-shaped output after the
# schemas were removed. It must not trigger the upstream
# hard-error path or another execution attempt.
tool_calls.clear()
output[:0] = prior_output
"""
if source.count(needle) != 1:
raise SystemExit(
f"expected exactly one Open WebUI stream anchor, found {source.count(needle)}"
)
source = source.replace(needle, replacement)
path.write_text(source)