p86a-e
This commit is contained in:
@@ -272,7 +272,8 @@ final readonly class AgentRunner
|
||||
optimizedShopQuery: $optimizedShopQuery,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
userId: $userId,
|
||||
currentPromptFallback: $routingPrompt
|
||||
currentPromptFallback: $routingPrompt,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
}
|
||||
|
||||
@@ -335,6 +336,28 @@ final readonly class AgentRunner
|
||||
$optimizedShopQuery = '';
|
||||
}
|
||||
|
||||
$productListAnchoredShopSearchQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||
prompt: $originalPrompt,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
|
||||
if ($productListAnchoredShopSearchQuery !== $shopSearchQuery) {
|
||||
$this->agentLogger->info('Enriched referential product-list shop query with history or RAG product anchors', [
|
||||
'userId' => $userId,
|
||||
'prompt' => $prompt,
|
||||
'routingPrompt' => $routingPrompt,
|
||||
'optimizedShopQuery' => $optimizedShopQuery,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'productListAnchoredShopSearchQuery' => $productListAnchoredShopSearchQuery,
|
||||
]);
|
||||
|
||||
$shopSearchQuery = $productListAnchoredShopSearchQuery;
|
||||
$optimizedShopQuery = '';
|
||||
}
|
||||
|
||||
$ragAnchoredShopSearchQuery = $this->enrichShopSearchQueryWithRagAnchor(
|
||||
prompt: $originalPrompt,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
@@ -1516,6 +1539,26 @@ final readonly class AgentRunner
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildProductListFollowUpWeakQueryCandidates(
|
||||
string $prompt,
|
||||
string $optimizedShopQuery,
|
||||
string $currentPromptFallback
|
||||
): array {
|
||||
$candidates = [];
|
||||
|
||||
foreach ([$optimizedShopQuery, $currentPromptFallback, $prompt] as $candidate) {
|
||||
$candidate = trim($candidate);
|
||||
if ($candidate !== '' && !in_array($candidate, $candidates, true)) {
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
private function shouldUseCommerceHistoryForShopQuery(string $prompt): bool
|
||||
{
|
||||
$prompt = trim($prompt);
|
||||
@@ -1528,6 +1571,10 @@ final readonly class AgentRunner
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isReferentialProductListShopFollowUpPrompt($prompt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isMetaOnlyShopQuery($prompt)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1579,7 +1626,11 @@ final readonly class AgentRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isCommercialTableFollowUpPrompt($prompt) || $this->isMetaOnlyShopQuery($prompt)) {
|
||||
if (
|
||||
$this->isCommercialTableFollowUpPrompt($prompt)
|
||||
|| $this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||
|| $this->isMetaOnlyShopQuery($prompt)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1632,6 +1683,10 @@ final readonly class AgentRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isReferentialProductListShopFollowUpPrompt($prompt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isMetaOnlyShopQuery($prompt)) {
|
||||
return false;
|
||||
}
|
||||
@@ -2547,12 +2602,16 @@ final readonly class AgentRunner
|
||||
return $overlap === 0 && !$this->isMetaOnlyShopQuery($prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
*/
|
||||
private function resolveShopSearchQuery(
|
||||
string $prompt,
|
||||
string $optimizedShopQuery,
|
||||
string $commerceHistoryContext,
|
||||
string $userId,
|
||||
string $currentPromptFallback = ''
|
||||
string $currentPromptFallback = '',
|
||||
array $knowledgeChunks = []
|
||||
): string {
|
||||
if ($this->isCommercialTableFollowUpPrompt($prompt)) {
|
||||
foreach ($this->buildCommercialTableFollowUpContextCandidates($commerceHistoryContext, $userId) as $contextCandidate) {
|
||||
@@ -2564,11 +2623,25 @@ final readonly class AgentRunner
|
||||
}
|
||||
}
|
||||
|
||||
$currentPromptFallback = trim($currentPromptFallback);
|
||||
foreach ($this->buildProductListFollowUpWeakQueryCandidates($prompt, $optimizedShopQuery, $currentPromptFallback) as $productListFallbackQuery) {
|
||||
$productListContextQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||
prompt: $prompt,
|
||||
shopSearchQuery: $productListFallbackQuery,
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
|
||||
if ($productListContextQuery !== $productListFallbackQuery) {
|
||||
return $productListContextQuery;
|
||||
}
|
||||
}
|
||||
|
||||
if ($optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) {
|
||||
return $this->guardStandaloneOptimizedShopQuery($prompt, $optimizedShopQuery);
|
||||
}
|
||||
|
||||
$currentPromptFallback = trim($currentPromptFallback);
|
||||
if ($currentPromptFallback !== '' && !$this->isMetaOnlyShopQuery($currentPromptFallback)) {
|
||||
return $currentPromptFallback;
|
||||
}
|
||||
@@ -3062,6 +3135,261 @@ final readonly class AgentRunner
|
||||
return $enriched !== '' ? $enriched : $shopSearchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
*/
|
||||
private function guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||
string $prompt,
|
||||
string $shopSearchQuery,
|
||||
string $commerceHistoryContext,
|
||||
string $userId,
|
||||
array $knowledgeChunks = []
|
||||
): string {
|
||||
$shopSearchQuery = trim($shopSearchQuery);
|
||||
|
||||
if (
|
||||
$shopSearchQuery === ''
|
||||
|| !$this->agentRunnerConfig->isShopQueryProductListFollowUpEnabled()
|
||||
|| !$this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||
|| !$this->isWeakProductListFollowUpShopQuery($shopSearchQuery)
|
||||
) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$anchors = [];
|
||||
foreach ($this->buildProductListFollowUpAnchorContextCandidates($commerceHistoryContext, $userId) as $contextCandidate) {
|
||||
$anchors = $this->extractLatestHistoryProductListAnchors($contextCandidate);
|
||||
if ($anchors !== []) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($anchors === []) {
|
||||
$anchors = $this->extractProductListAnchorsFromKnowledgeChunks($knowledgeChunks);
|
||||
}
|
||||
|
||||
if ($anchors === []) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$template = $this->agentRunnerConfig->getShopQueryProductListFollowUpTemplate();
|
||||
$rendered = $this->renderAgentTemplate($template, [
|
||||
'anchors' => implode(' ', $anchors),
|
||||
'query' => $shopSearchQuery,
|
||||
]);
|
||||
$rendered = preg_replace('/\s+/u', ' ', $rendered) ?? $rendered;
|
||||
$rendered = trim($rendered);
|
||||
|
||||
return $rendered !== '' ? $rendered : $shopSearchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildProductListFollowUpAnchorContextCandidates(string $commerceHistoryContext, string $userId): array
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
$commerceHistoryContext = trim($commerceHistoryContext);
|
||||
if ($commerceHistoryContext !== '') {
|
||||
$candidates[] = $commerceHistoryContext;
|
||||
}
|
||||
|
||||
$extendedHistoryBudget = $this->agentRunnerConfig->getShopQueryContextFallbackHistoryBudgetChars();
|
||||
if ($extendedHistoryBudget > mb_strlen($commerceHistoryContext, 'UTF-8')) {
|
||||
$extendedHistory = trim($this->contextService->buildUserContextWithinBudget($userId, $extendedHistoryBudget));
|
||||
if ($extendedHistory !== '') {
|
||||
$candidates[] = $extendedHistory;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback()) {
|
||||
$fullHistory = trim($this->contextService->buildUserContext($userId, true));
|
||||
if ($fullHistory !== '') {
|
||||
$candidates[] = $fullHistory;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($candidates));
|
||||
}
|
||||
|
||||
private function isReferentialProductListShopFollowUpPrompt(string $prompt): bool
|
||||
{
|
||||
$tokens = $this->tokenizeShopQueryCandidate($prompt);
|
||||
if ($tokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokenSet = array_fill_keys($tokens, true);
|
||||
$productTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryProductListFollowUpProductTerms()
|
||||
);
|
||||
$shopTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryProductListFollowUpShopTerms()
|
||||
);
|
||||
|
||||
return $this->tokenSetIntersects($tokenSet, $productTokens)
|
||||
&& $this->tokenSetIntersects($tokenSet, $shopTokens);
|
||||
}
|
||||
|
||||
private function isWeakProductListFollowUpShopQuery(string $shopSearchQuery): bool
|
||||
{
|
||||
if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery);
|
||||
if ($tokens === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$noiseTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryProductListFollowUpNoiseTerms()
|
||||
);
|
||||
|
||||
if ($noiseTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$residualTokens = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (isset($noiseTokens[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$residualTokens[$token] = $token;
|
||||
}
|
||||
|
||||
if (count($residualTokens) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($tokens) > max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpWeakQueryMaxTerms())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return count($residualTokens) <= max(0, $this->agentRunnerConfig->getShopQueryProductListFollowUpWeakQueryMaxResidualTerms());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractLatestHistoryProductListAnchors(string $commerceHistoryContext): array
|
||||
{
|
||||
$maxAnchors = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
|
||||
|
||||
foreach ($this->extractHistoryTurnsNewestFirst($commerceHistoryContext) as $turn) {
|
||||
$answer = preg_replace($this->agentRunnerConfig->getFollowUpHistoryQuestionStripPattern(), '', $turn, 1) ?? $turn;
|
||||
$anchors = $this->extractProductListAnchorsFromText($answer, $maxAnchors);
|
||||
|
||||
if ($anchors !== []) {
|
||||
return $anchors;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractProductListAnchorsFromKnowledgeChunks(array $knowledgeChunks): array
|
||||
{
|
||||
if ($knowledgeChunks === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$maxAnchors = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
|
||||
$text = trim(implode("\n\n", array_map('strval', $knowledgeChunks)));
|
||||
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->extractProductListAnchorsFromText($text, $maxAnchors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractProductListAnchorsFromText(string $text, int $maxAnchors): array
|
||||
{
|
||||
$anchors = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($this->agentRunnerConfig->getShopQueryProductListFollowUpAnchorPatterns() as $pattern) {
|
||||
if (@preg_match_all($pattern, $text, $matches, PREG_SET_ORDER) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$candidate = '';
|
||||
if (isset($match['anchor']) && is_string($match['anchor'])) {
|
||||
$candidate = $match['anchor'];
|
||||
} elseif (isset($match[1]) && is_string($match[1])) {
|
||||
$candidate = $match[1];
|
||||
} elseif (isset($match[0]) && is_string($match[0])) {
|
||||
$candidate = $match[0];
|
||||
}
|
||||
|
||||
$candidate = $this->normalizeShopQueryAnchor($candidate);
|
||||
$candidate = $this->canonicalizeProductListAnchor($candidate);
|
||||
if ($candidate === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = implode(' ', $this->tokenizeShopQueryCandidate($candidate));
|
||||
if ($key === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
$anchors[] = $candidate;
|
||||
|
||||
if (count($anchors) >= $maxAnchors) {
|
||||
return $anchors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $anchors;
|
||||
}
|
||||
|
||||
private function canonicalizeProductListAnchor(string $anchor): string
|
||||
{
|
||||
$tokens = $this->tokenizeShopQueryCandidate($anchor);
|
||||
if ($tokens === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (($tokens[0] ?? '') !== 'testomat') {
|
||||
return trim((string) preg_replace('/\s+/u', ' ', $anchor));
|
||||
}
|
||||
|
||||
if (!isset($tokens[1])) {
|
||||
return 'testomat';
|
||||
}
|
||||
|
||||
$canonical = ['testomat', $tokens[1]];
|
||||
$variantTerms = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||
);
|
||||
|
||||
for ($i = 2, $count = count($tokens); $i < $count; $i++) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (isset($variantTerms[$token]) || preg_match('/\d/u', $token) === 1) {
|
||||
$canonical[] = $token;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return trim(implode(' ', $canonical));
|
||||
}
|
||||
|
||||
private function guardMainDeviceReferentialShopQueryWithHistoryModelAnchor(
|
||||
string $prompt,
|
||||
string $shopSearchQuery,
|
||||
|
||||
@@ -1805,6 +1805,64 @@ final class AgentRunnerConfig
|
||||
return $this->genreString('context_resolution.history_anchor_enrichment.template')
|
||||
?: $this->getRequiredString('shop_runtime.context_resolution.history_anchor_enrichment.template');
|
||||
}
|
||||
|
||||
|
||||
public function isShopQueryProductListFollowUpEnabled(): bool
|
||||
{
|
||||
return $this->genreBool('context_resolution.product_list_followup.enabled') ?? false;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpWeakQueryMaxTerms(): int
|
||||
{
|
||||
return $this->genreInt('context_resolution.product_list_followup.weak_query_max_terms') ?? 4;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpWeakQueryMaxResidualTerms(): int
|
||||
{
|
||||
return $this->genreInt('context_resolution.product_list_followup.weak_query_max_residual_terms') ?? 0;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpMaxAnchors(): int
|
||||
{
|
||||
return $this->genreInt('context_resolution.product_list_followup.max_anchors') ?? 4;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpTemplate(): string
|
||||
{
|
||||
return $this->genreString('context_resolution.product_list_followup.template') ?: '{anchors}';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpProductTerms(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.product_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpShopTerms(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.shop_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpNoiseTerms(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.noise_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpAnchorPatterns(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.anchor_patterns');
|
||||
}
|
||||
public function isShopQueryRagAnchorEnrichmentEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('shop_runtime.context_resolution.rag_anchor_enrichment.enabled');
|
||||
|
||||
@@ -1342,6 +1342,37 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
}
|
||||
}
|
||||
|
||||
$contextResolution = is_array($configurationValues['context_resolution'] ?? null)
|
||||
? $configurationValues['context_resolution']
|
||||
: [];
|
||||
$productListFollowUp = is_array($contextResolution['product_list_followup'] ?? null)
|
||||
? $contextResolution['product_list_followup']
|
||||
: [];
|
||||
if ($productListFollowUp !== []) {
|
||||
if (array_key_exists('enabled', $productListFollowUp) && !is_bool($productListFollowUp['enabled'])) {
|
||||
$errors[] = 'genre.configuration_values.context_resolution.product_list_followup.enabled must be boolean.';
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'weak_query_max_terms',
|
||||
'weak_query_max_residual_terms',
|
||||
'max_anchors',
|
||||
] as $intKey) {
|
||||
if (array_key_exists($intKey, $productListFollowUp) && (($this->asInt($productListFollowUp[$intKey]) ?? -1) < 0)) {
|
||||
$errors[] = sprintf('genre.configuration_values.context_resolution.product_list_followup.%s must be numeric and non-negative.', $intKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('template', $productListFollowUp) && (!is_string($productListFollowUp['template']) || trim($productListFollowUp['template']) === '')) {
|
||||
$errors[] = 'genre.configuration_values.context_resolution.product_list_followup.template must be a non-empty string.';
|
||||
}
|
||||
|
||||
$this->validateStringList($this->toList($productListFollowUp['product_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.product_terms', $errors, $warnings);
|
||||
$this->validateStringList($this->toList($productListFollowUp['shop_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.shop_terms', $errors, $warnings);
|
||||
$this->validateStringList($this->toList($productListFollowUp['noise_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.noise_terms', $errors, $warnings);
|
||||
$this->validateRegexPatternList($productListFollowUp['anchor_patterns'] ?? [], 'genre.configuration_values.context_resolution.product_list_followup.anchor_patterns', $errors);
|
||||
}
|
||||
|
||||
$shopQueryRuntime = is_array($configurationValues['shop_query_runtime'] ?? null)
|
||||
? $configurationValues['shop_query_runtime']
|
||||
: [];
|
||||
|
||||
Reference in New Issue
Block a user