optimize technical truth

This commit is contained in:
team 1
2026-04-29 17:42:37 +02:00
parent 06f192b28a
commit 19c0f612dc
10 changed files with 454 additions and 307 deletions

View File

@@ -53,14 +53,8 @@ final readonly class AgentRunner
$primaryShopResults = [];
$knowledgeChunks = [];
$sources = [];
$urlContent = '';
$optimizedShopQuery = '';
$shopSearchQuery = '';
$shopSearchDisplayQuery = '';
$shopSearchUsedOptimizedQuery = false;
$commerceIntent = CommerceIntentLite::NONE;
$knowledgeRetrievalPrompt = $prompt;
$usedFollowUpRetrievalContext = false;
$commerceHistoryContext = '';
$attemptedShopRepair = false;
$usedShopRepair = false;
@@ -311,7 +305,7 @@ final readonly class AgentRunner
$usedShopRepair = $repairPayload['usedRepair'];
$shopRepairQueries = $repairPayload['repairQueries'];
if ($shopSearchQuery !== '' && !$primaryShopSearchHadSystemFailure) {
if (!$primaryShopSearchHadSystemFailure) {
yield $this->systemMsg(
$this->buildShopSearchMetaMessage(
query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery,
@@ -757,7 +751,7 @@ final readonly class AgentRunner
return '';
}
$value = $this->sanitizeHistoryQuestion((string) ($matches[0] ?? ''));
$value = $this->sanitizeHistoryQuestion(($matches[0] ?? ''));
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
return trim(str_replace('®', '', $value));
@@ -769,7 +763,7 @@ final readonly class AgentRunner
return '';
}
$value = preg_replace('/\s+/u', ' ', (string) ($matches[0] ?? '')) ?? '';
$value = preg_replace('/\s+/u', ' ', ($matches[0] ?? '')) ?? '';
return trim($value);
}
@@ -1820,6 +1814,10 @@ final readonly class AgentRunner
private function formatProductionUiSourceLabels(array $sourceLabels): array
{
$labels = [];
$seen = [];
$shopSystemKey = $this->canonicalProductionUiSourceLabelKey(
$this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())
);
foreach ($sourceLabels as $label) {
// Source labels are stored as badge HTML for the legacy "Genutzte Quellen" line.
@@ -1831,18 +1829,35 @@ final readonly class AgentRunner
continue;
}
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
$key = $this->canonicalProductionUiSourceLabelKey($label);
if ($key === $shopSystemKey || $key === 'liveshopdaten') {
$label = 'Live-Shopdaten';
$key = 'liveshopdaten';
}
if (!in_array($label, $labels, true)) {
$labels[] = $label;
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$labels[] = $label;
}
return $labels;
}
private function canonicalProductionUiSourceLabelKey(string $label): string
{
$label = html_entity_decode(strip_tags($label), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$label = str_replace(["\xc2\xa0", '', '', '', '', '—'], [' ', '-', '-', '-', '-', '-'], $label);
$label = preg_replace('/\s+/u', ' ', $label) ?? $label;
$label = mb_strtolower(trim($label), 'UTF-8');
$label = preg_replace('/[^\p{L}\p{N}]+/u', '', $label) ?? $label;
return $label;
}
/**
* @param ShopProductResult[] $shopResults
*/
@@ -1863,7 +1878,7 @@ final readonly class AgentRunner
}
$html = '<div class="retriex-meta-card retriex-product-results" data-retriex-meta-id="shop-results" data-retriex-meta-state="completed">'
. '<div class="retriex-meta-card__eyebrow">Live-Shopdaten</div>'
. '<div class="retriex-meta-card__eyebrow">Shop-Ergebnisse</div>'
. '<div class="retriex-meta-card__title">Shop-Ergebnisse</div>'
. '<div class="retriex-product-results__summary">' . htmlspecialchars($summary, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
@@ -2053,7 +2068,7 @@ final readonly class AgentRunner
. '" data-retriex-shop-result-count="'
. htmlspecialchars($resultCountAttribute, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '">'
. '<div class="retriex-meta-card__eyebrow">Live-Shopdaten</div>'
. '<div class="retriex-meta-card__eyebrow">Shop-Suche</div>'
. '<div class="retriex-meta-card__title">' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__body">'
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($resultLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'

View File

@@ -52,6 +52,7 @@ final readonly class PromptBuilder
$hasKnowledge = $knowledgeChunks !== [] || $urlContent !== '';
$isTechnicalProductQuestion = $this->isLikelyTechnicalProductQuestion($prompt);
$asksForAccessoryOrBundle = $this->asksForAccessoryOrBundle($prompt);
$requestedProductRole = $this->resolveRequestedProductRole($prompt);
$reliabilityState = $this->resolveReliabilityState(
hasKnowledge: $hasKnowledge,
hasShopResults: $hasShopResults,
@@ -60,13 +61,13 @@ final readonly class PromptBuilder
);
$systemBlock = $this->buildSystemBlock();
$shopBlock = $this->buildShopBlock($prompt, $shopResults, $swagFullOutPut);
$productRoleGuardBlock = $this->buildProductRoleGuardBlock($prompt, $shopResults);
$shopBlock = $this->buildShopBlock($prompt, $shopResults, $swagFullOutPut, $requestedProductRole);
$measurementEvidenceBlock = $this->buildMeasurementEvidenceBlock(
prompt: $prompt,
knowledgeChunks: $knowledgeChunks,
urlContent: $urlContent,
shopResults: $shopResults
shopResults: $shopResults,
requestedRole: $requestedProductRole
);
$outputPriorityBlock = $this->buildOutputPriorityBlock(
hasShopResults: $hasShopResults,
@@ -95,7 +96,6 @@ final readonly class PromptBuilder
$fixedPrompt = $this->implodeBlocks([
$systemBlock,
$shopBlock,
$productRoleGuardBlock,
$measurementEvidenceBlock,
$outputPriorityBlock,
$fallbackEscalationBlock,
@@ -113,7 +113,6 @@ final readonly class PromptBuilder
return $this->implodeBlocks([
$systemBlock,
$shopBlock,
$productRoleGuardBlock,
$measurementEvidenceBlock,
$outputPriorityBlock,
$fallbackEscalationBlock,
@@ -189,7 +188,7 @@ final readonly class PromptBuilder
* Shop data is the most current source for commercial details.
* It should not override technical matching logic.
*/
private function buildShopBlock(string $prompt, array $shopResults, ?string $swagFullOutPut): string
private function buildShopBlock(string $prompt, array $shopResults, ?string $swagFullOutPut, ?string $requestedProductRole = null): string
{
$parts = [];
@@ -213,7 +212,7 @@ final readonly class PromptBuilder
$totalCount = count($normalizedShopResults);
$limitedShopResults = array_slice($normalizedShopResults, 0, $this->config->getMaxShopResultsInPrompt());
$isDetailed = count($limitedShopResults) <= $this->config->getDetailedShopResultsMaxCount();
$requestedRole = $this->resolveRequestedProductRole($prompt);
$requestedRole = $requestedProductRole ?? $this->resolveRequestedProductRole($prompt);
$measurementGuard = $this->resolveRequestedMeasurementGuard($prompt);
$lines = [];
@@ -507,7 +506,7 @@ final readonly class PromptBuilder
}
}
$measurementEvidenceLine = $this->buildShopMeasurementEvidenceLine($product, $measurementGuard, $requestedRole);
$measurementEvidenceLine = $this->buildShopMeasurementEvidenceLine($product, $measurementGuard);
if ($measurementEvidenceLine !== '') {
$entryParts[] = $measurementEvidenceLine;
}
@@ -517,9 +516,6 @@ final readonly class PromptBuilder
if ($suppressCommercialFields) {
$entryParts[] = $this->config->getShopRoleIncompatibleCommercialSuppressionNote();
$entryParts[] = $this->config->getProductRoleGuardIncompatibleRecordNote();
return implode("\n", $entryParts);
}
if (!$suppressCommercialFields && $product->productNumber) {
@@ -544,11 +540,13 @@ final readonly class PromptBuilder
: $this->config->getShopAvailabilityNoLabel());
}
foreach ($product->highlights as $highlight) {
$highlight = $this->normalizeBlockText((string) $highlight);
if (!$suppressCommercialFields) {
foreach ($product->highlights as $highlight) {
$highlight = $this->normalizeBlockText((string) $highlight);
if ($highlight !== '') {
$entryParts[] = $this->config->getShopHighlightPrefix() . $highlight;
if ($highlight !== '') {
$entryParts[] = $this->config->getShopHighlightPrefix() . $highlight;
}
}
}
@@ -575,70 +573,6 @@ final readonly class PromptBuilder
return implode("\n", $entryParts);
}
/**
* @param ShopProductResult[] $shopResults
*/
private function buildProductRoleGuardBlock(string $prompt, array $shopResults): string
{
$requestedRole = $this->resolveRequestedProductRole($prompt);
if ($requestedRole !== 'main_device') {
return '';
}
$records = array_values(array_filter(
$shopResults,
static fn(mixed $product): bool => $product instanceof ShopProductResult
));
if ($records === []) {
return '';
}
$compatibleMainDeviceRecords = [];
$incompatibleAccessoryRecords = [];
$unknownRecords = [];
foreach ($records as $index => $product) {
$inferredRole = $this->resolveShopProductRole($product);
$roleCompatibility = $this->resolveShopRoleCompatibility($requestedRole, $inferredRole);
$name = $this->normalizeBlockText($product->name);
$label = sprintf('record %d%s', $index + 1, $name !== '' ? ' (' . $name . ')' : '');
if ($roleCompatibility === 'compatible' && $inferredRole === 'main_device') {
$compatibleMainDeviceRecords[] = $label;
continue;
}
if ($roleCompatibility === 'incompatible_accessory_for_main_device_request') {
$incompatibleAccessoryRecords[] = $label;
continue;
}
$unknownRecords[] = $label . ' role=' . $inferredRole . ' compatibility=' . $roleCompatibility;
}
$rules = $this->config->getProductRoleGuardMainDeviceRules();
$rules[] = '- Requested product role resolved from the user question: main_device.';
$rules[] = '- Compatible main-device shop records: ' . ($compatibleMainDeviceRecords !== [] ? implode('; ', $compatibleMainDeviceRecords) : 'none') . '.';
$rules[] = '- Role-incompatible accessory/consumable shop records: ' . ($incompatibleAccessoryRecords !== [] ? implode('; ', $incompatibleAccessoryRecords) : 'none') . '.';
if ($unknownRecords !== []) {
$rules[] = '- Unknown or ambiguous shop records must be kept separate and must not be upgraded into a main-device recommendation: ' . implode('; ', $unknownRecords) . '.';
}
if ($compatibleMainDeviceRecords === [] && $incompatibleAccessoryRecords !== []) {
$rules[] = '- Mandatory answer behavior: ' . $this->config->getProductRoleGuardNoMainDeviceTemplate();
$rules[] = '- Start with the no-match finding for the requested main device. Do not start with an accessory product name.';
$rules[] = '- If mentioning incompatible shop hits, use a short separate section like "Nur Zubehörtreffer gefunden" and do not include price, availability, URL, product number, or recommendation wording for those records.';
}
return $this->buildRuleBlock(
$this->config->getProductRoleGuardSectionLabel(),
$rules
);
}
/**
* @param string[] $knowledgeChunks
* @param ShopProductResult[] $shopResults
@@ -647,14 +581,9 @@ final readonly class PromptBuilder
string $prompt,
array $knowledgeChunks,
string $urlContent,
array $shopResults
array $shopResults,
?string $requestedRole = null
): string {
$requestedRole = $this->resolveRequestedProductRole($prompt);
if ($requestedRole === 'accessory_or_consumable') {
return '';
}
$guard = $this->resolveRequestedMeasurementGuard($prompt);
if ($guard === null) {
@@ -662,14 +591,19 @@ final readonly class PromptBuilder
}
$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'] ?? 'requested measurement parameter'));
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) ($guard['safe_no_evidence_answer_de'] ?? ''));
$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'] ?? '')
: ($guard['safe_no_evidence_answer_de'] ?? '')
));
$knowledgeText = $this->normalizeBlockText(implode("\n\n", array_map('strval', $knowledgeChunks)) . "\n\n" . $urlContent);
$knowledgeHasEvidence = $this->containsMeasurementPositiveEvidence($knowledgeText, $positiveTerms);
$knowledgeHasOnlyWeakMention = !$knowledgeHasEvidence
&& $this->containsAnyMeasurementWeakMention($knowledgeText, $guard);
$knowledgeHasEvidence = $this->containsMeasurementPositiveEvidence($knowledgeText, $positiveTerms, $positiveContextTerms, $negativeContextTerms);
$shopEvidenceLines = [];
$shopHasEvidence = false;
@@ -679,7 +613,7 @@ final readonly class PromptBuilder
continue;
}
$hasEvidence = $this->shopProductHasMeasurementEvidence($product, $positiveTerms);
$hasEvidence = $this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms);
$productName = $this->normalizeBlockText($product->name);
if ($hasEvidence) {
@@ -702,16 +636,19 @@ final readonly class PromptBuilder
$rules = $this->config->getMeasurementEvidenceIntroRules();
$rules[] = '- User requested measurement parameter: ' . $label . '.';
$rules[] = '- Positive evidence terms that count for this request: ' . implode(', ', $positiveTerms) . '.';
$rules[] = '- Positive parameter terms for this request: ' . implode(', ', $positiveTerms) . '.';
if ($positiveContextTerms !== []) {
$rules[] = '- These parameter terms count as suitability evidence only in a measurement-purpose context such as: ' . implode(', ', $positiveContextTerms) . '.';
}
if ($negativeContextTerms !== []) {
$rules[] = '- These contexts are not suitability evidence by themselves: ' . implode(', ', $negativeContextTerms) . '.';
}
if ($nonEquivalentTerms !== []) {
$rules[] = '- Terms that must NOT be treated as equivalent positive evidence: ' . implode(', ', $nonEquivalentTerms) . '.';
}
$rules[] = '- RAG/URL evidence scan for this exact measurement capability: ' . ($knowledgeHasEvidence ? 'explicit positive capability evidence found.' : 'no explicit positive capability evidence found.');
if ($knowledgeHasOnlyWeakMention) {
$rules[] = '- RAG/URL weak mention scan: the parameter is mentioned, but only as a weak/non-capability mention. Do not use this as suitability evidence.';
}
$rules[] = '- RAG/URL evidence scan for this exact parameter: ' . ($knowledgeHasEvidence ? 'explicit positive evidence found.' : 'no explicit positive evidence found.');
$rules = array_merge($rules, $shopEvidenceLines);
if (!$knowledgeHasEvidence && !$shopHasEvidence) {
@@ -719,11 +656,14 @@ final readonly class PromptBuilder
if ($safeNoEvidenceAnswer !== '') {
$rules[] = '- Start the answer with this meaning in the user language: ' . $safeNoEvidenceAnswer;
}
$rules[] = '- You may list exact shop hits only as commercial/search hits under a heading such as "Shop-Treffer (technische Eignung nicht sicher belegt)".';
if ($resolvedRequestedRole === 'accessory_or_consumable') {
$rules[] = '- Do not recommend accessories for a different measurement parameter just because they are accessories. If only accessories for other parameters are present, say that only non-matching accessory hits were found.';
} else {
$rules[] = '- You may list exact shop hits only as commercial/search hits under a heading such as "Shop-Treffer (technische Eignung nicht sicher belegt)".';
}
}
$rules[] = '- Do not output measurement ranges, methods, application areas, advantages, or alternative suitable models unless the same source record contains explicit positive capability evidence for the requested measurement parameter.';
$rules[] = '- Do not list products as relevant just because the requested parameter appears in an operating range, reagent/indicator property, output-transfer field, metadata, or generic mention.';
$rules[] = '- Do not output measurement ranges, methods, application areas, advantages, or alternative suitable models unless the same source record contains explicit positive evidence for the requested measurement parameter.';
$rules[] = '- The generated shop search query, search intent, ranking position, and user question are not factual evidence for product suitability.';
return $this->buildRuleBlock(
@@ -732,20 +672,22 @@ final readonly class PromptBuilder
);
}
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard, string $requestedRole = 'unknown'): string
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard): string
{
if ($guard === null || $requestedRole === 'accessory_or_consumable') {
if ($guard === null) {
return '';
}
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
if ($positiveTerms === []) {
return '';
}
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms)) {
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
return sprintf(
'Requested measurement evidence: explicit positive evidence for %s is present in this same SHOP PRODUCT RECORD.',
$label
@@ -803,36 +745,86 @@ final readonly class PromptBuilder
/**
* @param string[] $positiveTerms
* @param string[] $positiveContextTerms
* @param string[] $negativeContextTerms
*/
private function shopProductHasMeasurementEvidence(ShopProductResult $product, array $positiveTerms): bool
{
return $this->containsMeasurementPositiveEvidence(
$this->buildShopProductEvidenceText($product),
$positiveTerms
);
private function shopProductHasMeasurementEvidence(
ShopProductResult $product,
array $positiveTerms,
array $positiveContextTerms,
array $negativeContextTerms
): bool {
foreach ($this->buildShopProductEvidenceFragments($product) as $fragment) {
if ($this->containsMeasurementPositiveEvidence($fragment, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
return true;
}
}
return false;
}
private function buildShopProductEvidenceText(ShopProductResult $product): string
/**
* @return string[]
*/
private function buildShopProductEvidenceFragments(ShopProductResult $product): array
{
return $this->normalizeBlockText(implode(' ', array_filter([
$fragments = array_filter([
$product->name,
$product->productNumber,
$product->manufacturer,
$product->url,
implode(' ', array_map('strval', $product->highlights)),
$product->description,
$product->customFields,
$product->url,
], static fn($value): bool => is_scalar($value) && trim((string) $value) !== '')));
], static fn($value): bool => is_scalar($value) && trim((string) $value) !== '');
$out = [];
foreach ($fragments as $fragment) {
foreach ($this->splitMeasurementEvidenceFragments((string) $fragment) as $part) {
if ($part !== '') {
$out[] = $part;
}
}
}
return $out;
}
/**
* @param string[] $positiveTerms
* @param string[] $positiveContextTerms
* @param string[] $negativeContextTerms
*/
private function containsMeasurementPositiveEvidence(string $text, array $positiveTerms): bool
{
$normalizedText = $this->normalizeForMeasurementMatching($text);
private function containsMeasurementPositiveEvidence(
string $text,
array $positiveTerms,
array $positiveContextTerms,
array $negativeContextTerms
): bool {
foreach ($this->splitMeasurementEvidenceFragments($text) as $fragment) {
$normalizedFragment = $this->normalizeForMeasurementMatching($fragment);
foreach ($positiveTerms as $term) {
if ($normalizedFragment === '' || !$this->containsAnyMeasurementTerm($normalizedFragment, $positiveTerms)) {
continue;
}
if ($negativeContextTerms !== [] && $this->containsAnyMeasurementTerm($normalizedFragment, $negativeContextTerms)) {
continue;
}
if ($positiveContextTerms === [] || $this->containsAnyMeasurementTerm($normalizedFragment, $positiveContextTerms)) {
return true;
}
}
return false;
}
/**
* @param string[] $terms
*/
private function containsAnyMeasurementTerm(string $normalizedText, array $terms): bool
{
foreach ($terms as $term) {
if ($this->containsMeasurementTerm($normalizedText, $term)) {
return true;
}
@@ -842,29 +834,21 @@ final readonly class PromptBuilder
}
/**
* Returns true when a measurement parameter is mentioned only as request wording or as a known non-equivalent/weak term.
* This is used for prompt diagnostics and must not be treated as positive suitability evidence.
* @return string[]
*/
private function containsAnyMeasurementWeakMention(string $text, array $guard): bool
private function splitMeasurementEvidenceFragments(string $text): array
{
$weakTerms = array_merge(
$this->extractMeasurementGuardStringList($guard, 'request_terms'),
$this->extractMeasurementGuardStringList($guard, 'non_equivalent_terms')
);
if ($weakTerms === []) {
return false;
$text = $this->normalizeBlockText($text);
if ($text === '') {
return [];
}
$normalizedText = $this->normalizeForMeasurementMatching($text);
$parts = preg_split('/[\n.;|]+/u', $text) ?: [$text];
foreach ($weakTerms as $term) {
if ($this->containsMeasurementTerm($normalizedText, $term)) {
return true;
}
}
return false;
return array_values(array_filter(
array_map(fn(string $part): string => $this->normalizeBlockText($part), $parts),
static fn(string $part): bool => $part !== ''
));
}
private function containsMeasurementTerm(string $normalizedText, string $term): bool
@@ -945,21 +929,42 @@ final readonly class PromptBuilder
private function resolveRequestedProductRole(string $prompt): string
{
$normalized = mb_strtolower($prompt, 'UTF-8');
$asksForAccessory = $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryRequestKeywords())
|| $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryProductRoleKeywords());
$normalized = mb_strtolower($this->normalizeBlockText($prompt), 'UTF-8');
$hasAccessoryIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryProductRoleKeywords());
$hasMainDeviceIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getMainDeviceRequestRoleKeywords());
if ($asksForAccessory) {
if ($hasAccessoryIntent && !$this->hasDirectMainDeviceRequest($normalized)) {
return 'accessory_or_consumable';
}
if ($this->containsAnyPromptKeyword($normalized, $this->config->getMainDeviceRequestRoleKeywords())) {
if ($hasMainDeviceIntent) {
return 'main_device';
}
if ($hasAccessoryIntent) {
return 'accessory_or_consumable';
}
return 'unknown';
}
private function hasDirectMainDeviceRequest(string $normalizedPrompt): bool
{
$patterns = [
'/\b(welcher|welches|welche)\s+[^?.!,;]{0,40}(testomat|messgerät|messgeraet|analysegerät|analysegeraet|gerät|geraet|analysator)\b/u',
'/\b(suche|finde|empfiehl|empfehle)\s+[^?.!,;]{0,40}(testomat|messgerät|messgeraet|analysegerät|analysegeraet|gerät|geraet|analysator)\b/u',
'/\b(testomat|messgerät|messgeraet|analysegerät|analysegeraet|gerät|geraet|analysator)\s+[^?.!,;]{0,40}(messen|misst|überwachen|ueberwachen|kann|für|fuer)\b/u',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $normalizedPrompt) === 1) {
return true;
}
}
return false;
}
private function resolveShopProductRole(ShopProductResult $product): string
{
$primaryRole = $this->resolveShopPrimaryProductRole($product);
@@ -1113,4 +1118,4 @@ final readonly class PromptBuilder
{
return max($min, min($max, $value));
}
}
}