This commit is contained in:
team 1
2026-05-09 20:28:43 +02:00
parent aae4935d69
commit 8827a5a13b
4 changed files with 382 additions and 0 deletions

View File

@@ -519,6 +519,7 @@ final readonly class AgentRunner
}
$shopResults = $this->guardShopResultsByReferencedProductAnchor($shopSearchQuery, $shopResults);
$shopResults = $this->guardShopResultsByExactRequestedAccessoryCode($prompt, $shopSearchQuery, $shopResults);
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
$attemptedShopRepair = $repairPayload['attemptedRepair'] || $directIdentityRepairPayload['attemptedRepair'];
$usedShopRepair = $repairPayload['usedRepair'] || $directIdentityRepairPayload['usedRepair'];
@@ -3538,6 +3539,303 @@ final readonly class AgentRunner
return $this->containsAllShopQueryTokens($productText, $anchor);
}
/**
* @param ShopProductResult[] $shopResults
* @return ShopProductResult[]
*/
private function guardShopResultsByExactRequestedAccessoryCode(string $prompt, string $shopSearchQuery, array $shopResults): array
{
if ($shopResults === []) {
return $shopResults;
}
$requestedCodes = $this->extractExactRequestedAccessoryCodes($prompt, $shopSearchQuery);
if ($requestedCodes === []) {
return $shopResults;
}
$filtered = [];
foreach ($shopResults as $product) {
if (!$product instanceof ShopProductResult) {
continue;
}
if ($this->shopProductMatchesExactRequestedAccessoryCode($product, $requestedCodes)) {
$filtered[] = $product;
}
}
return $filtered !== [] ? $filtered : $shopResults;
}
/**
* @return string[]
*/
private function extractExactRequestedAccessoryCodes(string $prompt, string $shopSearchQuery): array
{
$text = $this->normalizeOneLine(trim($prompt . ' ' . $shopSearchQuery));
if ($text === '') {
return [];
}
$codeTerms = $this->agentRunnerConfig->getRequestedAccessoryCodeTerms();
if ($codeTerms === []) {
return [];
}
$tokens = $this->tokenizeAccessoryCodeContext($text);
if ($tokens === []) {
return [];
}
$termTokenSequences = [];
foreach ($codeTerms as $term) {
$termTokens = $this->tokenizeAccessoryCodeContext($term);
if ($termTokens !== []) {
$termTokenSequences[] = $termTokens;
}
}
if ($termTokenSequences === []) {
return [];
}
$codes = [];
foreach ($termTokenSequences as $termTokens) {
$termLength = count($termTokens);
foreach ($tokens as $position => $_token) {
if (!$this->tokenSequenceMatchesAt($tokens, $termTokens, $position)) {
continue;
}
$code = $this->findNearestRequestedAccessoryCodeAfter($tokens, $position + $termLength, 3, $termTokenSequences);
if ($code === '') {
$code = $this->findNearestRequestedAccessoryCodeBefore($tokens, $position - 1, 3, $termTokenSequences);
}
if ($code !== '') {
$codes[$code] = $code;
}
}
}
return array_values($codes);
}
/**
* @param string[] $tokens
* @param array<int, string[]> $termTokenSequences
*/
private function findNearestRequestedAccessoryCodeAfter(array $tokens, int $start, int $window, array $termTokenSequences): string
{
$end = min(count($tokens) - 1, $start + max(0, $window - 1));
for ($index = max(0, $start); $index <= $end; $index++) {
$code = $this->buildRequestedAccessoryCodeFromTokenWindow($tokens, $index, $termTokenSequences);
if ($code !== '') {
return $code;
}
}
return '';
}
/**
* @param string[] $tokens
* @param array<int, string[]> $termTokenSequences
*/
private function findNearestRequestedAccessoryCodeBefore(array $tokens, int $start, int $window, array $termTokenSequences): string
{
$end = max(0, $start - max(0, $window - 1));
for ($index = min(count($tokens) - 1, $start); $index >= $end; $index--) {
$code = $this->buildRequestedAccessoryCodeFromTokenWindow($tokens, $index, $termTokenSequences);
if ($code !== '') {
return $code;
}
}
return '';
}
/**
* @param string[] $tokens
* @param string[] $needle
*/
private function tokenSequenceMatchesAt(array $tokens, array $needle, int $position): bool
{
if ($needle === [] || $position < 0 || $position + count($needle) > count($tokens)) {
return false;
}
foreach ($needle as $offset => $needleToken) {
if (($tokens[$position + $offset] ?? null) !== $needleToken) {
return false;
}
}
return true;
}
/**
* @param string[] $tokens
* @param array<int, string[]> $termTokenSequences
*/
private function buildRequestedAccessoryCodeFromTokenWindow(array $tokens, int $index, array $termTokenSequences): string
{
$token = $tokens[$index] ?? '';
if (!$this->isStrictAccessoryCodeToken($token)) {
return '';
}
$next = $tokens[$index + 1] ?? '';
if ($this->isSingleLetterVariantSuffix($next) && !$this->tokenStartsAnyConfiguredTerm($tokens, $termTokenSequences, $index + 1)) {
return $this->normalizeAccessoryCodePhrase($token . ' ' . $next);
}
$previous = $tokens[$index - 1] ?? '';
if ($this->isShortAlphaCodePrefix($previous) && !$this->tokenStartsAnyConfiguredTerm($tokens, $termTokenSequences, $index - 1)) {
return $this->normalizeAccessoryCodePhrase($previous . ' ' . $token);
}
return $this->normalizeAccessoryCodePhrase($token);
}
/**
* @param string[] $tokens
* @param array<int, string[]> $termTokenSequences
*/
private function tokenStartsAnyConfiguredTerm(array $tokens, array $termTokenSequences, int $position): bool
{
foreach ($termTokenSequences as $termTokens) {
if ($this->tokenSequenceMatchesAt($tokens, $termTokens, $position)) {
return true;
}
}
return false;
}
/**
* @param string[] $requestedCodes
*/
private function shopProductMatchesExactRequestedAccessoryCode(ShopProductResult $product, array $requestedCodes): bool
{
$identityText = $this->normalizeOneLine(trim(implode(' ', array_filter([
$product->name,
$product->url,
]))));
if ($identityText === '') {
return false;
}
$tokens = $this->tokenizeAccessoryCodeContext($identityText);
if ($tokens === []) {
return false;
}
foreach ($requestedCodes as $code) {
if ($this->accessoryCodeTokensContainExactCode($tokens, $code)) {
return true;
}
}
return false;
}
/**
* @param string[] $tokens
*/
private function accessoryCodeTokensContainExactCode(array $tokens, string $requestedCode): bool
{
$codeTokens = $this->tokenizeAccessoryCodeContext($requestedCode);
if ($codeTokens === []) {
return false;
}
$compactCode = $this->normalizeAccessoryCodeForExactMatch($requestedCode);
$codeLength = count($codeTokens);
foreach ($tokens as $index => $token) {
if ($codeLength === 1 && $this->normalizeAccessoryCodeForExactMatch($token) === $compactCode) {
$next = $tokens[$index + 1] ?? '';
if (!$this->isSingleLetterVariantSuffix($next)) {
return true;
}
continue;
}
if ($this->tokenSequenceMatchesAt($tokens, $codeTokens, $index)) {
return true;
}
if ($this->normalizeAccessoryCodeForExactMatch(implode(' ', array_slice($tokens, $index, $codeLength))) === $compactCode) {
return true;
}
}
return false;
}
/**
* @return string[]
*/
private function tokenizeAccessoryCodeContext(string $text): array
{
$normalized = mb_strtolower($this->normalizeOneLine($text), 'UTF-8');
if ($normalized === '') {
return [];
}
preg_match_all('/[\p{L}]+\d[\p{L}\p{N}\-]*|\d+(?:[,.]\d+)?[\p{L}\p{N}\-]*|[\p{L}]+/u', $normalized, $matches);
return array_values(array_filter(
array_map(static fn(string $token): string => trim($token), $matches[0] ?? []),
static fn(string $token): bool => $token !== ''
));
}
private function isStrictAccessoryCodeToken(string $token): bool
{
$token = trim($token);
if ($token === '' || str_contains($token, ',') || str_contains($token, '.')) {
return false;
}
if (preg_match('/^\d+$/u', $token) === 1) {
return mb_strlen($token, 'UTF-8') >= 2;
}
foreach ($this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns() as $pattern) {
if (@preg_match($pattern, $token) === 1) {
return true;
}
}
return preg_match('/^(?:[a-z]{1,4}\d{1,5}[a-z0-9-]*|\d{2,5}[a-z0-9-]*)$/iu', $token) === 1;
}
private function isSingleLetterVariantSuffix(string $token): bool
{
return preg_match('/^[a-z]$/iu', trim($token)) === 1;
}
private function isShortAlphaCodePrefix(string $token): bool
{
return preg_match('/^[a-z]{1,4}$/iu', trim($token)) === 1;
}
private function normalizeAccessoryCodePhrase(string $code): string
{
return $this->normalizeOneLine(mb_strtolower($code, 'UTF-8'));
}
private function normalizeAccessoryCodeForExactMatch(string $code): string
{
return preg_replace('/[^a-z0-9]+/iu', '', mb_strtolower($code, 'UTF-8')) ?? '';
}
/**
* @param ShopProductResult[] $shopResults
* @return ShopProductResult[]