p72
This commit is contained in:
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user