From 7d9a3b2f296dcfc1b55813058d6eef2f51c1df7c Mon Sep 17 00:00:00 2001 From: team 1 Date: Sun, 10 May 2026 08:16:19 +0200 Subject: [PATCH] p73 --- ...73_GENERIC_ACCESSORY_SHOP_REPAIR_README.md | 89 +++++++++++++++++ src/Commerce/SearchRepairService.php | 99 +++++++++++++++++++ src/Config/SearchRepairConfig.php | 13 +++ 3 files changed, 201 insertions(+) create mode 100644 patch_history/RETRIEX_PATCH_73_GENERIC_ACCESSORY_SHOP_REPAIR_README.md diff --git a/patch_history/RETRIEX_PATCH_73_GENERIC_ACCESSORY_SHOP_REPAIR_README.md b/patch_history/RETRIEX_PATCH_73_GENERIC_ACCESSORY_SHOP_REPAIR_README.md new file mode 100644 index 0000000..4dc3358 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_73_GENERIC_ACCESSORY_SHOP_REPAIR_README.md @@ -0,0 +1,89 @@ +# RetrieX Patch p73 - Generic Accessory Shop Repair + +## Ziel + +Produktnahe Shop-Suchen mit Kombination aus Produktfamilie, Mess-/Anwendungsbegriff und Zubehörbegriff sollen nicht mehr in Geräte-only Ergebnisse abdriften, wenn der Shop den Mess-/Anwendungsbegriff nicht literal am Zubehörprodukt führt. + +Beispiel: + +```text +Suche im Shop nach Testomat Resthärte Indikator +``` + +Vorher konnte die Suche trotz `Indikator` vor allem Testomat-Geräte liefern, weil `Resthärte` im Shop nicht als expliziter Produkttitel-/Metadatenbegriff am Indikator auftauchte und die Search-Repair-Logik ohne konkrete Typnummer keine Zubehör-Reparaturquery gebaut hat. + +## Lösung + +p73 ergänzt die bestehende Search-Repair-Logik generisch: + +- Wenn eine Anfrage ein Zubehör-/Bundle-Signal enthält, aber keinen konkreten Zubehörcode wie `300` nennt, +- und keine spezifischen Zubehörkandidaten extrahiert werden konnten, +- wird aus der bereits bereinigten primären Shopquery eine fokussierte Zubehör-Reparaturquery gebildet. + +Dabei werden generische Kandidatentokens aus der bestehenden YAML-Konfiguration entfernt, während Produktfamilien-/Markenanker und echte Zubehörbegriffe erhalten bleiben. + +Beispielwirkung: + +```text +testomat resthärte indikator -> testomat indikator +``` + +Dadurch kann die erweiterte Shopsuche passende Indikator-/Reagenzprodukte nachladen, statt die Antwort auf Geräte zu verengen. + +## Generik / Governance + +- Keine Testomat-Sonderlogik. +- Keine Resthärte-Sonderlogik im PHP-Core. +- Keine feste Indikator-300-Regel. +- Keine neue harte Tokenliste im PHP-Core. +- Zubehörbegriffe und generische Tokens kommen aus `genre.yaml` / vorhandenen Vocabulary-Views. +- Bestehende exakte Code-Precision aus p72 bleibt unberührt: explizite Codes wie `300` bleiben weiterhin präzise und ziehen nicht automatisch Varianten wie `300 S`. + +## Geänderte Dateien + +- `src/Commerce/SearchRepairService.php` +- `src/Config/SearchRepairConfig.php` +- `patch_history/RETRIEX_PATCH_73_GENERIC_ACCESSORY_SHOP_REPAIR_README.md` + +## Lokale Checks + +Ausgeführt im Patch-Arbeitsbaum: + +```bash +php -l src/Commerce/SearchRepairService.php +php -l src/Config/SearchRepairConfig.php +php -l src/Agent/AgentRunner.php +php -l src/Config/AgentRunnerConfig.php +``` + +Zusätzlich wurde ein Reflection-Smoke-Test für die Repairquery-Bildung ausgeführt: + +```text +Suche im Shop nach Testomat Resthärte Indikator +primaryQuery: testomat resthärte indikator +=> repair query: testomat indikator +``` + +## Empfohlene Regressionstests in Nutzerumgebung + +```bash +bin/console mto:agent:config:validate +bin/console mto:agent:regression:test +bin/console mto:agent:config:audit-source --details +``` + +Fachliche Smoke-Prompts: + +```text +Suche im Shop nach Testomat Resthärte Indikator +``` + +Erwartung: Die erweiterte Suche darf Zubehör-/Indikatorprodukte nachladen und die Antwort darf nicht nur Geräte empfehlen. + +```text +Was ist der niedrigste Grenzwert für die Wasserhärte, welcher mit einem Testomaten überwacht werden kann? +mit welchem indikator wird der wert gemessen +was kostet der indikator +``` + +Erwartung: p72-Präzision bleibt erhalten; konkreter Code `300` zieht nicht automatisch `300 S`. diff --git a/src/Commerce/SearchRepairService.php b/src/Commerce/SearchRepairService.php index 7314c56..2d785ff 100644 --- a/src/Commerce/SearchRepairService.php +++ b/src/Commerce/SearchRepairService.php @@ -218,6 +218,18 @@ final readonly class SearchRepairService ); } + if ( + $requestedAccessoryCodes === [] + && $accessoryCandidates === [] + && $this->asksForBundleOrAccessory($prompt . ' ' . $primaryQuery) + ) { + $genericAccessoryQueries = $this->buildGenericAccessoryFocusRepairQueries($primaryQuery, $prompt); + + if ($genericAccessoryQueries !== []) { + return $this->normalizeRepairQueries($genericAccessoryQueries, $primaryQuery); + } + } + $topPrimaryName = $primaryShopResults[0]->name ?? ''; $topPrimaryProductNumber = $primaryShopResults[0]->productNumber ?? null; $topPrimaryPhrase = trim($topPrimaryName . ' ' . ($topPrimaryProductNumber ?? '')); @@ -269,6 +281,93 @@ final readonly class SearchRepairService return $this->normalizeRepairQueries($queries, $primaryQuery); } + /** + * @return string[] + */ + private function buildGenericAccessoryFocusRepairQueries(string $primaryQuery, string $prompt): array + { + $source = trim($primaryQuery) !== '' ? $primaryQuery : $prompt; + $tokens = $this->tokenize($source); + + if ($tokens === []) { + return []; + } + + $accessoryQueryTokens = $this->buildTokenSet($this->config->getGenericAccessoryFocusQueryTerms()); + $accessorySignalTokens = $this->buildTokenSet(array_merge( + $this->config->getAccessoryOrBundleTerms(), + $this->config->getGenericAccessoryFocusQueryTerms() + )); + + if (!$this->containsAnyToken($tokens, $accessorySignalTokens)) { + return []; + } + + $removeTokens = $this->buildTokenSet($this->config->getGenericCandidateTokens()); + foreach ($this->buildTokenSet($this->config->getAccessoryOrBundleTerms()) as $token => $_) { + if (!isset($accessoryQueryTokens[$token])) { + $removeTokens[$token] = true; + } + } + + $kept = []; + foreach ($tokens as $token) { + if (isset($removeTokens[$token]) || isset($kept[$token])) { + continue; + } + + $kept[$token] = $token; + } + + if (!$this->containsAnyToken(array_values($kept), $accessoryQueryTokens)) { + foreach ($tokens as $token) { + if (isset($accessoryQueryTokens[$token])) { + $kept[$token] = $token; + break; + } + } + } + + if (!$this->containsAnyToken(array_values($kept), $accessoryQueryTokens)) { + return []; + } + + $maxTokens = max(2, $this->config->getCandidateWordCountCap()); + $queryTokens = array_slice(array_values($kept), 0, $maxTokens); + $query = $this->sanitizeQuery(implode(' ', $queryTokens)); + + return $query !== '' ? [$query] : []; + } + + /** @param string[] $terms */ + private function buildTokenSet(array $terms): array + { + $tokens = []; + + foreach ($terms as $term) { + foreach ($this->tokenize((string) $term) as $token) { + $tokens[$token] = true; + } + } + + return $tokens; + } + + /** + * @param string[] $tokens + * @param array $tokenSet + */ + private function containsAnyToken(array $tokens, array $tokenSet): bool + { + foreach ($tokens as $token) { + if (isset($tokenSet[$token])) { + return true; + } + } + + return false; + } + /** * @return string[] */ diff --git a/src/Config/SearchRepairConfig.php b/src/Config/SearchRepairConfig.php index 1cd6e89..cdd215a 100644 --- a/src/Config/SearchRepairConfig.php +++ b/src/Config/SearchRepairConfig.php @@ -293,6 +293,19 @@ final class SearchRepairConfig ); } + /** @return string[] */ + public function getGenericAccessoryFocusQueryTerms(): array + { + return array_values(array_unique(array_merge( + $this->getAccessoryCandidateTerms(), + $this->genreStringList('product_roles.accessory_product_terms.terms'), + $this->genreStringList('product_roles.requested_accessory_code_terms.terms'), + $this->genreStringList('product_roles.shop_views.accessory_query_terms'), + $this->genreStringList('product_roles.shop_views.accessory_product_terms'), + $this->genreStringList('product_roles.shop_views.accessory_focus_terms') + ))); + } + /** @return string[] */ public function getSpecificityBoostTerms(): array {