p43A
This commit is contained in:
@@ -1238,7 +1238,7 @@ final readonly class AgentRunner
|
||||
* These anchors are only used to resolve follow-up references such as
|
||||
* "der Wert" or "welcher Indikator". They are not factual evidence for
|
||||
* the final answer. To avoid propagating wrong earlier answers, only the
|
||||
* first explicit Testomat model reference and the first explicit °dH value
|
||||
* first explicit product-model reference and the first explicit measurement value
|
||||
* are kept. Indicator names, reagent codes, prices, URLs and product
|
||||
* numbers are intentionally ignored here.
|
||||
*
|
||||
@@ -1261,12 +1261,12 @@ final readonly class AgentRunner
|
||||
|
||||
$anchors = [];
|
||||
|
||||
$model = $this->extractFirstTestomatModelAnchor($answer);
|
||||
$model = $this->extractFirstProductModelAnchor($answer);
|
||||
if ($model !== '') {
|
||||
$anchors[] = $model;
|
||||
}
|
||||
|
||||
$hardnessValue = $this->extractFirstHardnessValueAnchor($answer);
|
||||
$hardnessValue = $this->extractFirstMeasurementValueAnchor($answer);
|
||||
if ($hardnessValue !== '') {
|
||||
$anchors[] = $hardnessValue;
|
||||
}
|
||||
@@ -1325,9 +1325,9 @@ final readonly class AgentRunner
|
||||
return array_reverse($turns);
|
||||
}
|
||||
|
||||
private function extractFirstTestomatModelAnchor(string $text): string
|
||||
private function extractFirstProductModelAnchor(string $text): string
|
||||
{
|
||||
if (preg_match($this->agentRunnerConfig->getFollowUpReferenceAnchorTestomatModelPattern(), $text, $matches) !== 1) {
|
||||
if (preg_match($this->agentRunnerConfig->getFollowUpReferenceAnchorProductModelPattern(), $text, $matches) !== 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1337,9 +1337,9 @@ final readonly class AgentRunner
|
||||
return trim(str_replace('®', '', $value));
|
||||
}
|
||||
|
||||
private function extractFirstHardnessValueAnchor(string $text): string
|
||||
private function extractFirstMeasurementValueAnchor(string $text): string
|
||||
{
|
||||
if (preg_match($this->agentRunnerConfig->getFollowUpReferenceAnchorHardnessValuePattern(), $text, $matches) !== 1) {
|
||||
if (preg_match($this->agentRunnerConfig->getFollowUpReferenceAnchorMeasurementValuePattern(), $text, $matches) !== 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1500,7 +1500,7 @@ final readonly class AgentRunner
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->extractFirstTestomatModelAnchor($prompt) !== '') {
|
||||
if ($this->extractFirstProductModelAnchor($prompt) !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1564,7 +1564,7 @@ final readonly class AgentRunner
|
||||
|
||||
private function hasStandaloneConcreteShopSubject(string $prompt): bool
|
||||
{
|
||||
if ($this->extractFirstTestomatModelAnchor($prompt) !== '') {
|
||||
if ($this->extractFirstProductModelAnchor($prompt) !== '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1622,7 +1622,7 @@ final readonly class AgentRunner
|
||||
return $prompt;
|
||||
}
|
||||
|
||||
if ($this->extractFirstTestomatModelAnchor($prompt) === '') {
|
||||
if ($this->extractFirstProductModelAnchor($prompt) === '') {
|
||||
return $optimizedShopQuery;
|
||||
}
|
||||
|
||||
@@ -2249,7 +2249,7 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
$model = $this->extractFirstTestomatModelAnchor($turn);
|
||||
$model = $this->extractFirstProductModelAnchor($turn);
|
||||
|
||||
if ($model !== '') {
|
||||
$query = str_replace(
|
||||
@@ -2334,7 +2334,7 @@ final readonly class AgentRunner
|
||||
}
|
||||
}
|
||||
|
||||
$modelAnchor = $this->extractFirstTestomatModelAnchor($turn);
|
||||
$modelAnchor = $this->extractFirstProductModelAnchor($turn);
|
||||
|
||||
if ($modelAnchor !== '' && !$this->isMetaOnlyShopQuery($modelAnchor)) {
|
||||
return mb_strtolower($modelAnchor, 'UTF-8');
|
||||
|
||||
@@ -125,14 +125,34 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredString('follow_up_context.history_question_strip_pattern');
|
||||
}
|
||||
|
||||
public function getFollowUpReferenceAnchorProductModelPattern(): string
|
||||
{
|
||||
$value = $this->optionalValue('follow_up_context.reference_anchor.product_model_pattern');
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->getRequiredString('follow_up_context.reference_anchor.testomat_model_pattern');
|
||||
}
|
||||
|
||||
public function getFollowUpReferenceAnchorMeasurementValuePattern(): string
|
||||
{
|
||||
$value = $this->optionalValue('follow_up_context.reference_anchor.measurement_value_pattern');
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->getRequiredString('follow_up_context.reference_anchor.hardness_value_pattern');
|
||||
}
|
||||
|
||||
public function getFollowUpReferenceAnchorTestomatModelPattern(): string
|
||||
{
|
||||
return $this->getRequiredString('follow_up_context.reference_anchor.testomat_model_pattern');
|
||||
return $this->getFollowUpReferenceAnchorProductModelPattern();
|
||||
}
|
||||
|
||||
public function getFollowUpReferenceAnchorHardnessValuePattern(): string
|
||||
{
|
||||
return $this->getRequiredString('follow_up_context.reference_anchor.hardness_value_pattern');
|
||||
return $this->getFollowUpReferenceAnchorMeasurementValuePattern();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ final class CommerceQueryParserConfig
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $config = [],
|
||||
private readonly ?DomainVocabularyConfig $vocabulary = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -268,7 +269,10 @@ final class CommerceQueryParserConfig
|
||||
/** @return string[] */
|
||||
public function getSemanticShopSearchTokens(): array
|
||||
{
|
||||
return $this->stringList('semantic_shop_search_tokens');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'semantic_shop_search_tokens',
|
||||
'vocabulary_views.semantic_shop_search_tokens'
|
||||
);
|
||||
}
|
||||
|
||||
public function buildExactTokenRemovalPattern(string $token): string
|
||||
@@ -319,6 +323,27 @@ final class CommerceQueryParserConfig
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
private function configuredStringListOrVocabularyView(string $configPath, string $viewPathConfigPath): array
|
||||
{
|
||||
if ($this->hasPath($configPath)) {
|
||||
return $this->stringList($configPath);
|
||||
}
|
||||
|
||||
if ($this->vocabulary === null) {
|
||||
throw $this->missing($configPath);
|
||||
}
|
||||
|
||||
$viewPath = $this->string($viewPathConfigPath);
|
||||
$terms = $this->vocabulary->view($viewPath, []);
|
||||
|
||||
if ($terms === []) {
|
||||
throw $this->invalid($viewPathConfigPath, sprintf('references empty vocabulary view "%s"', $viewPath));
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private function stringMap(string $path): array
|
||||
{
|
||||
@@ -372,6 +397,20 @@ final class CommerceQueryParserConfig
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function hasPath(string $path): bool
|
||||
{
|
||||
$current = $this->config;
|
||||
foreach (explode('.', $path) as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function value(string $path): mixed
|
||||
{
|
||||
$current = $this->config;
|
||||
|
||||
@@ -13,6 +13,7 @@ final class NdjsonHybridRetrieverConfig
|
||||
*/
|
||||
public function __construct(
|
||||
private array $config = [],
|
||||
private ?DomainVocabularyConfig $vocabulary = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -216,55 +217,82 @@ final class NdjsonHybridRetrieverConfig
|
||||
/** @return string[] */
|
||||
public function genericProductTokens(): array
|
||||
{
|
||||
return $this->requiredStringList('generic_product_tokens');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'generic_product_tokens',
|
||||
'vocabulary_views.generic_product_tokens'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function importantShortModelTokens(): array
|
||||
{
|
||||
return $this->requiredStringList('important_short_model_tokens');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'important_short_model_tokens',
|
||||
'vocabulary_views.important_short_model_tokens'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function familyDescriptorTokens(): array
|
||||
{
|
||||
return $this->requiredStringList('family_descriptor_tokens');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'family_descriptor_tokens',
|
||||
'vocabulary_views.family_descriptor_tokens'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function looksLikeReagentTokens(): array
|
||||
{
|
||||
return $this->requiredStringList('looks_like_reagent_tokens');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'looks_like_reagent_tokens',
|
||||
'vocabulary_views.looks_like_reagent_tokens'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function looksLikeSafetyDocs(): array
|
||||
{
|
||||
return $this->requiredStringList('looks_like_safety_docs');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'looks_like_safety_docs',
|
||||
'vocabulary_views.looks_like_safety_docs'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function looksLikeReagentWords(): array
|
||||
{
|
||||
return $this->requiredStringList('looks_like_reagent_words');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'looks_like_reagent_words',
|
||||
'vocabulary_views.looks_like_reagent_words'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function looksLikeDocumentWords(): array
|
||||
{
|
||||
return $this->requiredStringList('looks_like_document_words');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'looks_like_document_words',
|
||||
'vocabulary_views.looks_like_document_words'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function looksLikeSafetyWords(): array
|
||||
{
|
||||
return $this->requiredStringList('looks_like_safety_words');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'looks_like_safety_words',
|
||||
'vocabulary_views.looks_like_safety_words'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function looksLikeDeviceWords(): array
|
||||
{
|
||||
return $this->requiredStringList('looks_like_device_words');
|
||||
return $this->configuredStringListOrVocabularyView(
|
||||
'looks_like_device_words',
|
||||
'vocabulary_views.looks_like_device_words'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -471,6 +499,74 @@ final class NdjsonHybridRetrieverConfig
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
/** @return string[] */
|
||||
private function configuredStringListOrVocabularyView(string $configPath, string $viewPathConfigPath): array
|
||||
{
|
||||
if ($this->hasKey($configPath)) {
|
||||
return $this->requiredStringList($configPath);
|
||||
}
|
||||
|
||||
if ($this->vocabulary === null) {
|
||||
throw $this->missing($configPath);
|
||||
}
|
||||
|
||||
$viewPath = $this->requiredPathString($viewPathConfigPath);
|
||||
$terms = $this->vocabulary->view($viewPath, []);
|
||||
|
||||
if ($terms === []) {
|
||||
throw $this->invalid($viewPathConfigPath, sprintf('references empty vocabulary view "%s"', $viewPath));
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
private function requiredPathString(string $key): string
|
||||
{
|
||||
$value = $this->requiredPathValue($key);
|
||||
|
||||
if (!is_scalar($value)) {
|
||||
throw $this->invalid($key, 'must be a non-empty string');
|
||||
}
|
||||
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
throw $this->invalid($key, 'must be a non-empty string');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function requiredPathValue(string $key): mixed
|
||||
{
|
||||
$current = $this->config;
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
throw $this->missing($key);
|
||||
}
|
||||
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
private function hasKey(string $key): bool
|
||||
{
|
||||
$current = $this->config;
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function requiredValue(string $key): mixed
|
||||
{
|
||||
if (!array_key_exists($key, $this->config)) {
|
||||
|
||||
@@ -11,6 +11,7 @@ final class PromptBuilderConfig
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $config = [],
|
||||
private readonly ?DomainVocabularyConfig $vocabulary = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -159,6 +160,35 @@ final class PromptBuilderConfig
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getConfiguredStringListOrVocabularyView(string $configPath, string $viewPathConfigPath): array
|
||||
{
|
||||
if ($this->hasPath($configPath)) {
|
||||
return $this->getRequiredStringList($configPath);
|
||||
}
|
||||
|
||||
if ($this->vocabulary === null) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'RetrieX prompt config path "%s" is missing and no vocabulary resolver is available.',
|
||||
$configPath
|
||||
));
|
||||
}
|
||||
|
||||
$viewPath = $this->getRequiredString($viewPathConfigPath);
|
||||
$terms = $this->vocabulary->view($viewPath, []);
|
||||
|
||||
if ($terms === []) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'RetrieX prompt vocabulary view "%s" resolved to an empty list.',
|
||||
$viewPath
|
||||
));
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
@@ -193,6 +223,21 @@ final class PromptBuilderConfig
|
||||
|
||||
|
||||
|
||||
private function hasPath(string $path): bool
|
||||
{
|
||||
$current = $this->config;
|
||||
|
||||
foreach (explode('.', $path) as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getOptionalValue(string $path): mixed
|
||||
{
|
||||
$current = $this->config;
|
||||
@@ -573,7 +618,10 @@ final class PromptBuilderConfig
|
||||
*/
|
||||
public function getTechnicalProductKeywords(): array
|
||||
{
|
||||
return $this->getRequiredStringList('technical_product_keywords');
|
||||
return $this->getConfiguredStringListOrVocabularyView(
|
||||
'technical_product_keywords',
|
||||
'vocabulary_views.technical_product_keywords'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,7 +629,10 @@ final class PromptBuilderConfig
|
||||
*/
|
||||
public function getAccessoryRequestKeywords(): array
|
||||
{
|
||||
return $this->getRequiredStringList('accessory_request_keywords');
|
||||
return $this->getConfiguredStringListOrVocabularyView(
|
||||
'accessory_request_keywords',
|
||||
'vocabulary_views.accessory_request_keywords'
|
||||
);
|
||||
}
|
||||
|
||||
public function getMeasurementEvidenceSectionLabel(): string
|
||||
|
||||
Reference in New Issue
Block a user