diff --git a/public/assets/js/base.js b/public/assets/js/base.js
index f54eac9..53aba97 100644
--- a/public/assets/js/base.js
+++ b/public/assets/js/base.js
@@ -179,6 +179,59 @@ document.addEventListener('DOMContentLoaded', () => {
cleanupEmptyBlocks(container);
}
+ function hasVisibleContentAfterNode(container, markerNode) {
+ const walker = document.createTreeWalker(
+ container,
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
+ null
+ );
+
+ let afterMarker = false;
+
+ while (walker.nextNode()) {
+ const node = walker.currentNode;
+
+ if (node === markerNode) {
+ afterMarker = true;
+ continue;
+ }
+
+ if (!afterMarker) {
+ continue;
+ }
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ if (node.parentElement?.closest('.think')) {
+ continue;
+ }
+
+ if ((node.textContent || '').trim() !== '') {
+ return true;
+ }
+
+ continue;
+ }
+
+ if (node.nodeType !== Node.ELEMENT_NODE) {
+ continue;
+ }
+
+ if (node.classList?.contains('think') || node.closest?.('.think')) {
+ continue;
+ }
+
+ if (node.tagName === 'BR') {
+ continue;
+ }
+
+ if ((node.textContent || '').trim() !== '' || hasMeaningfulChildContent(node)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
function cleanupThinkSpans(container) {
if (!container) {
return;
@@ -191,12 +244,14 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
- if (hasNonThinkContent(container)) {
- removeThinkSpansOnly(container);
+ const lastThink = thinkSpans[thinkSpans.length - 1];
+
+ if (!hasVisibleContentAfterNode(container, lastThink)) {
+ keepOnlyLastThink(container);
return;
}
- keepOnlyLastThink(container);
+ removeThinkSpansOnly(container);
}
function renderBubbleContent(bubble, raw) {
diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php
index 0196504..c473208 100644
--- a/src/Agent/AgentRunner.php
+++ b/src/Agent/AgentRunner.php
@@ -765,11 +765,14 @@ final readonly class AgentRunner
private function streamFinalAnswer(string $finalPrompt): Generator
{
$fullOutput = '';
- $firstThinkLoop = true;
+ $thinkingNoticeShown = false;
$chunker = new StreamChunker();
$this->thinkSuppressor->reset();
+ yield $this->systemMsg($this->agentRunnerConfig->getThinkingWhileStreamingMessage(), 'think');
+ $thinkingNoticeShown = true;
+
foreach ($this->ollamaClient->stream($finalPrompt) as $token) {
if (!is_string($token)) {
continue;
@@ -778,9 +781,9 @@ final readonly class AgentRunner
$cleanToken = $this->thinkSuppressor->filter($token);
if ($cleanToken === '') {
- if ($firstThinkLoop) {
+ if (!$thinkingNoticeShown) {
yield $this->systemMsg($this->agentRunnerConfig->getThinkingWhileStreamingMessage(), 'think');
- $firstThinkLoop = false;
+ $thinkingNoticeShown = true;
}
continue;
@@ -902,7 +905,7 @@ final readonly class AgentRunner
return '
'
. '
Live-Shopdaten
'
- . '
Shopware-Suche wird ausgeführt
'
+ . '
Shop-Suche wird ausgeführt
'
. '
'
. '' . htmlspecialchars($badge, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''
. 'Intent: ' . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''
diff --git a/src/Commerce/CommerceQueryParser.php b/src/Commerce/CommerceQueryParser.php
index ead10d2..d620afe 100644
--- a/src/Commerce/CommerceQueryParser.php
+++ b/src/Commerce/CommerceQueryParser.php
@@ -79,7 +79,6 @@ final readonly class CommerceQueryParser
private function normalize(string $prompt): string
{
$value = $this->textNormalizer->normalize($prompt);
- $value = $this->queryCleaner->clean($value);
$value = mb_strtolower(trim($value));
$value = str_replace(
$this->config->getNormalizationSearch(),
@@ -274,7 +273,16 @@ final readonly class CommerceQueryParser
for ($offset = 1; $offset <= $this->config->getModelContextTokenWindow(); $offset++) {
$previousIndex = $index - $offset;
- if (!isset($tokens[$previousIndex]) || !$this->isLikelyModelContextToken($tokens[$previousIndex])) {
+ if (!isset($tokens[$previousIndex])) {
+ break;
+ }
+
+ if ($this->isSemanticShopToken($tokens[$previousIndex])) {
+ $keep[$previousIndex] = true;
+ continue;
+ }
+
+ if (!$this->isLikelyModelContextToken($tokens[$previousIndex])) {
break;
}
@@ -287,7 +295,7 @@ final readonly class CommerceQueryParser
}
}
- if ($this->isSemanticShopToken($token) || $this->isKnownBrandToken($token)) {
+ if ($this->isSemanticShopToken($token) || $this->isKnownBrandToken($token) || $this->isMeasurementValueToken($token)) {
$keep[$index] = true;
}
}
@@ -314,6 +322,10 @@ final readonly class CommerceQueryParser
return true;
}
+ if ($this->isMeasurementValueToken($token)) {
+ return false;
+ }
+
if (preg_match($this->config->getContainsDigitPattern(), $token) === 1) {
return false;
}
@@ -334,6 +346,11 @@ final readonly class CommerceQueryParser
return preg_match($this->config->getModelNumberTokenPattern(), $token) === 1;
}
+ private function isMeasurementValueToken(string $token): bool
+ {
+ return preg_match($this->config->getMeasurementValueTokenPattern(), $token) === 1;
+ }
+
private function isLikelyModelContextToken(string $token): bool
{
if ($this->isQueryNoiseToken($token)) {
diff --git a/src/Config/CommerceIntentConfig.php b/src/Config/CommerceIntentConfig.php
index 05f4f4c..84a6054 100644
--- a/src/Config/CommerceIntentConfig.php
+++ b/src/Config/CommerceIntentConfig.php
@@ -37,6 +37,13 @@ final class CommerceIntentConfig
'kalibrierlösung',
'kalibrierloesung',
'kalibrierung',
+ 'chemie',
+ 'reagenz',
+ 'reagenzien',
+ 'verbrauchsmaterial',
+ 'zubehör',
+ 'zubehoer',
+ 'ersatzteil',
];
}
@@ -50,6 +57,10 @@ final class CommerceIntentConfig
'eignet',
'besser',
'besten',
+ 'gut für',
+ 'gut fuer',
+ 'passend für',
+ 'passend fuer',
'geeignet',
'geeigent',
'empfiehl',
@@ -195,6 +206,12 @@ final class CommerceIntentConfig
'/\bartikel\b/u',
'/\bsku\b/u',
'/\bonline\b/u',
+ '/\bchemie\b/u',
+ '/\breagenz(?:ien)?\b/u',
+ '/\bverbrauchsmaterial(?:ien)?\b/u',
+ '/\bzubehör\b/u',
+ '/\bzubehoer\b/u',
+ '/\bersatzteil(?:e)?\b/u',
];
}
diff --git a/src/Config/CommerceQueryParserConfig.php b/src/Config/CommerceQueryParserConfig.php
index 438eb17..3e611c9 100644
--- a/src/Config/CommerceQueryParserConfig.php
+++ b/src/Config/CommerceQueryParserConfig.php
@@ -133,6 +133,14 @@ final class CommerceQueryParserConfig
'kostet',
'kosten',
'ua',
+ 'also',
+ 'gut',
+ 'gute',
+ 'guten',
+ 'guter',
+ 'gutes',
+ 'passen',
+ 'passend',
];
}
@@ -297,7 +305,7 @@ final class CommerceQueryParserConfig
public function getModelContextTokenWindow(): int
{
- return 2;
+ return 4;
}
public function getMinMeaningfulAlphaTokenLength(): int
@@ -312,7 +320,11 @@ final class CommerceQueryParserConfig
public function getInstructionOrPresentationTokenPattern(): string
{
- return '/^(?:zeig(?:e)?|such(?:e)?|find(?:e)?|gib|gebe|nenn(?:e)?|liefer(?:e)?|erstelle?|mach(?:e)?|brauch(?:e)?|will|möchte|moechte|hätte|haette|kannst|bitte|mal|alle|alles|komplett|vollständig|vollstaendig|gesamt|ganze|ganzen|liste|listung|auflistung|tabelle|tabellarisch|übersicht|uebersicht|anzeigen?|ausgeben?|darstellen?|antwort(?:e)?|erklär(?:e)?|erklaer(?:e)?|info|infos|informationen|dazu|hierzu|damit|davon|an|als|mit|ohne|inkl|inklusive)$/u';
+ return '/^(?:zeig(?:e)?|such(?:e)?|find(?:e)?|gib|gebe|nenn(?:e)?|liefer(?:e)?|erstelle?|mach(?:e)?|brauch(?:e)?|will|möchte|moechte|hätte|haette|kannst|bitte|mal|alle|alles|komplett|vollständig|vollstaendig|gesamt|ganze|ganzen|liste|listung|auflistung|tabelle|tabellarisch|übersicht|uebersicht|anzeigen?|ausgeben?|darstellen?|antwort(?:e)?|erklär(?:e)?|erklaer(?:e)?|info|infos|informationen|dazu|hierzu|damit|davon|an|als|mit|ohne|inkl|inklusive|also|gut|gute|guten|guter|gutes|passend|passen)$/u';
+ }
+ public function getMeasurementValueTokenPattern(): string
+ {
+ return '/^\d+[.,]\d+$/u';
}
/**
@@ -332,6 +344,9 @@ final class CommerceQueryParserConfig
'zubehor',
'ersatzteil',
'verbrauchsmaterial',
+ 'chemie',
+ 'indikatorchemie',
+ 'reagenzchemie',
'kit',
'set',
'filter',