optimize technical truth
This commit is contained in:
@@ -1427,6 +1427,7 @@ final readonly class AgentRunner
|
||||
|
||||
if ($hasShopResults) {
|
||||
return $this->buildNoLlmShopFallbackAnswer(
|
||||
prompt: $prompt,
|
||||
hasKnowledge: $hasKnowledge,
|
||||
shopResults: $shopResults
|
||||
);
|
||||
@@ -1453,15 +1454,22 @@ final readonly class AgentRunner
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
*/
|
||||
private function buildNoLlmShopFallbackAnswer(bool $hasKnowledge, array $shopResults): string
|
||||
private function buildNoLlmShopFallbackAnswer(string $prompt, bool $hasKnowledge, array $shopResults): string
|
||||
{
|
||||
$intro = $hasKnowledge
|
||||
? $this->agentRunnerConfig->getNoLlmFallbackShopWithKnowledgeMessage()
|
||||
: $this->agentRunnerConfig->getNoLlmFallbackShopOnlyMessage();
|
||||
$requestedProductRole = $this->resolveNoLlmRequestedProductRole($prompt);
|
||||
|
||||
$lines = [$intro, ''];
|
||||
$lines = [$intro];
|
||||
|
||||
foreach ($this->buildNoLlmShopProductLines($shopResults) as $line) {
|
||||
if ($this->hasOnlyNoLlmAccessoryResultsForMainDeviceRequest($requestedProductRole, $shopResults)) {
|
||||
$lines[] = $this->agentRunnerConfig->getNoLlmFallbackAccessoryOnlyForMainDeviceMessage();
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
|
||||
foreach ($this->buildNoLlmShopProductLines($shopResults, $requestedProductRole) as $line) {
|
||||
$lines[] = $line;
|
||||
}
|
||||
|
||||
@@ -1499,7 +1507,7 @@ final readonly class AgentRunner
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildNoLlmShopProductLines(array $shopResults): array
|
||||
private function buildNoLlmShopProductLines(array $shopResults, string $requestedProductRole): array
|
||||
{
|
||||
$maxResults = max(1, $this->agentRunnerConfig->getNoLlmFallbackMaxShopResults());
|
||||
$lines = [];
|
||||
@@ -1510,7 +1518,7 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = $this->formatNoLlmShopProductLine($product, $index);
|
||||
$lines[] = $this->formatNoLlmShopProductLine($product, $index, $requestedProductRole);
|
||||
$index++;
|
||||
|
||||
if (count($lines) >= $maxResults) {
|
||||
@@ -1525,9 +1533,10 @@ final readonly class AgentRunner
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function formatNoLlmShopProductLine(ShopProductResult $product, int $index): string
|
||||
private function formatNoLlmShopProductLine(ShopProductResult $product, int $index, string $requestedProductRole): string
|
||||
{
|
||||
$parts = [];
|
||||
$productRole = $this->resolveNoLlmShopProductRole($product);
|
||||
|
||||
$name = $this->normalizeOneLine($product->name);
|
||||
$parts[] = $name !== '' ? $name : 'Unbenanntes Shop-Produkt';
|
||||
@@ -1552,9 +1561,86 @@ final readonly class AgentRunner
|
||||
$parts[] = 'URL: ' . $this->normalizeOneLine($product->url);
|
||||
}
|
||||
|
||||
if ($requestedProductRole === 'main_device_or_system' && $productRole === 'accessory_or_consumable') {
|
||||
$parts[] = 'Hinweis: Zubehör/Verbrauchsartikel; nicht als Messanlage/Gerät bestätigt';
|
||||
}
|
||||
|
||||
return sprintf('%d. %s', $index, implode(' | ', $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
*/
|
||||
private function hasOnlyNoLlmAccessoryResultsForMainDeviceRequest(string $requestedProductRole, array $shopResults): bool
|
||||
{
|
||||
if ($requestedProductRole !== 'main_device_or_system') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$seenProducts = 0;
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seenProducts++;
|
||||
|
||||
if ($this->resolveNoLlmShopProductRole($product) !== 'accessory_or_consumable') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $seenProducts > 0;
|
||||
}
|
||||
|
||||
private function resolveNoLlmRequestedProductRole(string $prompt): string
|
||||
{
|
||||
$normalized = mb_strtolower($prompt, 'UTF-8');
|
||||
|
||||
if ($this->containsAnyConfiguredTerm($normalized, $this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords())) {
|
||||
return 'accessory_or_consumable';
|
||||
}
|
||||
|
||||
if ($this->containsAnyConfiguredTerm($normalized, $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords())) {
|
||||
return 'main_device_or_system';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
private function resolveNoLlmShopProductRole(ShopProductResult $product): string
|
||||
{
|
||||
$normalized = mb_strtolower($this->normalizeOneLine(implode(' ', [
|
||||
$product->name,
|
||||
(string) $product->description,
|
||||
(string) $product->customFields,
|
||||
implode(' ', $product->highlights),
|
||||
])), 'UTF-8');
|
||||
|
||||
if ($this->containsAnyConfiguredTerm($normalized, $this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords())) {
|
||||
return 'accessory_or_consumable';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $terms
|
||||
*/
|
||||
private function containsAnyConfiguredTerm(string $haystack, array $terms): bool
|
||||
{
|
||||
foreach ($terms as $term) {
|
||||
$term = mb_strtolower(trim($term), 'UTF-8');
|
||||
|
||||
if ($term !== '' && str_contains($haystack, $term)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string[] $sources
|
||||
|
||||
@@ -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,7 +61,11 @@ final readonly class PromptBuilder
|
||||
);
|
||||
|
||||
$systemBlock = $this->buildSystemBlock();
|
||||
$shopBlock = $this->buildShopBlock($shopResults, $swagFullOutPut);
|
||||
$shopBlock = $this->buildShopBlock(
|
||||
shopResults: $shopResults,
|
||||
swagFullOutPut: $swagFullOutPut,
|
||||
requestedProductRole: $requestedProductRole
|
||||
);
|
||||
$outputPriorityBlock = $this->buildOutputPriorityBlock(
|
||||
hasShopResults: $hasShopResults,
|
||||
isTechnicalProductQuestion: $isTechnicalProductQuestion
|
||||
@@ -178,7 +183,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(array $shopResults, ?string $swagFullOutPut): string
|
||||
private function buildShopBlock(array $shopResults, ?string $swagFullOutPut, string $requestedProductRole): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
@@ -208,7 +213,8 @@ final readonly class PromptBuilder
|
||||
$lines[] = $this->buildShopProductEntry(
|
||||
product: $product,
|
||||
index: $i + 1,
|
||||
isDetailed: $isDetailed
|
||||
isDetailed: $isDetailed,
|
||||
requestedProductRole: $requestedProductRole
|
||||
);
|
||||
}
|
||||
|
||||
@@ -464,12 +470,34 @@ final readonly class PromptBuilder
|
||||
return $rules;
|
||||
}
|
||||
|
||||
private function buildShopProductEntry(ShopProductResult $product, int $index, bool $isDetailed): string
|
||||
{
|
||||
private function buildShopProductEntry(
|
||||
ShopProductResult $product,
|
||||
int $index,
|
||||
bool $isDetailed,
|
||||
string $requestedProductRole
|
||||
): string {
|
||||
$productName = $this->normalizeBlockText($product->name);
|
||||
$productRole = $this->resolveShopProductRole($product);
|
||||
$roleCompatibility = $this->resolveShopProductRoleCompatibility($requestedProductRole, $productRole);
|
||||
$isMainDeviceRequestAccessoryMismatch = $requestedProductRole === 'main_device_or_system'
|
||||
&& $productRole === 'accessory_or_consumable';
|
||||
|
||||
$entryParts = [
|
||||
"[{$index}] " . $this->normalizeBlockText($product->name),
|
||||
sprintf($this->config->getShopRecordHeaderTemplate(), $index),
|
||||
$this->config->getShopExactProductNameLabel() . ': ' . $productName,
|
||||
$this->config->getShopRequestedRoleLabel() . ': ' . $requestedProductRole,
|
||||
$this->config->getShopInferredRoleLabel() . ': ' . $productRole,
|
||||
$this->config->getShopRoleCompatibilityLabel() . ': ' . $roleCompatibility,
|
||||
];
|
||||
|
||||
foreach ($this->config->getShopAtomicRecordNoteLines() as $noteLine) {
|
||||
$noteLine = $this->normalizeBlockText($noteLine);
|
||||
|
||||
if ($noteLine !== '') {
|
||||
$entryParts[] = $noteLine;
|
||||
}
|
||||
}
|
||||
|
||||
if ($product->productNumber) {
|
||||
$entryParts[] = $this->config->getShopProductNumberLabel() . ': '
|
||||
. $this->normalizeBlockText($product->productNumber);
|
||||
@@ -492,12 +520,16 @@ final readonly class PromptBuilder
|
||||
: $this->config->getShopAvailabilityNoLabel());
|
||||
}
|
||||
|
||||
foreach ($product->highlights as $highlight) {
|
||||
$highlight = $this->normalizeBlockText((string) $highlight);
|
||||
if (!$isMainDeviceRequestAccessoryMismatch) {
|
||||
foreach ($product->highlights as $highlight) {
|
||||
$highlight = $this->normalizeBlockText((string) $highlight);
|
||||
|
||||
if ($highlight !== '') {
|
||||
$entryParts[] = $this->config->getShopHighlightPrefix() . $highlight;
|
||||
if ($highlight !== '') {
|
||||
$entryParts[] = $this->config->getShopHighlightPrefix() . $highlight;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$entryParts[] = $this->config->getShopRoleMismatchNotice();
|
||||
}
|
||||
|
||||
if ($product->url) {
|
||||
@@ -510,12 +542,12 @@ final readonly class PromptBuilder
|
||||
. $this->normalizeBlockText($product->productImage);
|
||||
}
|
||||
|
||||
if ($isDetailed && $product->description) {
|
||||
if (!$isMainDeviceRequestAccessoryMismatch && $isDetailed && $product->description) {
|
||||
$entryParts[] = $this->config->getShopDescriptionLabel() . ': '
|
||||
. $this->normalizeBlockText($product->description);
|
||||
}
|
||||
|
||||
if ($product->customFields) {
|
||||
if (!$isMainDeviceRequestAccessoryMismatch && $product->customFields) {
|
||||
$entryParts[] = $this->config->getShopMetaInformationLabel() . ': '
|
||||
. $this->normalizeBlockText($product->customFields);
|
||||
}
|
||||
@@ -596,6 +628,81 @@ final readonly class PromptBuilder
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
private function resolveRequestedProductRole(string $prompt): string
|
||||
{
|
||||
$normalized = mb_strtolower($prompt, 'UTF-8');
|
||||
|
||||
if ($this->containsAnyConfiguredTerm($normalized, $this->config->getAccessoryProductRoleKeywords())) {
|
||||
return 'accessory_or_consumable';
|
||||
}
|
||||
|
||||
if ($this->containsAnyConfiguredTerm($normalized, $this->config->getMainDeviceRequestRoleKeywords())) {
|
||||
return 'main_device_or_system';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
private function resolveShopProductRole(ShopProductResult $product): string
|
||||
{
|
||||
$text = mb_strtolower($this->implodeLines([
|
||||
$product->name,
|
||||
(string) $product->description,
|
||||
(string) $product->customFields,
|
||||
implode(' ', $product->highlights),
|
||||
]), 'UTF-8');
|
||||
|
||||
// Accessory/consumable wins over broad device-family words such as "Testomat".
|
||||
// Example: "Testomat Indikator TH2005" must stay an indicator, not a main device.
|
||||
if ($this->containsAnyConfiguredTerm($text, $this->config->getAccessoryProductRoleKeywords())) {
|
||||
return 'accessory_or_consumable';
|
||||
}
|
||||
|
||||
if ($this->containsAnyConfiguredTerm($text, $this->config->getMainDeviceProductRoleKeywords())) {
|
||||
return 'main_device_or_system';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
private function resolveShopProductRoleCompatibility(string $requestedProductRole, string $shopProductRole): string
|
||||
{
|
||||
if ($requestedProductRole === 'unknown' || $shopProductRole === 'unknown') {
|
||||
return 'unknown - do not use as primary product unless suitability is explicit in the same record';
|
||||
}
|
||||
|
||||
if ($requestedProductRole === $shopProductRole) {
|
||||
return 'compatible';
|
||||
}
|
||||
|
||||
if ($requestedProductRole === 'main_device_or_system' && $shopProductRole === 'accessory_or_consumable') {
|
||||
return 'not compatible - user asked for a main device/system, this shop record is accessory/consumable';
|
||||
}
|
||||
|
||||
if ($requestedProductRole === 'accessory_or_consumable' && $shopProductRole === 'main_device_or_system') {
|
||||
return 'not compatible - user asked for accessory/consumable, this shop record is a main device/system';
|
||||
}
|
||||
|
||||
return 'unknown - keep separate from the main answer';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $terms
|
||||
*/
|
||||
private function containsAnyConfiguredTerm(string $haystack, array $terms): bool
|
||||
{
|
||||
foreach ($terms as $term) {
|
||||
$term = mb_strtolower(trim($term), 'UTF-8');
|
||||
|
||||
if ($term !== '' && str_contains($haystack, $term)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isLikelyTechnicalProductQuestion(string $prompt): bool
|
||||
{
|
||||
$normalized = mb_strtolower($prompt, 'UTF-8');
|
||||
|
||||
@@ -6,6 +6,53 @@ namespace App\Config;
|
||||
|
||||
final class AgentRunnerConfig
|
||||
{
|
||||
private const NO_LLM_MAIN_DEVICE_REQUEST_ROLE_KEYWORDS = [
|
||||
'anlage',
|
||||
'messanlage',
|
||||
'gerät',
|
||||
'geraet',
|
||||
'messgerät',
|
||||
'messgeraet',
|
||||
'analysegerät',
|
||||
'analysegeraet',
|
||||
'analysator',
|
||||
'analyzer',
|
||||
'system',
|
||||
'testomat',
|
||||
'pockettester',
|
||||
];
|
||||
|
||||
private const NO_LLM_ACCESSORY_PRODUCT_ROLE_KEYWORDS = [
|
||||
'indikator',
|
||||
'indicator',
|
||||
'indikatortyp',
|
||||
'reagenz',
|
||||
'reagent',
|
||||
'reagenzsatz',
|
||||
'kalibrierlösung',
|
||||
'kalibrierloesung',
|
||||
'pufferlösung',
|
||||
'pufferloesung',
|
||||
'reinigungslösung',
|
||||
'reinigungsloesung',
|
||||
'kalibrier',
|
||||
'puffer',
|
||||
'zubehör',
|
||||
'zubehor',
|
||||
'accessory',
|
||||
'ersatzteil',
|
||||
'verbrauch',
|
||||
'consumable',
|
||||
'kit',
|
||||
'set',
|
||||
'flasche',
|
||||
'bottle',
|
||||
'100 ml',
|
||||
'500 ml',
|
||||
'100ml',
|
||||
'500ml',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
@@ -194,6 +241,14 @@ final class AgentRunnerConfig
|
||||
);
|
||||
}
|
||||
|
||||
public function getNoLlmFallbackAccessoryOnlyForMainDeviceMessage(): string
|
||||
{
|
||||
return $this->getString(
|
||||
'no_llm_fallback.messages.accessory_only_for_main_device',
|
||||
'Die Shop-Treffer wirken wie Zubehör/Verbrauchsmaterial und nicht wie eine angefragte Messanlage oder ein Hauptgerät. Ich werte sie deshalb nicht als passende Hauptlösung.'
|
||||
);
|
||||
}
|
||||
|
||||
public function getNoLlmFallbackEscalationMessage(): string
|
||||
{
|
||||
return $this->getString(
|
||||
@@ -234,6 +289,28 @@ final class AgentRunnerConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNoLlmMainDeviceRequestRoleKeywords(): array
|
||||
{
|
||||
return $this->getStringList(
|
||||
'no_llm_fallback.product_roles.main_device_request_keywords',
|
||||
self::NO_LLM_MAIN_DEVICE_REQUEST_ROLE_KEYWORDS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNoLlmAccessoryProductRoleKeywords(): array
|
||||
{
|
||||
return $this->getStringList(
|
||||
'no_llm_fallback.product_roles.accessory_product_keywords',
|
||||
self::NO_LLM_ACCESSORY_PRODUCT_ROLE_KEYWORDS
|
||||
);
|
||||
}
|
||||
|
||||
public function getNoLlmFallbackShopUnavailableWithKnowledgeMessage(): string
|
||||
{
|
||||
return $this->getString(
|
||||
|
||||
@@ -272,6 +272,9 @@ final class PromptBuilderConfig
|
||||
'Do not merge a device identified in retrieved knowledge with price, URL, product number, or availability from a different shop item such as a reagent, accessory, kit, consumable, or service item.',
|
||||
'If a shop result has no price field, do not state a price for it.',
|
||||
'Never interpret a missing price or a zero price as free, kostenlos, gratis, or available for 0.00 EUR.',
|
||||
'Treat every SHOP PRODUCT RECORD as atomic: exact product name, product number, price, availability, URL, image, description, and metadata must stay together.',
|
||||
'When outputting a shop item, use the exact shop product name from that same SHOP PRODUCT RECORD as the heading. Never use a retrieved-knowledge device name as the heading for a different shop URL or product number.',
|
||||
'If a technical device from retrieved knowledge and a shop record are not clearly the same exact product identity, separate Fachliche Einordnung from Shop-Treffer instead of merging them.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -280,6 +283,49 @@ final class PromptBuilderConfig
|
||||
return $this->getString('shop_results.overflow_notice_template', 'Only the top %d ranked shop results are shown here out of %d total results.');
|
||||
}
|
||||
|
||||
public function getShopRecordHeaderTemplate(): string
|
||||
{
|
||||
return $this->getString('shop_results.record_header_template', '[%d] SHOP PRODUCT RECORD');
|
||||
}
|
||||
|
||||
public function getShopExactProductNameLabel(): string
|
||||
{
|
||||
return $this->getString('shop_results.exact_product_name_label', 'Exact shop product name');
|
||||
}
|
||||
|
||||
public function getShopRequestedRoleLabel(): string
|
||||
{
|
||||
return $this->getString('shop_results.requested_role_label', 'Requested product role');
|
||||
}
|
||||
|
||||
public function getShopInferredRoleLabel(): string
|
||||
{
|
||||
return $this->getString('shop_results.inferred_role_label', 'Inferred shop product role');
|
||||
}
|
||||
|
||||
public function getShopRoleCompatibilityLabel(): string
|
||||
{
|
||||
return $this->getString('shop_results.role_compatibility_label', 'Role compatibility with request');
|
||||
}
|
||||
|
||||
public function getShopRoleMismatchNotice(): string
|
||||
{
|
||||
return $this->getString(
|
||||
'shop_results.role_mismatch_notice',
|
||||
'Role mismatch: this record is kept only as a separate shop hit; do not use its description, price, URL, or product number as the main device/system answer.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopAtomicRecordNoteLines(): array
|
||||
{
|
||||
return $this->getStringList('shop_results.atomic_record_note_lines', [
|
||||
'Record boundary: all fields below belong only to this exact shop product record.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getOutputPrioritySectionLabel(): string
|
||||
{
|
||||
return $this->getString('sections.output_priority_label', 'OUTPUT PRIORITY');
|
||||
@@ -387,8 +433,12 @@ final class PromptBuilderConfig
|
||||
'- Keep price, availability, and URL on separate lines when they are present.',
|
||||
'- Only use shop price, URL, product number, or availability for the main product when the shop result clearly matches that same main product.',
|
||||
'- If the matching shop item appears to be an accessory, reagent, consumable, set, or kit, keep it separate and do not present its commercial fields as the main device.',
|
||||
'- If a SHOP PRODUCT RECORD is classified as accessory_or_consumable while the requested product role is main_device_or_system, do not use that record as a product recommendation headline.',
|
||||
'- If the commercial match is uncertain, say that commercial details for the main product are not clearly available in the provided shop results.',
|
||||
'- If no price is shown for a shop item, omit the price instead of writing 0,00 €, free, kostenlos, or a guessed price.',
|
||||
'- For every shop hit shown in the answer, copy the exact shop product name verbatim from the same SHOP PRODUCT RECORD as the item heading.',
|
||||
'- Never place a shop URL, product number, price, or availability below a different heading taken from retrieved knowledge.',
|
||||
'- If technical RAG knowledge and shop records cannot be matched with high confidence, use separate sections: Fachliche Einordnung and Shop-Treffer.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -491,6 +541,8 @@ final class PromptBuilderConfig
|
||||
'- Do not assign the product number, price, URL, or availability of a reagent, accessory, kit, set, consumable, or service item to a device identified in retrieved knowledge.',
|
||||
'- Only use commercial fields for the main product when the shop item and the technically identified product clearly refer to the same product identity.',
|
||||
'- If the shop match is ambiguous, keep the technical identification and commercial details separate.',
|
||||
'- Shop product names are authoritative for their own shop URL, product number, price, availability, image, description, and metadata.',
|
||||
'- Do not rewrite a shop record heading with a similar device name from retrieved knowledge. If identities differ or are uncertain, separate the RAG device from the shop hit.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -638,6 +690,39 @@ final class PromptBuilderConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMainDeviceRequestRoleKeywords(): array
|
||||
{
|
||||
return $this->getStringList(
|
||||
'product_roles.main_device_request_keywords',
|
||||
self::MAIN_DEVICE_REQUEST_ROLE_KEYWORDS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMainDeviceProductRoleKeywords(): array
|
||||
{
|
||||
return $this->getStringList(
|
||||
'product_roles.main_device_product_keywords',
|
||||
self::MAIN_DEVICE_PRODUCT_ROLE_KEYWORDS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAccessoryProductRoleKeywords(): array
|
||||
{
|
||||
return $this->getStringList(
|
||||
'product_roles.accessory_product_keywords',
|
||||
self::ACCESSORY_PRODUCT_ROLE_KEYWORDS
|
||||
);
|
||||
}
|
||||
|
||||
public function getTechnicalProductModelPattern(): string
|
||||
{
|
||||
return $this->getString('technical_product_model_pattern', '/\b[\p{L}]{2,}\s?\d{2,5}\b/u');
|
||||
|
||||
Reference in New Issue
Block a user