optimize technical truth
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user