This commit is contained in:
team 1
2026-05-10 16:06:53 +02:00
parent 63210a14de
commit 6e72dfb2e5
9 changed files with 839 additions and 4 deletions

View File

@@ -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,

View File

@@ -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');

View File

@@ -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']
: [];