feat: add bounded marketplace research

This commit is contained in:
Mikei386
2026-08-24 13:54:17 +02:00
parent 308b8a4d1f
commit 0a5da6e5f1
6 changed files with 137 additions and 7 deletions
+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-v7} image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v8}
container_name: mike-ai-open-webui container_name: mike-ai-open-webui
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+17
View File
@@ -371,6 +371,23 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
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", [])) self.assertIn("server:mcp:web-general-local", result.get("tool_ids", []))
async def test_ebay_research_gets_general_marketplace_protocol(self):
result = await self._select(
"Suche auf eBay nach einem vollständigen Highscreen 386 PC mit Preis und Versand."
)
self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
self.assertIn("Marketplace research protocol", result["messages"][0]["content"])
self.assertIn("roughly three searches and five page fetches", result["messages"][0]["content"])
self.assertTrue(result["metadata"]["mikeai_marketplace_research"])
async def test_generic_marketplace_research_does_not_need_site_rule(self):
result = await self._select(
"Finde auf einem Marktplatz aktuelle Angebote für einen gebrauchten Synthesizer."
)
self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
self.assertIn("deduplicate by listing URL or item number", result["messages"][0]["content"])
self.assertTrue(result["metadata"]["mikeai_marketplace_research"])
async def test_manual_tool_is_preserved(self): async def test_manual_tool_is_preserved(self):
result = await self._select( result = await self._select(
"Prüfe Sonarr.", ["server:mcp:manually-selected"] "Prüfe Sonarr.", ["server:mcp:manually-selected"]
+20 -2
View File
@@ -209,17 +209,35 @@ 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-v7`. V7 begrenzt zusätzlich die `mike-ai/openwebui:main-01f4282-agent-loop-v8`.
Das in V8 enthaltene Verhalten aus V7 begrenzt zusätzlich die
Werkzeugantworten in OpenWebUIs internen Fortsetzungsrunden auf 12.000 Zeichen Werkzeugantworten in OpenWebUIs internen Fortsetzungsrunden auf 12.000 Zeichen
je Ergebnis und 64.000 Zeichen pro Antwortlauf. Damit greift die Begrenzung je Ergebnis und 64.000 Zeichen pro Antwortlauf. Damit greift die Begrenzung
auch bei Ergebnissen, die erst nach dem ersten Request entstehen. 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 Auto Tool Selector 4.3 hält native Websuche immer verfügbar, ergänzt bei
expliziter Webrecherche den TinySearch-Fallback und erkennt unter anderem expliziter Webrecherche den TinySearch-Fallback und erkennt unter anderem
`Home Assistant`, `Home-Assistant` sowie direkte `ha_*`-Werkzeugbezüge. Für `Home Assistant`, `Home-Assistant` sowie direkte `ha_*`-Werkzeugbezüge. Für
Home Assistant verlangt er gezielte Zustandsabfragen und verbietet die Home Assistant verlangt er gezielte Zustandsabfragen und verbietet die
Ableitung einer `entity_id` aus einer YAML-`id`. Ableitung einer `entity_id` aus einer YAML-`id`.
Auto Tool Selector 4.3 ergänzt ein site-unabhängiges Marketplace-Protokoll.
Es begrenzt normale Kaufsuchen auf wenige fokussierte Recherche- und
Verifikationsschritte, nutzt Ergebnis-/Kategorieseiten bei blockierten
Detailseiten und verlangt einen aktuellen Beleg, bevor ein Angebot als aktiv
bezeichnet wird. Dafür existiert kein eBay-, MakerWorld- oder Shop-spezifischer
MCP. OpenWebUI V8 setzt für erkannte Marketplace-Aufträge über beide breiten
Webengines zusammen höchstens drei Such- und fünf Abrufoperationen durch;
danach folgt die sichtbare Synthese aus den vorhandenen Belegen.
Der produktive eBay-Praxistest am 24. August 2026 endete nach exakt drei
TinySearch-Suchen und drei konkreten Seitenabrufen. Qwen lieferte danach eine
sichtbare Antwort, trennte verifizierte, plausible und nicht verifizierbare
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.
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.
Der Schreibpfad ist auf `docs/*.md`, Vorschau, ausdrückliche Freigabe, atomare Der Schreibpfad ist auf `docs/*.md`, Vorschau, ausdrückliche Freigabe, atomare
+12
View File
@@ -31,6 +31,18 @@ bereit und hängt anhand der Anfrage passende Fachkataloge an. Er verweigert
keine Fähigkeit und erfordert keine Site-spezifischen Regeln. Alle zentralen keine Fähigkeit und erfordert keine Site-spezifischen Regeln. Alle zentralen
MCPs sind über feste WireGuard-Ports auch für Hermes und Pi erreichbar. MCPs sind über feste WireGuard-Ports auch für Hermes und Pi erreichbar.
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
Reformulierungen, bevorzugt bei blockierten Detailseiten öffentliche Such- und
Kategorieseiten, dedupliziert Artikelnummern und beendet eine normale Suche
nach ungefähr drei Such- und fünf Abrufschritten. Ein Angebot gilt nur mit
aktuellem Preis-, Laufzeit-, Gebots- oder Kaufbeleg als aktiv.
OpenWebUI V8 setzt diese Marketplace-Grenze request-lokal technisch durch:
höchstens drei Suchoperationen über beide allgemeinen Engines zusammen und
höchstens fünf Seitenabrufe. Andere Aufgaben behalten das allgemeine
40-Aufrufe-Budget.
## Agentische Grenzen ## Agentische Grenzen
- maximal 48 interne Werkzeugrunden - maximal 48 interne Werkzeugrunden
@@ -1,7 +1,7 @@
""" """
title: MikeAI Auto Tool Selector title: MikeAI Auto Tool Selector
author: MikeAI author: MikeAI
version: 4.2.0 version: 4.3.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.
""" """
@@ -90,8 +90,32 @@ class Filter:
), ),
) )
def _needs_marketplace_research(self, raw_text: str) -> bool:
"""Recognise shopping/listing research without requiring a site adapter."""
text = self._normalise(raw_text)
search_intent = self._matches(
text,
(
r"\b(?:such|find|recherchier|pr[uü]f|schau|vergleich)\w*\b",
r"\b(?:ich\s+suche|ich\s+m[oö]chte\s+kaufen|kaufberatung)\b",
),
)
marketplace_context = self._matches(
text,
(
r"\b(?:ebay|kleinanzeigen|etsy|amazon|makerworld|marktplatz|marketplace|verkaufsplattform|online[- ]shop)\b",
r"\b(?:angebot(?:e|en)?|artikel|produkt|preis(?:e|en)?|versand(?:kosten)?|sofort[- ]kaufen|auktion|gebraucht(?:markt)?)\b",
),
)
return search_intent and marketplace_context
@staticmethod @staticmethod
def _add_system_rule(body: dict, labels: list[str], selected: list[str]) -> None: def _add_system_rule(
body: dict,
labels: list[str],
selected: list[str],
marketplace_research: bool = False,
) -> None:
rule = ( rule = (
"MikeAI automatic tool selection made these capabilities available for " "MikeAI automatic tool selection made these capabilities available for "
f"this request: {', '.join(labels)}. Availability is not authorization. " f"this request: {', '.join(labels)}. Availability is not authorization. "
@@ -140,6 +164,21 @@ class Filter:
"the same YAML read, state lookup, or validation unless new evidence " "the same YAML read, state lookup, or validation unless new evidence "
"or a changed configuration makes the repetition necessary." "or a changed configuration makes the repetition necessary."
) )
if marketplace_research:
rule += (
" Marketplace research protocol: this is a general shopping/listing "
"task, not a request for a site-specific tool. Start with one focused "
"search and use at most two reformulations. Prefer marketplace search, "
"category, and result pages when individual listing pages are blocked; "
"their snippets may contain price, shipping, condition, and location. "
"Build a small candidate set, deduplicate by listing URL or item number, "
"and fetch only the most promising candidates. For a normal request, "
"stop after roughly three searches and five page fetches unless the user "
"explicitly requests an exhaustive audit. Call a listing active only "
"when current evidence shows a price, remaining time, bid, or buy status; "
"otherwise label it possibly active. Never invent missing commerce fields, "
"and finish with the verified shortlist instead of repeating synonyms."
)
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):
@@ -301,7 +340,8 @@ class Filter:
return body return body
latest_text = self._latest_user_text(body) latest_text = self._latest_user_text(body)
needs_native_web = self._needs_native_web(latest_text) marketplace_research = self._needs_marketplace_research(latest_text)
needs_native_web = self._needs_native_web(latest_text) or marketplace_research
selected = self._classify(latest_text) selected = self._classify(latest_text)
# Web work gets two independent, general-purpose engines. Native # Web work gets two independent, general-purpose engines. Native
# OpenWebUI search has good result rendering but can return empty sets # OpenWebUI search has good result rendering but can return empty sets
@@ -321,6 +361,10 @@ class Filter:
metadata_features = metadata.setdefault("features", {}) metadata_features = metadata.setdefault("features", {})
if isinstance(metadata_features, dict): if isinstance(metadata_features, dict):
metadata_features["web_search"] = True metadata_features["web_search"] = True
if marketplace_research:
# Consumed by the versioned Open WebUI continuation patch.
# This is a request-local execution policy, not a site rule.
metadata["mikeai_marketplace_research"] = True
if not selected: if not selected:
return body return body
@@ -353,7 +397,12 @@ class Filter:
if needs_native_web: if needs_native_web:
labels.append("native Websuche") labels.append("native Websuche")
if selected: if selected:
self._add_system_rule(body, labels, selected) self._add_system_rule(
body,
labels,
selected,
marketplace_research=marketplace_research,
)
if needs_native_web: if needs_native_web:
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):
@@ -39,6 +39,13 @@ replacement = """ tool_call_iterations = 0
max_single_tool_result_chars = 12000 max_single_tool_result_chars = 12000
max_total_tool_result_chars = 64000 max_total_tool_result_chars = 64000
tool_result_chars_used = 0 tool_result_chars_used = 0
marketplace_research = bool(
(metadata or {}).get('mikeai_marketplace_research', False)
)
marketplace_search_executions = 0
marketplace_fetch_executions = 0
max_marketplace_search_executions = 3
max_marketplace_fetch_executions = 5
tool_execution_counts = {} tool_execution_counts = {}
tool_signature_counts = {} tool_signature_counts = {}
max_tool_call_iterations = getattr( max_tool_call_iterations = getattr(
@@ -61,6 +68,13 @@ replacement = """ response_tool_calls = tool_calls.pop(0)
for candidate in response_tool_calls: for candidate in response_tool_calls:
function = candidate.get('function') or candidate function = candidate.get('function') or candidate
tool_name = function.get('name') or candidate.get('name') or 'unknown' tool_name = function.get('name') or candidate.get('name') or 'unknown'
normalized_tool_name = tool_name.casefold()
is_marketplace_search = normalized_tool_name in ('search', 'search_web', 'research') or normalized_tool_name.endswith(
('web-general-local_search', 'web-general-local_research')
)
is_marketplace_fetch = normalized_tool_name in ('fetch_url', 'scrape_urls') or normalized_tool_name.endswith(
'web-general-local_scrape_urls'
)
arguments = function.get('arguments') or candidate.get('arguments') or '' arguments = function.get('arguments') or candidate.get('arguments') or ''
signature = f'{tool_name}:{arguments}' signature = f'{tool_name}:{arguments}'
if tool_call_executions + len(accepted_tool_calls) >= max_tool_call_executions: if tool_call_executions + len(accepted_tool_calls) >= max_tool_call_executions:
@@ -75,7 +89,27 @@ replacement = """ response_tool_calls = tool_calls.pop(0)
skipped_tool_calls.append(candidate) skipped_tool_calls.append(candidate)
skip_reasons.append(f'repeated identical call suppressed for {tool_name}') skip_reasons.append(f'repeated identical call suppressed for {tool_name}')
continue continue
if (
marketplace_research
and is_marketplace_search
and marketplace_search_executions >= max_marketplace_search_executions
):
skipped_tool_calls.append(candidate)
skip_reasons.append('marketplace search budget reached')
continue
if (
marketplace_research
and is_marketplace_fetch
and marketplace_fetch_executions >= max_marketplace_fetch_executions
):
skipped_tool_calls.append(candidate)
skip_reasons.append('marketplace fetch budget reached')
continue
accepted_tool_calls.append(candidate) accepted_tool_calls.append(candidate)
if marketplace_research and is_marketplace_search:
marketplace_search_executions += 1
if marketplace_research and is_marketplace_fetch:
marketplace_fetch_executions += 1
tool_signature_counts[signature] = tool_signature_counts.get(signature, 0) + 1 tool_signature_counts[signature] = tool_signature_counts.get(signature, 0) + 1
tool_execution_counts[tool_name] = tool_execution_counts.get(tool_name, 0) + 1 tool_execution_counts[tool_name] = tool_execution_counts.get(tool_name, 0) + 1
response_tool_calls = accepted_tool_calls response_tool_calls = accepted_tool_calls