feat: add bounded marketplace research
This commit is contained in:
+1
-1
@@ -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:
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user