last update before full new 1.5.0
This commit is contained in:
@@ -52,6 +52,7 @@ final readonly class AgentRunner
|
||||
$shopResults = [];
|
||||
$primaryShopResults = [];
|
||||
$knowledgeChunks = [];
|
||||
$knowledgeEvidenceState = 'none';
|
||||
$sources = [];
|
||||
$optimizedShopQuery = '';
|
||||
$shopSearchQuery = '';
|
||||
@@ -105,6 +106,7 @@ final readonly class AgentRunner
|
||||
$usedFollowUpRetrievalContext = $knowledgeRetrievalPrompt !== $prompt;
|
||||
|
||||
$knowledgeChunks = $this->retriever->retrieve($knowledgeRetrievalPrompt);
|
||||
$knowledgeEvidenceState = $this->resolveKnowledgeEvidenceState($prompt, $knowledgeChunks, $urlContent);
|
||||
if ($knowledgeChunks !== []) {
|
||||
$this->addSource($sources, $this->agentRunnerConfig->getRagKnowledgeSourceLabel());
|
||||
}
|
||||
@@ -116,7 +118,7 @@ final readonly class AgentRunner
|
||||
shopCount: null,
|
||||
shopCountMode: 'not_requested',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $knowledgeChunks !== [] ? 'fachlich belegt' : 'noch keine belastbaren Treffer'
|
||||
confidenceLabel: $this->resolveRagEvidenceConfidenceLabel($knowledgeEvidenceState)
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
@@ -138,7 +140,7 @@ final readonly class AgentRunner
|
||||
shopCount: null,
|
||||
shopCountMode: 'loading',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $knowledgeChunks !== [] ? 'fachlich belegt; Shopdaten werden geprüft' : 'Shopdaten werden geprüft'
|
||||
confidenceLabel: $this->resolveRagEvidenceShopCheckConfidenceLabel($knowledgeEvidenceState)
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
@@ -241,7 +243,7 @@ final readonly class AgentRunner
|
||||
shopCount: null,
|
||||
shopCountMode: 'loading',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $knowledgeChunks !== [] ? 'fachlich belegt; Shopdaten werden geprüft' : 'Shopdaten werden geprüft'
|
||||
confidenceLabel: $this->resolveRagEvidenceShopCheckConfidenceLabel($knowledgeEvidenceState)
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
@@ -274,6 +276,17 @@ final readonly class AgentRunner
|
||||
$shopUnavailableMessage,
|
||||
'err'
|
||||
);
|
||||
yield $this->systemMsg(
|
||||
$this->buildShopSearchMetaMessage(
|
||||
query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery,
|
||||
commerceIntent: $commerceIntent,
|
||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||
originalQuery: $shopSearchQuery,
|
||||
completed: true,
|
||||
unavailable: true
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
$historyNotices[] = $this->buildHistoryNotice(
|
||||
'Shopdaten konnten nicht geladen werden',
|
||||
$primaryShopSearchFailureReason
|
||||
@@ -337,7 +350,7 @@ final readonly class AgentRunner
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : 'count',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
|
||||
hasKnowledge: $knowledgeChunks !== [] || trim($urlContent) !== '',
|
||||
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
|
||||
isCommerceIntent: true,
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
hasShopResults: $shopResults !== [],
|
||||
@@ -363,7 +376,8 @@ final readonly class AgentRunner
|
||||
fullContext: $forceFullContext,
|
||||
swagFullOutPut: $optimizedShopQuery,
|
||||
commerceSearchAttempted: $shopSearchAttempted,
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
|
||||
knowledgeEvidenceState: $knowledgeEvidenceState
|
||||
);
|
||||
|
||||
if ($this->debug && $this->logPrompt) {
|
||||
@@ -401,7 +415,7 @@ final readonly class AgentRunner
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
|
||||
hasKnowledge: $knowledgeChunks !== [] || trim($urlContent) !== '',
|
||||
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
|
||||
isCommerceIntent: $this->isCommerceIntent($commerceIntent),
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
hasShopResults: $shopResults !== [],
|
||||
@@ -426,7 +440,8 @@ final readonly class AgentRunner
|
||||
commerceIntent: $commerceIntent,
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
|
||||
shopSearchFailureReason: $primaryShopSearchFailureReason ?? null
|
||||
shopSearchFailureReason: $primaryShopSearchFailureReason ?? null,
|
||||
knowledgeEvidenceState: $knowledgeEvidenceState
|
||||
);
|
||||
|
||||
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt, $noLlmFallbackAnswer);
|
||||
@@ -439,7 +454,7 @@ final readonly class AgentRunner
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
|
||||
hasKnowledge: $knowledgeChunks !== [] || trim($urlContent) !== '',
|
||||
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
|
||||
isCommerceIntent: $this->isCommerceIntent($commerceIntent),
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
hasShopResults: $shopResults !== [],
|
||||
@@ -1413,9 +1428,10 @@ final readonly class AgentRunner
|
||||
string $commerceIntent,
|
||||
bool $shopSearchAttempted,
|
||||
bool $shopSearchHadSystemFailure,
|
||||
?string $shopSearchFailureReason
|
||||
?string $shopSearchFailureReason,
|
||||
string $knowledgeEvidenceState = 'unknown'
|
||||
): string {
|
||||
$hasKnowledge = $knowledgeChunks !== [] || trim($urlContent) !== '';
|
||||
$hasKnowledge = $this->isDirectKnowledgeEvidence($knowledgeEvidenceState) || ($knowledgeEvidenceState === 'unknown' && ($knowledgeChunks !== [] || trim($urlContent) !== ''));
|
||||
$hasShopResults = $shopResults !== [];
|
||||
$isCommerceIntent = $this->isCommerceIntent($commerceIntent);
|
||||
|
||||
@@ -1636,6 +1652,172 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Distinguish semantic nearest-neighbor retrieval hits from direct factual evidence.
|
||||
*
|
||||
* Vector retrieval can return useful context even when the essential user term is not
|
||||
* present. Those hits should stay visible as RAG hits, but they must not be counted as
|
||||
* "fachlich belegt" unless at least one salient request term or configured synonym
|
||||
* appears in the retrieved knowledge or user-provided URL content.
|
||||
*
|
||||
* @param string[] $knowledgeChunks
|
||||
*/
|
||||
private function resolveKnowledgeEvidenceState(string $prompt, array $knowledgeChunks, string $urlContent): string
|
||||
{
|
||||
if ($knowledgeChunks === [] && trim($urlContent) === '') {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (trim($urlContent) !== '') {
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
$needles = $this->buildRagEvidenceNeedles($prompt);
|
||||
|
||||
if ($needles === []) {
|
||||
// No meaningful term could be extracted. Preserve the previous behavior for
|
||||
// very short follow-ups instead of hiding potentially valid context.
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
$haystack = $this->normalizeRagEvidenceText(implode("\n\n", array_map('strval', $knowledgeChunks)));
|
||||
|
||||
foreach ($needles as $needleGroup) {
|
||||
foreach ($needleGroup as $needle) {
|
||||
if ($this->containsRagEvidenceTerm($haystack, $needle)) {
|
||||
return 'direct';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'weak';
|
||||
}
|
||||
|
||||
private function isDirectKnowledgeEvidence(string $knowledgeEvidenceState): bool
|
||||
{
|
||||
return $knowledgeEvidenceState === 'direct';
|
||||
}
|
||||
|
||||
private function resolveRagEvidenceConfidenceLabel(string $knowledgeEvidenceState): string
|
||||
{
|
||||
return match ($knowledgeEvidenceState) {
|
||||
'direct' => 'fachlich belegt',
|
||||
'weak' => 'RAG-Näherungstreffer, kein direkter Fachbeleg',
|
||||
default => 'noch keine belastbaren Treffer',
|
||||
};
|
||||
}
|
||||
|
||||
private function resolveRagEvidenceShopCheckConfidenceLabel(string $knowledgeEvidenceState): string
|
||||
{
|
||||
return match ($knowledgeEvidenceState) {
|
||||
'direct' => 'fachlich belegt; Shopdaten werden geprüft',
|
||||
'weak' => 'RAG-Näherungstreffer; Shopdaten werden geprüft',
|
||||
default => 'Shopdaten werden geprüft',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string[]>
|
||||
*/
|
||||
private function buildRagEvidenceNeedles(string $prompt): array
|
||||
{
|
||||
$normalizedPrompt = $this->normalizeRagEvidenceText($prompt);
|
||||
$stopTerms = [];
|
||||
|
||||
foreach ($this->agentRunnerConfig->getRagEvidenceStopTerms() as $term) {
|
||||
$term = $this->normalizeRagEvidenceText($term);
|
||||
if ($term !== '') {
|
||||
$stopTerms[$term] = true;
|
||||
}
|
||||
}
|
||||
|
||||
preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\-]{1,}/u', $normalizedPrompt, $matches);
|
||||
$tokens = $matches[0] ?? [];
|
||||
$groups = [];
|
||||
$synonyms = $this->normalizedRagEvidenceSynonyms();
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$token = trim((string) $token);
|
||||
|
||||
if ($token === '' || isset($stopTerms[$token]) || mb_strlen($token, 'UTF-8') < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = $synonyms[$token] ?? [$token];
|
||||
$group = array_values(array_unique(array_filter(array_map(
|
||||
fn (string $item): string => $this->normalizeRagEvidenceText($item),
|
||||
$group
|
||||
))));
|
||||
|
||||
if ($group !== []) {
|
||||
$groups[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
private function normalizedRagEvidenceSynonyms(): array
|
||||
{
|
||||
$out = [];
|
||||
|
||||
foreach ($this->agentRunnerConfig->getRagEvidenceSynonyms() as $key => $items) {
|
||||
$key = $this->normalizeRagEvidenceText((string) $key);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($items as $item) {
|
||||
$item = $this->normalizeRagEvidenceText((string) $item);
|
||||
if ($item !== '' && !in_array($item, $terms, true)) {
|
||||
$terms[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($key, $terms, true)) {
|
||||
$terms[] = $key;
|
||||
}
|
||||
|
||||
$out[$key] = $terms;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function containsRagEvidenceTerm(string $haystack, string $needle): bool
|
||||
{
|
||||
$needle = $this->normalizeRagEvidenceText($needle);
|
||||
|
||||
if ($haystack === '' || $needle === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = '/(?<![\p{L}\p{N}])' . preg_quote($needle, '/') . '(?![\p{L}\p{N}])/u';
|
||||
|
||||
return preg_match($pattern, $haystack) === 1;
|
||||
}
|
||||
|
||||
private function normalizeRagEvidenceText(string $value): string
|
||||
{
|
||||
$value = html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$value = mb_strtolower($value, 'UTF-8');
|
||||
$value = str_replace(['‐', '‑', '‒', '–', '—'], '-', $value);
|
||||
$value = strtr($value, [
|
||||
'ä' => 'ae',
|
||||
'ö' => 'oe',
|
||||
'ü' => 'ue',
|
||||
'ß' => 'ss',
|
||||
]);
|
||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $sources
|
||||
*/
|
||||
@@ -1814,10 +1996,6 @@ 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.
|
||||
@@ -1829,35 +2007,18 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->canonicalProductionUiSourceLabelKey($label);
|
||||
|
||||
if ($key === $shopSystemKey || $key === 'liveshopdaten') {
|
||||
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
|
||||
$label = 'Live-Shopdaten';
|
||||
$key = 'liveshopdaten';
|
||||
}
|
||||
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
if (!in_array($label, $labels, true)) {
|
||||
$labels[] = $label;
|
||||
}
|
||||
|
||||
$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
|
||||
*/
|
||||
@@ -2037,7 +2198,8 @@ final readonly class AgentRunner
|
||||
?int $resultCount = null,
|
||||
bool $completed = false,
|
||||
bool $attemptedRepair = false,
|
||||
bool $usedRepair = false
|
||||
bool $usedRepair = false,
|
||||
bool $unavailable = false
|
||||
): string {
|
||||
$query = $this->normalizeOneLine($query);
|
||||
$originalQuery = $this->normalizeOneLine($originalQuery);
|
||||
@@ -2048,11 +2210,13 @@ final readonly class AgentRunner
|
||||
|
||||
$queryModeLabel = $usedOptimizedQuery ? 'optimiert' : 'direkt';
|
||||
$intentLabel = $commerceIntent !== '' ? $commerceIntent : 'commerce';
|
||||
$title = $completed ? 'Shop-Suche abgeschlossen' : 'Shop-Suche wird ausgeführt';
|
||||
$title = $unavailable ? 'Shopdaten nicht verfügbar' : ($completed ? 'Shop-Suche abgeschlossen' : 'Shop-Suche wird ausgeführt');
|
||||
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';
|
||||
$resultLabel = $resultCount === null
|
||||
? 'Shoptreffer: wird geladen'
|
||||
: 'Shoptreffer: ' . max(0, $resultCount);
|
||||
$resultLabel = $unavailable
|
||||
? 'Shoptreffer: nicht verfügbar'
|
||||
: ($resultCount === null
|
||||
? 'Shoptreffer: wird geladen'
|
||||
: 'Shoptreffer: ' . max(0, $resultCount));
|
||||
$state = $completed ? 'completed' : 'running';
|
||||
$resultCountAttribute = $resultCount === null ? '' : (string) max(0, $resultCount);
|
||||
$repairLabel = '';
|
||||
|
||||
Reference in New Issue
Block a user