This commit is contained in:
team 1
2026-05-05 08:16:45 +02:00
parent b259b6cd2d
commit de12386a98
11 changed files with 523 additions and 6 deletions

View File

@@ -1651,12 +1651,122 @@ final readonly class AgentRunner
}
$guardedQuery = $this->guardStandaloneOptimizedShopQuery($prompt, $shopSearchQuery);
$query = $guardedQuery !== $shopSearchQuery
? $this->preserveCurrentInputShopQueryTerms($prompt, $guardedQuery)
: $this->preserveCurrentInputShopQueryTerms($prompt, $shopSearchQuery);
if ($guardedQuery !== $shopSearchQuery) {
return $this->preserveCurrentInputShopQueryTerms($prompt, $guardedQuery);
return $this->cleanupDirectProductAttributeShopQuery($prompt, $query);
}
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
{
$shopSearchQuery = trim($shopSearchQuery);
if (
$shopSearchQuery === ''
|| !$this->agentRunnerConfig->isShopQueryProductAttributeCleanupEnabled()
) {
return $shopSearchQuery;
}
return $this->preserveCurrentInputShopQueryTerms($prompt, $shopSearchQuery);
$combined = trim($prompt . ' ' . $shopSearchQuery);
if (!$this->containsAnyShopQueryTerm($combined, $this->agentRunnerConfig->getShopQueryProductAttributeCleanupProductTypeTerms())) {
return $shopSearchQuery;
}
$constraintTokens = $this->extractConfiguredShopQueryConstraintTokens(
$combined,
$this->agentRunnerConfig->getShopQueryProductAttributeCleanupComparativeConstraintPatterns()
);
if ($constraintTokens === []) {
return $shopSearchQuery;
}
$removeTokens = array_fill_keys($constraintTokens, true);
foreach ($this->agentRunnerConfig->getShopQueryProductAttributeCleanupStopTerms() as $term) {
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
$removeTokens[$token] = true;
}
}
$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->getShopQueryProductAttributeCleanupMinTokens())) {
return $shopSearchQuery;
}
$cleaned = implode(' ', array_values($kept));
return $cleaned !== '' ? $cleaned : $shopSearchQuery;
}
/**
* @param string[] $terms
*/
private function containsAnyShopQueryTerm(string $text, array $terms): bool
{
$tokens = array_fill_keys($this->tokenizeShopQueryCandidate($text), true);
if ($tokens === []) {
return false;
}
foreach ($terms as $term) {
$termTokens = $this->tokenizeShopQueryCandidate($term);
if ($termTokens === []) {
continue;
}
$matches = true;
foreach ($termTokens as $termToken) {
if (!isset($tokens[$termToken])) {
$matches = false;
break;
}
}
if ($matches) {
return true;
}
}
return false;
}
/**
* @param string[] $patterns
* @return string[]
*/
private function extractConfiguredShopQueryConstraintTokens(string $text, array $patterns): array
{
$tokens = [];
foreach ($patterns as $pattern) {
if (@preg_match_all($pattern, $text, $matches, PREG_SET_ORDER) === false) {
continue;
}
foreach ($matches as $match) {
$value = $match['value'] ?? ($match[1] ?? '');
if (!is_scalar($value)) {
continue;
}
foreach ($this->tokenizeShopQueryCandidate((string) $value) as $token) {
$tokens[$token] = $token;
}
}
}
return array_values($tokens);
}
private function preserveCurrentInputShopQueryTerms(string $prompt, string $shopSearchQuery): string

View File

@@ -222,6 +222,9 @@ final readonly class PromptBuilder
$isDetailed = count($limitedShopResults) <= $this->config->getDetailedShopResultsMaxCount();
$requestedRole = $requestedProductRole ?? $this->resolveRequestedProductRole($prompt);
$measurementGuard = $this->resolveRequestedMeasurementGuard($prompt);
if ($measurementGuard !== null && $this->shouldSkipMeasurementEvidenceForAccessoryLookup($prompt, $requestedRole)) {
$measurementGuard = null;
}
$lines = [];
foreach ($limitedShopResults as $i => $product) {
@@ -779,13 +782,17 @@ final readonly class PromptBuilder
return '';
}
$resolvedRequestedRole = $requestedRole ?? $this->resolveRequestedProductRole($prompt);
if ($this->shouldSkipMeasurementEvidenceForAccessoryLookup($prompt, $resolvedRequestedRole)) {
return '';
}
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
$nonEquivalentTerms = $this->extractMeasurementGuardStringList($guard, 'non_equivalent_terms');
$label = $this->normalizeBlockText((string) ($guard['label'] ?? $this->config->getMeasurementEvidenceRuleTemplate('default_requested_parameter_label')));
$strictNoEvidence = (bool) ($guard['strict_no_evidence'] ?? true);
$resolvedRequestedRole = $requestedRole ?? $this->resolveRequestedProductRole($prompt);
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) (
$resolvedRequestedRole === 'accessory_or_consumable'
? ($guard['safe_no_accessory_evidence_answer_de'] ?? $guard['safe_no_evidence_answer_de'] ?? '')
@@ -869,6 +876,21 @@ final readonly class PromptBuilder
}
private function shouldSkipMeasurementEvidenceForAccessoryLookup(string $prompt, string $requestedRole): bool
{
if ($requestedRole !== 'accessory_or_consumable') {
return false;
}
$normalizedPrompt = $this->normalizeForMeasurementMatching($prompt);
if ($this->containsAnyPromptKeyword($normalizedPrompt, $this->config->getMeasurementEvidenceAccessoryLookupGuardTerms())) {
return false;
}
return $this->containsAnyPromptKeyword($normalizedPrompt, $this->config->getMeasurementEvidenceAccessoryLookupPassthroughTerms());
}
/**
* @param array<string, string> $values
*/

View File

@@ -208,6 +208,16 @@ final readonly class SearchRepairService
);
}
if (
$requestedAccessoryCodes === []
&& $this->isDirectProductAttributeLookup($prompt . ' ' . $primaryQuery)
) {
return $this->normalizeRepairQueries(
$this->buildDirectProductAttributeRepairQueries($prompt, $primaryQuery),
$primaryQuery
);
}
$topPrimaryName = $primaryShopResults[0]->name ?? '';
$topPrimaryProductNumber = $primaryShopResults[0]->productNumber ?? null;
$topPrimaryPhrase = trim($topPrimaryName . ' ' . ($topPrimaryProductNumber ?? ''));
@@ -259,6 +269,125 @@ final readonly class SearchRepairService
return $this->normalizeRepairQueries($queries, $primaryQuery);
}
/**
* @return string[]
*/
private function buildDirectProductAttributeRepairQueries(string $prompt, string $primaryQuery): array
{
$queries = [];
foreach ([$primaryQuery, $prompt] as $source) {
$query = $this->cleanupDirectProductAttributeRepairQuery($source);
if ($query !== '') {
$queries[] = $query;
}
}
return array_values(array_unique($queries));
}
private function cleanupDirectProductAttributeRepairQuery(string $source): string
{
$source = trim($source);
if ($source === '') {
return '';
}
$constraintTokens = $this->extractDirectProductAttributeConstraintTokens($source);
if ($constraintTokens === []) {
return '';
}
$removeTokens = array_fill_keys($constraintTokens, true);
foreach ($this->config->getDirectProductAttributeLookupStopTerms() as $term) {
foreach ($this->tokenize($term) as $token) {
$removeTokens[$token] = true;
}
}
$kept = [];
foreach ($this->tokenize($source) as $token) {
if (isset($removeTokens[$token]) || isset($kept[$token])) {
continue;
}
$kept[$token] = $token;
}
if (count($kept) < $this->config->getDirectProductAttributeLookupMinTokens()) {
return '';
}
$query = implode(' ', array_values($kept));
return $this->containsDirectProductTypeTerm($query) ? $query : '';
}
/**
* @return string[]
*/
private function extractDirectProductAttributeConstraintTokens(string $text): array
{
$tokens = [];
foreach ($this->config->getDirectProductAttributeLookupComparativeConstraintPatterns() as $pattern) {
if (@preg_match_all($pattern, $text, $matches, PREG_SET_ORDER) === false) {
continue;
}
foreach ($matches as $match) {
$value = $match['value'] ?? ($match[1] ?? '');
if (!is_scalar($value)) {
continue;
}
foreach ($this->tokenize((string) $value) as $token) {
$tokens[$token] = $token;
}
}
}
return array_values($tokens);
}
private function isDirectProductAttributeLookup(string $text): bool
{
return $this->config->isDirectProductAttributeLookupRepairEnabled()
&& $this->containsDirectProductTypeTerm($text)
&& $this->extractDirectProductAttributeConstraintTokens($text) !== [];
}
private function containsDirectProductTypeTerm(string $text): bool
{
$tokens = array_fill_keys($this->tokenize($text), true);
if ($tokens === []) {
return false;
}
foreach ($this->config->getDirectProductAttributeLookupProductTypeTerms() as $term) {
$termTokens = $this->tokenize($term);
if ($termTokens === []) {
continue;
}
$matches = true;
foreach ($termTokens as $termToken) {
if (!isset($tokens[$termToken])) {
$matches = false;
break;
}
}
if ($matches) {
return true;
}
}
return false;
}
/**
* @param string[] $queries
* @return string[]

View File

@@ -918,6 +918,40 @@ final class AgentRunnerConfig
return $this->getOptionalStringList('shop_prompt.current_input_preservation.terms');
}
public function isShopQueryProductAttributeCleanupEnabled(): bool
{
return $this->getRequiredBool('shop_prompt.product_attribute_query_cleanup.enabled');
}
public function getShopQueryProductAttributeCleanupMinTokens(): int
{
return $this->getRequiredInt('shop_prompt.product_attribute_query_cleanup.min_query_tokens_after_cleanup');
}
/**
* @return string[]
*/
public function getShopQueryProductAttributeCleanupProductTypeTerms(): array
{
return $this->getRequiredStringList('shop_prompt.product_attribute_query_cleanup.product_type_terms');
}
/**
* @return string[]
*/
public function getShopQueryProductAttributeCleanupStopTerms(): array
{
return $this->getRequiredStringList('shop_prompt.product_attribute_query_cleanup.stop_terms');
}
/**
* @return string[]
*/
public function getShopQueryProductAttributeCleanupComparativeConstraintPatterns(): array
{
return $this->getRequiredStringList('shop_prompt.product_attribute_query_cleanup.comparative_constraint_patterns');
}
public function getShopPromptIntro(): string
{
return $this->getRequiredString('shop_prompt.intro');

View File

@@ -670,6 +670,22 @@ final class PromptBuilderConfig
return $this->getRequiredString('measurement_evidence_guard.generic_safe_no_accessory_evidence_answer_template_de');
}
/**
* @return string[]
*/
public function getMeasurementEvidenceAccessoryLookupGuardTerms(): array
{
return $this->getRequiredStringList('measurement_evidence_guard.accessory_lookup_guard_terms');
}
/**
* @return string[]
*/
public function getMeasurementEvidenceAccessoryLookupPassthroughTerms(): array
{
return $this->getRequiredStringList('measurement_evidence_guard.accessory_lookup_passthrough_terms');
}
public function getMeasurementEvidenceRuleTemplate(string $key): string
{
return $this->getRequiredString('measurement_evidence_guard.rule_templates.' . $key);

View File

@@ -845,6 +845,13 @@ final readonly class RetriexEffectiveConfigProvider
'min_primary_results_without_repair' => $this->searchRepairConfig->getMinPrimaryResultsWithoutRepair(),
'strict_requested_accessory_code_repair' => $this->searchRepairConfig->shouldRestrictRequestedAccessoryCodeRepair(),
'prefer_prompt_anchored_model_for_requested_accessory_code' => $this->searchRepairConfig->shouldPreferPromptAnchoredModelForRequestedAccessoryCode(),
'direct_product_attribute_lookup' => [
'enabled' => $this->searchRepairConfig->isDirectProductAttributeLookupRepairEnabled(),
'min_query_tokens_after_cleanup' => $this->searchRepairConfig->getDirectProductAttributeLookupMinTokens(),
'product_type_terms' => $this->searchRepairConfig->getDirectProductAttributeLookupProductTypeTerms(),
'stop_terms' => $this->searchRepairConfig->getDirectProductAttributeLookupStopTerms(),
'comparative_constraint_patterns' => $this->searchRepairConfig->getDirectProductAttributeLookupComparativeConstraintPatterns(),
],
'requested_accessory_code_fallback_query_templates' => $this->searchRepairConfig->getRequestedAccessoryCodeFallbackQueryTemplates(),
'requested_accessory_code_fallback_terms' => $this->searchRepairConfig->getRequestedAccessoryCodeFallbackTerms(),
'requested_accessory_code_context_prefix_terms' => $this->searchRepairConfig->getRequestedAccessoryCodeContextPrefixTerms(),

View File

@@ -50,6 +50,37 @@ final class SearchRepairConfig
return $this->requiredBool('prefer_prompt_anchored_model_for_requested_accessory_code');
}
public function isDirectProductAttributeLookupRepairEnabled(): bool
{
return $this->requiredBool('direct_product_attribute_lookup.enabled');
}
public function getDirectProductAttributeLookupMinTokens(): int
{
return $this->requiredPositiveInt('direct_product_attribute_lookup.min_query_tokens_after_cleanup');
}
/** @return string[] */
public function getDirectProductAttributeLookupProductTypeTerms(): array
{
return $this->configOrVocabularyStringList(
'direct_product_attribute_lookup.product_type_terms',
'search_repair.direct_product_type_terms'
);
}
/** @return string[] */
public function getDirectProductAttributeLookupStopTerms(): array
{
return $this->requiredStringList('direct_product_attribute_lookup.stop_terms');
}
/** @return string[] */
public function getDirectProductAttributeLookupComparativeConstraintPatterns(): array
{
return $this->requiredStringList('direct_product_attribute_lookup.comparative_constraint_patterns');
}
/** @return string[] */
public function getRequestedAccessoryCodeFallbackQueryTemplates(): array
{