From 0a5da6e5f1b457b30f41ac5a92e7c1d95bd049f7 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:54:17 +0200 Subject: [PATCH] feat: add bounded marketplace research --- compose.yaml | 2 +- dev/test_openwebui_filters.py | 17 ++++++ docs/CURRENT_REFERENCE.md | 22 ++++++- docs/TOOL_ARCHITECTURE_2026-08-24.md | 12 ++++ .../openwebui/filters/auto_tool_selector.py | 57 +++++++++++++++++-- platform/openwebui/patch_tool_finalization.py | 34 +++++++++++ 6 files changed, 137 insertions(+), 7 deletions(-) diff --git a/compose.yaml b/compose.yaml index 01e7eed..c73e7c5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -714,7 +714,7 @@ services: build: context: . 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 restart: unless-stopped volumes: diff --git a/dev/test_openwebui_filters.py b/dev/test_openwebui_filters.py index 389e7b9..2eb1bdb 100644 --- a/dev/test_openwebui_filters.py +++ b/dev/test_openwebui_filters.py @@ -371,6 +371,23 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): self.assertNotIn("server:mcp:web-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): result = await self._select( "Prüfe Sonarr.", ["server:mcp:manually-selected"] diff --git a/docs/CURRENT_REFERENCE.md b/docs/CURRENT_REFERENCE.md index 796eefb..4468836 100644 --- a/docs/CURRENT_REFERENCE.md +++ b/docs/CURRENT_REFERENCE.md @@ -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 für eine sichtbare werkzeugfreie Schlussantwort bereit. Das produktive 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 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 +Auto Tool Selector 4.3 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`. +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. Ein root-eigener Minutentimer erzeugt nur einen begrenzten Laufzeitsnapshot. Der Schreibpfad ist auf `docs/*.md`, Vorschau, ausdrückliche Freigabe, atomare diff --git a/docs/TOOL_ARCHITECTURE_2026-08-24.md b/docs/TOOL_ARCHITECTURE_2026-08-24.md index b48a7df..280bcfd 100644 --- a/docs/TOOL_ARCHITECTURE_2026-08-24.md +++ b/docs/TOOL_ARCHITECTURE_2026-08-24.md @@ -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 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 - maximal 48 interne Werkzeugrunden diff --git a/platform/openwebui/filters/auto_tool_selector.py b/platform/openwebui/filters/auto_tool_selector.py index 4351247..b022c30 100644 --- a/platform/openwebui/filters/auto_tool_selector.py +++ b/platform/openwebui/filters/auto_tool_selector.py @@ -1,7 +1,7 @@ """ title: MikeAI Auto Tool Selector 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. """ @@ -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 - 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 = ( "MikeAI automatic tool selection made these capabilities available for " 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 " "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", []) for message in messages: if message.get("role") == "system" and isinstance(message.get("content"), str): @@ -301,7 +340,8 @@ class Filter: return 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) # Web work gets two independent, general-purpose engines. Native # OpenWebUI search has good result rendering but can return empty sets @@ -321,6 +361,10 @@ class Filter: metadata_features = metadata.setdefault("features", {}) if isinstance(metadata_features, dict): 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: return body @@ -353,7 +397,12 @@ class Filter: if needs_native_web: labels.append("native Websuche") if selected: - self._add_system_rule(body, labels, selected) + self._add_system_rule( + body, + labels, + selected, + marketplace_research=marketplace_research, + ) if needs_native_web: for message in body.get("messages", []): if message.get("role") == "system" and isinstance(message.get("content"), str): diff --git a/platform/openwebui/patch_tool_finalization.py b/platform/openwebui/patch_tool_finalization.py index 04275e1..c887e94 100644 --- a/platform/openwebui/patch_tool_finalization.py +++ b/platform/openwebui/patch_tool_finalization.py @@ -39,6 +39,13 @@ replacement = """ tool_call_iterations = 0 max_single_tool_result_chars = 12000 max_total_tool_result_chars = 64000 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_signature_counts = {} max_tool_call_iterations = getattr( @@ -61,6 +68,13 @@ replacement = """ response_tool_calls = tool_calls.pop(0) for candidate in response_tool_calls: function = candidate.get('function') or candidate 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 '' signature = f'{tool_name}:{arguments}' 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) skip_reasons.append(f'repeated identical call suppressed for {tool_name}') 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) + 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_execution_counts[tool_name] = tool_execution_counts.get(tool_name, 0) + 1 response_tool_calls = accepted_tool_calls