fix p45
This commit is contained in:
@@ -467,6 +467,8 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
$shopResults = $repairPayload['results'];
|
||||
$shopResults = $this->guardDirectProductShopResults($prompt, $shopSearchQuery, $shopResults);
|
||||
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
||||
$attemptedShopRepair = $repairPayload['attemptedRepair'];
|
||||
$usedShopRepair = $repairPayload['usedRepair'];
|
||||
$shopRepairQueries = $repairPayload['repairQueries'];
|
||||
@@ -604,7 +606,21 @@ final readonly class AgentRunner
|
||||
knowledgeEvidenceState: $knowledgeEvidenceState
|
||||
);
|
||||
|
||||
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt, $noLlmFallbackAnswer);
|
||||
$deterministicDirectShopAnswer = $this->buildDeterministicDirectShopResultAnswer(
|
||||
prompt: $prompt,
|
||||
shopResults: $shopResults,
|
||||
commerceIntent: $commerceIntent,
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
|
||||
shopSearchQuery: $shopSearchQuery
|
||||
);
|
||||
|
||||
if ($deterministicDirectShopAnswer !== '') {
|
||||
$fullOutput = $deterministicDirectShopAnswer;
|
||||
yield $this->systemMsg($deterministicDirectShopAnswer, 'answer');
|
||||
} else {
|
||||
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt, $noLlmFallbackAnswer);
|
||||
}
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
@@ -1565,7 +1581,49 @@ final readonly class AgentRunner
|
||||
? $this->preserveCurrentInputShopQueryTerms($prompt, $guardedQuery)
|
||||
: $this->preserveCurrentInputShopQueryTerms($prompt, $shopSearchQuery);
|
||||
|
||||
return $this->cleanupDirectProductAttributeShopQuery($prompt, $query);
|
||||
$query = $this->cleanupDirectProductAttributeShopQuery($prompt, $query);
|
||||
|
||||
return $this->cleanupShopQueryStopwords($query);
|
||||
}
|
||||
|
||||
private function cleanupShopQueryStopwords(string $shopSearchQuery): string
|
||||
{
|
||||
$shopSearchQuery = trim($shopSearchQuery);
|
||||
|
||||
if (
|
||||
$shopSearchQuery === ''
|
||||
|| !$this->agentRunnerConfig->isShopQueryStopwordCleanupEnabled()
|
||||
) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$removeTokens = [];
|
||||
foreach ($this->agentRunnerConfig->getShopQueryStopwordCleanupTerms() as $term) {
|
||||
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
|
||||
$removeTokens[$token] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($removeTokens === []) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$kept = [];
|
||||
foreach ($this->tokenizeShopQueryCandidate($shopSearchQuery) as $token) {
|
||||
if (isset($removeTokens[$token]) || isset($kept[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$kept[$token] = $token;
|
||||
}
|
||||
|
||||
if (count($kept) < max(1, $this->agentRunnerConfig->getShopQueryStopwordCleanupMinTokens())) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$cleaned = implode(' ', array_values($kept));
|
||||
|
||||
return $cleaned !== '' ? $cleaned : $shopSearchQuery;
|
||||
}
|
||||
|
||||
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
|
||||
@@ -2883,6 +2941,269 @@ final readonly class AgentRunner
|
||||
return mb_strtolower($line, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function guardDirectProductShopResults(string $prompt, string $shopSearchQuery, array $shopResults): array
|
||||
{
|
||||
if (
|
||||
$shopResults === []
|
||||
|| !$this->agentRunnerConfig->isDirectShopResultGuardEnabled()
|
||||
) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$requestedTerms = $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery);
|
||||
if ($requestedTerms === []) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shopProductMatchesAnyDirectProductTerm($product, $requestedTerms)) {
|
||||
$filtered[] = $product;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function sortShopResultsForLengthRequest(string $prompt, string $shopSearchQuery, array $shopResults): array
|
||||
{
|
||||
if (
|
||||
count($shopResults) < 2
|
||||
|| !$this->agentRunnerConfig->isShopResultLengthSortEnabled()
|
||||
|| !$this->isShopResultLengthSortRequested($prompt . ' ' . $shopSearchQuery)
|
||||
) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$hasLength = false;
|
||||
$decorated = [];
|
||||
|
||||
foreach (array_values($shopResults) as $index => $product) {
|
||||
$length = $product instanceof ShopProductResult
|
||||
? $this->extractShopProductLengthMeters($product)
|
||||
: null;
|
||||
$hasLength = $hasLength || $length !== null;
|
||||
$decorated[] = [
|
||||
'index' => $index,
|
||||
'length' => $length,
|
||||
'product' => $product,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$hasLength) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
usort($decorated, static function (array $a, array $b): int {
|
||||
if ($a['length'] === null && $b['length'] === null) {
|
||||
return $a['index'] <=> $b['index'];
|
||||
}
|
||||
|
||||
if ($a['length'] === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($b['length'] === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$lengthCompare = $a['length'] <=> $b['length'];
|
||||
|
||||
return $lengthCompare !== 0 ? $lengthCompare : ($a['index'] <=> $b['index']);
|
||||
});
|
||||
|
||||
return array_values(array_map(
|
||||
static fn(array $row): mixed => $row['product'],
|
||||
$decorated
|
||||
));
|
||||
}
|
||||
|
||||
private function isShopResultLengthSortRequested(string $text): bool
|
||||
{
|
||||
foreach ($this->agentRunnerConfig->getShopResultLengthSortTriggerPatterns() as $pattern) {
|
||||
if (@preg_match($pattern, $text) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function extractShopProductLengthMeters(ShopProductResult $product): ?float
|
||||
{
|
||||
$text = trim(implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->description,
|
||||
implode(' ', $product->highlights),
|
||||
$product->customFields,
|
||||
])));
|
||||
|
||||
if ($text === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->agentRunnerConfig->getShopResultLengthSortValuePatterns() as $pattern) {
|
||||
if (@preg_match($pattern, $text, $matches) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $matches['value'] ?? ($matches[1] ?? null);
|
||||
if (!is_scalar($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized = str_replace(',', '.', (string) $value);
|
||||
if (is_numeric($normalized)) {
|
||||
return (float) $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractRequestedDirectProductTerms(string $prompt, string $shopSearchQuery = ''): array
|
||||
{
|
||||
$combined = trim($prompt . ' ' . $shopSearchQuery);
|
||||
if ($combined === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($this->agentRunnerConfig->getShopQueryProductAttributeCleanupProductTypeTerms() as $term) {
|
||||
if ($this->containsAllShopQueryTokens($combined, $term)) {
|
||||
$terms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($terms));
|
||||
}
|
||||
|
||||
private function containsAllShopQueryTokens(string $text, string $term): bool
|
||||
{
|
||||
$tokens = array_fill_keys($this->tokenizeShopQueryCandidate($text), true);
|
||||
$termTokens = $this->tokenizeShopQueryCandidate($term);
|
||||
|
||||
if ($tokens === [] || $termTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($termTokens as $termToken) {
|
||||
if (!isset($tokens[$termToken])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $requestedTerms
|
||||
*/
|
||||
private function shopProductMatchesAnyDirectProductTerm(ShopProductResult $product, array $requestedTerms): bool
|
||||
{
|
||||
$productText = trim(implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->description,
|
||||
implode(' ', $product->highlights),
|
||||
$product->customFields,
|
||||
])));
|
||||
|
||||
foreach ($requestedTerms as $term) {
|
||||
if ($this->containsAllShopQueryTokens($productText, $term)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
*/
|
||||
private function buildDeterministicDirectShopResultAnswer(
|
||||
string $prompt,
|
||||
array $shopResults,
|
||||
string $commerceIntent,
|
||||
bool $shopSearchAttempted,
|
||||
bool $shopSearchHadSystemFailure,
|
||||
string $shopSearchQuery
|
||||
): string {
|
||||
if (
|
||||
!$this->agentRunnerConfig->isDirectShopResultAnswerEnabled()
|
||||
|| !$this->isCommerceIntent($commerceIntent)
|
||||
|| !$shopSearchAttempted
|
||||
|| $shopSearchHadSystemFailure
|
||||
|| $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery) === []
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($shopResults === []) {
|
||||
return $this->agentRunnerConfig->getDirectShopResultAnswerNoResultsMessage();
|
||||
}
|
||||
|
||||
$lines = [$this->agentRunnerConfig->getDirectShopResultAnswerIntro()];
|
||||
|
||||
if ($this->isShopResultLengthSortRequested($prompt . ' ' . $shopSearchQuery)) {
|
||||
$note = trim($this->agentRunnerConfig->getDirectShopResultAnswerSortedByLengthNote());
|
||||
if ($note !== '') {
|
||||
$lines[] = $note;
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
foreach ($this->buildDirectShopProductLines($shopResults, 'accessory_or_consumable') as $line) {
|
||||
$lines[] = $line;
|
||||
}
|
||||
|
||||
return trim(implode("\n", $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildDirectShopProductLines(array $shopResults, string $requestedProductRole): array
|
||||
{
|
||||
$maxResults = max(1, $this->agentRunnerConfig->getDirectShopResultAnswerMaxResults());
|
||||
$lines = [];
|
||||
$index = 1;
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = $this->formatNoLlmShopProductLine($product, $index, $requestedProductRole);
|
||||
$index++;
|
||||
|
||||
if (count($lines) >= $maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($lines === []) {
|
||||
return [$this->agentRunnerConfig->getNoLlmProductField('unreadable_results_message')];
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deterministic safety answer for environments where the LLM returns no tokens.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user