diff --git a/config/services.yaml b/config/services.yaml index 45addee..f5ba347 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -41,6 +41,12 @@ parameters: mto.vector.timeout: 600 mto.vector.service_url: 'http://127.0.0.1:8090' + mto.commerce.enabled: true + mto.commerce.max_shop_results: '%env(SHOPWARE_STORE_API_MAX_RESULT)%' + mto.commerce.shop_timeout: 5 + mto.commerce.store_api_base_url: '%env(SHOPWARE_STORE_API_BASE_URL)%' + mto.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%' + # ------------------------------------------------------------ # Services @@ -99,10 +105,31 @@ services: App\Knowledge\Retrieval\NdjsonHybridRetriever: ~ - # CachedRetriever entfernt: war Interface-inkompatibel und erzeugt Drift/Chaos App\Knowledge\Retrieval\RetrieverInterface: alias: App\Knowledge\Retrieval\NdjsonHybridRetriever + # ------------------------------------------------------------ + # Commerce / Shopware Store API + # ------------------------------------------------------------ + + App\Intent\CommerceIntentLite: ~ + + App\Commerce\CommerceQueryParser: ~ + + App\Shopware\ShopwareCriteriaBuilder: ~ + + App\Shopware\StoreApiClient: + arguments: + $baseUrl: '%mto.commerce.store_api_base_url%' + $salesChannelAccessKey: '%mto.commerce.sales_channel_access_key%' + $timeoutSeconds: '%mto.commerce.shop_timeout%' + + App\Commerce\ShopSearchService: + arguments: + $enabled: '%mto.commerce.enabled%' + $maxResults: '%mto.commerce.max_shop_results%' + $baseUrl: '%mto.commerce.store_api_base_url%' + # ------------------------------------------------------------ # Index Configuration Provider # ------------------------------------------------------------ diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index 957165f..0be01ec 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -4,9 +4,11 @@ declare(strict_types=1); namespace App\Agent; +use App\Commerce\ShopSearchService; use App\Context\ContextService; use App\Context\UrlAnalyzer; use App\Infrastructure\OllamaClient; +use App\Intent\CommerceIntentLite; use App\Knowledge\Retrieval\RetrieverInterface; use Generator; use Psr\Log\LoggerInterface; @@ -21,6 +23,8 @@ final readonly class AgentRunner private ContextService $contextService, private UrlAnalyzer $urlAnalyzer, private RetrieverInterface $retriever, + private ShopSearchService $shopSearchService, + private CommerceIntentLite $commerceIntentLite, private OllamaClient $ollamaClient, private LoggerInterface $agentLogger, private bool $debug, @@ -33,6 +37,7 @@ final readonly class AgentRunner public function run(string $prompt, string $userId, ?bool $includeFullContext = false): Generator { $prompt = trim($prompt); + $shopResults = []; if ($prompt === '') { yield '❌ Empty prompt.'; @@ -57,16 +62,46 @@ final readonly class AgentRunner // --------------------------------------------------------- // 3) Retrieve RAG knowledge // --------------------------------------------------------- + yield "Hole Daten aus dem RAG Wissen... \n"; + $knowledgeChunks = $this->retriever->retrieve($prompt); // --------------------------------------------------------- - // 4) Build final prompt + // 4) Optional commerce/shop search + // --------------------------------------------------------- + + $commerceMeta = $this->commerceIntentLite->detect($prompt); + $commerceIntent = (string) ($commerceMeta['intent'] ?? CommerceIntentLite::NONE); + + if($commerceIntent === CommerceIntentLite::ADVISORY_PRODUCT_SEARCH || $commerceIntent === CommerceIntentLite::PRODUCT_SEARCH){ + yield "Rufe Shop auf (type: ".$commerceIntent.")... \n"; + $shopResults = $this->shopSearchService->search($prompt,$commerceIntent); + } + + if ($commerceIntent === CommerceIntentLite::PRODUCT_SEARCH) { + $knowledgeChunks = array_slice($knowledgeChunks, 0, 2); + } elseif ($commerceIntent === CommerceIntentLite::ADVISORY_PRODUCT_SEARCH) { + $knowledgeChunks = array_slice($knowledgeChunks, 0, 3); + } + + if($shopResults){ + yield "Verarbeite Shopdaten... \n
\n"; + }else{ + yield "Keine releveanten Shopdaten gefunden... \n
\n"; + } + + yield "Denke nach...\n
\n"; + + + // --------------------------------------------------------- + // 5) Build final prompt // --------------------------------------------------------- $finalPrompt = $this->promptBuilder->build( prompt: $prompt, userId: $userId, urlContent: $urlContent, knowledgeChunks: $knowledgeChunks, + shopResults: $shopResults, fullContext: $includeFullContext ); @@ -84,7 +119,7 @@ final readonly class AgentRunner } // --------------------------------------------------------- - // 5) Stream tokens from the LLM backend (chunked streaming) + // 6) Stream tokens from the LLM backend (chunked streaming) // --------------------------------------------------------- $fullOutput = ''; $chunker = new StreamChunker(); @@ -120,7 +155,7 @@ final readonly class AgentRunner } // --------------------------------------------------------- - // 6) Persist conversation history + // 7) Persist conversation history // --------------------------------------------------------- $this->contextService->appendHistory( $userId, @@ -132,6 +167,8 @@ final readonly class AgentRunner 'userId' => $userId, 'outputLength' => mb_strlen($fullOutput), 'contextMode' => 'recent', + 'commerceIntent' => $commerceIntent, + 'shopResultsCount' => count($shopResults), ]); } catch (Throwable $e) { $this->agentLogger->error('Agent run failed', [ @@ -142,4 +179,4 @@ final readonly class AgentRunner yield "\n❌ An internal error occurred while processing the request. \nError: " . $e->getMessage(); } } -} +} \ No newline at end of file diff --git a/src/Agent/PromptBuilder.php b/src/Agent/PromptBuilder.php index 8029020..21e73cb 100644 --- a/src/Agent/PromptBuilder.php +++ b/src/Agent/PromptBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Agent; +use App\Commerce\Dto\ShopProductResult; use App\Context\ContextService; use App\Repository\SystemPromptRepository; use DateTimeImmutable; @@ -24,6 +25,7 @@ final readonly class PromptBuilder * @param string $userId * @param string $urlContent * @param string[] $knowledgeChunks + * @param ShopProductResult[] $shopResults * @param bool $fullContext * @return string */ @@ -32,6 +34,7 @@ final readonly class PromptBuilder string $userId, string $urlContent, array $knowledgeChunks, + array $shopResults = [], ?bool $fullContext = false, ): string { @@ -69,7 +72,59 @@ final readonly class PromptBuilder } // ------------------------------------------------------------ - // 3) EXTERNAL KNOWLEDGE (SUPPORTING) + // 3) LIVE SHOP RESULTS (AUTHORITATIVE FOR PRODUCTS) + // ------------------------------------------------------------ + $shopBlock = ''; + + if ($shopResults !== []) { + $lines = []; + + foreach ($shopResults as $i => $product) { + if (!$product instanceof ShopProductResult) { + continue; + } + + $n = $i + 1; + $parts = [ + "[{$n}] " . $product->name, + ]; + + if ($product->productNumber) { + $parts[] = "Product number: " . $product->productNumber; + } + + if ($product->manufacturer) { + $parts[] = "Manufacturer: " . $product->manufacturer; + } + + if ($product->price) { + $parts[] = "Price: " . $product->price; + } + + if ($product->available !== null) { + $parts[] = "Available: " . ($product->available ? 'yes' : 'no'); + } + + foreach ($product->highlights as $highlight) { + $parts[] = "- " . $highlight; + } + + if ($product->url) { + $parts[] = "URL: " . $product->url; + } + + $lines[] = implode("\n", $parts); + } + + if ($lines !== []) { + $shopBlock = + "LIVE SHOP RESULTS (authoritative for products):\n" . + implode("\n\n", $lines); + } + } + + // ------------------------------------------------------------ + // 4) EXTERNAL KNOWLEDGE (SUPPORTING) // ------------------------------------------------------------ $knowledgeParts = []; @@ -98,22 +153,23 @@ final readonly class PromptBuilder } // ------------------------------------------------------------ - // 4) USER QUESTION + // 5) USER QUESTION // ------------------------------------------------------------ $userBlock = "USER QUESTION:\n" . $prompt; // ------------------------------------------------------------ - // 5) FINAL PROMPT ASSEMBLY + // 6) FINAL PROMPT ASSEMBLY // ------------------------------------------------------------ $blocks = array_filter([ $systemBlock, $contextBlock, + $shopBlock, $knowledgeBlock, $userBlock, ]); return implode("\n\n", $blocks); } -} +} \ No newline at end of file diff --git a/src/Command/TestShopSearchCommand.php b/src/Command/TestShopSearchCommand.php new file mode 100644 index 0000000..1481197 --- /dev/null +++ b/src/Command/TestShopSearchCommand.php @@ -0,0 +1,72 @@ +addArgument( + 'query', + InputArgument::OPTIONAL, + 'Die zu testende Suchanfrage', + 'zeige mir testomat modelle wasserhärte unter 5000 euro' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $query = (string) $input->getArgument('query'); + + $output->writeln('Test query: ' . $query); + $output->writeln(''); + + $results = $this->shopSearchService->search($query); + + if ($results === []) { + $output->writeln('Keine Shop-Ergebnisse gefunden.'); + + return Command::SUCCESS; + } + + foreach ($results as $index => $result) { + $n = $index + 1; + + $output->writeln(sprintf('[%d] %s', $n, $result->name)); + $output->writeln(' ID: ' . $result->id); + $output->writeln(' Produktnummer: ' . ($result->productNumber ?? '-')); + $output->writeln(' Hersteller: ' . ($result->manufacturer ?? '-')); + $output->writeln(' Preis: ' . ($result->price ?? '-')); + $output->writeln(' Verfügbar: ' . ($result->available === null ? '-' : ($result->available ? 'ja' : 'nein'))); + $output->writeln(' URL: ' . ($result->url ?? '-')); + $output->writeln(' Description: ' . ($result->description ?? '-')); + + if ($result->highlights !== []) { + $output->writeln(' Highlights:'); + foreach ($result->highlights as $highlight) { + $output->writeln(' - ' . $highlight); + } + } + + $output->writeln(''); + } + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commerce/CommerceQueryParser.php b/src/Commerce/CommerceQueryParser.php new file mode 100644 index 0000000..1a7e950 --- /dev/null +++ b/src/Commerce/CommerceQueryParser.php @@ -0,0 +1,267 @@ +normalize($originalPrompt); + + [$priceMin, $priceMax] = $this->extractPriceRange($normalized); + $sizes = $this->extractSizes($normalized); + $colors = $this->extractColors($normalized); + $brand = $this->extractBrand($normalized); + $category = $this->extractCategory($normalized); + $properties = []; + + $searchText = $this->buildSearchText( + $normalized, + $colors, + $sizes, + $brand, + $priceMin, + $priceMax + ); + + return new CommerceSearchQuery( + originalPrompt: $originalPrompt, + normalizedPrompt: $normalized, + searchText: $searchText !== '' ? $searchText : $normalized, + category: $category, + brand: $brand, + colors: $colors, + sizes: $sizes, + properties: $properties, + priceMin: $priceMin, + priceMax: $priceMax, + intent: $intent, + needsLlmFallback: false, + ); + } + + private function normalize(string $prompt): string + { + $value = mb_strtolower(trim($prompt)); + $value = str_replace(['€'], ' euro ', $value); + $value = preg_replace('/[^\p{L}\p{N}\s.,\-]/u', ' ', $value) ?? $value; + $value = preg_replace('/\s+/u', ' ', $value) ?? $value; + + return trim($value); + } + + /** + * @return array{0:?float,1:?float} + */ + private function extractPriceRange(string $prompt): array + { + $priceMin = 10; + $priceMax = null; + + if (preg_match('/\bzwischen\s+(\d+(?:[.,]\d+)?)\s+und\s+(\d+(?:[.,]\d+)?)\s+euro\b/u', $prompt, $m) === 1) { + $a = $this->toFloat($m[1]); + $b = $this->toFloat($m[2]); + + if ($a !== null && $b !== null) { + return [min($a, $b), max($a, $b)]; + } + } + + if (preg_match('/\b(?:unter|bis|max(?:imal)?)\s+(\d+(?:[.,]\d+)?)\s+euro\b/u', $prompt, $m) === 1) { + $priceMax = $this->toFloat($m[1]); + } + + if (preg_match('/\b(?:ab|mindestens|min)\s+(\d+(?:[.,]\d+)?)\s+euro\b/u', $prompt, $m) === 1) { + $priceMin = $this->toFloat($m[1]); + } + + return [$priceMin, $priceMax]; + } + + /** + * @return string[] + */ + private function extractSizes(string $prompt): array + { + $sizes = []; + + if (preg_match_all('/\b(?:größe|groesse|grösse)\s*([a-z0-9.-]+)\b/u', $prompt, $matches) === false) { + return []; + } + + foreach ($matches[1] as $size) { + $sizes[] = trim($size); + } + + if (preg_match_all('/\b(xs|s|m|l|xl|xxl|xxxl)\b/u', $prompt, $tokenMatches) !== false) { + foreach ($tokenMatches[1] as $sizeToken) { + $sizes[] = trim($sizeToken); + } + } + + return array_values(array_unique(array_filter($sizes, static fn ($v) => $v !== ''))); + } + + /** + * @return string[] + */ + private function extractColors(string $prompt): array + { + $colors = []; + + foreach ($this->knownColors as $color) { + if (preg_match('/\b' . preg_quote($color, '/') . '\b/u', $prompt) === 1) { + $colors[] = $color; + } + } + + return array_values(array_unique($colors)); + } + + private function extractBrand(string $prompt): ?string + { + foreach ($this->knownBrands as $brand) { + if (str_contains($prompt, $brand)) { + return $brand; + } + } + + if (preg_match('/\bmarke\s+([a-z0-9][a-z0-9\s\-]+)/u', $prompt, $m) === 1) { + return trim($m[1]); + } + + return null; + } + + private function extractCategory(string $prompt): ?string + { + foreach ($this->knownCategories as $category) { + if (preg_match('/\b' . preg_quote($category, '/') . '\b/u', $prompt) === 1) { + return $category; + } + } + + return null; + } + + private function buildSearchText( + string $prompt, + array $colors, + array $sizes, + ?string $brand, + ?float $priceMin, + ?float $priceMax + ): string { + $text = ' ' . $prompt . ' '; + + $phrasesToRemove = [ + 'ich suche', + 'suche', + 'habt ihr', + 'gibt es', + 'zeige mir', + 'welches gerät', + 'welche gerät', + 'welches modell', + 'welches ist besser', + 'welches ist am besten', + 'alternative', + 'alternativen', + ]; + + foreach ($phrasesToRemove as $phrase) { + $text = str_replace($phrase, ' ', $text); + } + + foreach ($colors as $color) { + $text = preg_replace('/\b' . preg_quote($color, '/') . '\b/u', ' ', $text) ?? $text; + } + + foreach ($sizes as $size) { + $text = preg_replace('/\b' . preg_quote($size, '/') . '\b/u', ' ', $text) ?? $text; + } + + if ($brand !== null && $brand !== '') { + $text = str_replace($brand, ' ', $text); + } + + if ($priceMin !== null || $priceMax !== null) { + if ($priceMin !== null || $priceMax !== null) { + $text = preg_replace('/\bzwischen\s+\d+(?:[.,]\d+)?\s+und\s+\d+(?:[.,]\d+)?\s*euro\b/u', ' ', $text) ?? $text; + $text = preg_replace('/\b(?:unter|bis|max(?:imal)?|ab|mindestens|min)\s+\d+(?:[.,]\d+)?\s*euro\b/u', ' ', $text) ?? $text; + $text = preg_replace('/\beuro\b/u', ' ', $text) ?? $text; + } + } + + $text = preg_replace('/\s+/u', ' ', $text) ?? $text; + $text = trim($text, " \t\n\r\0\x0B-.,"); + $tokens = array_filter(explode(' ', $text), static fn (string $token): bool => mb_strlen($token) > 1); + + return trim(implode(' ', $tokens)); + } + + private function toFloat(string $value): ?float + { + $value = str_replace(',', '.', trim($value)); + + return is_numeric($value) ? (float) $value : null; + } +} \ No newline at end of file diff --git a/src/Commerce/Dto/CommerceSearchQuery.php b/src/Commerce/Dto/CommerceSearchQuery.php new file mode 100644 index 0000000..7cb12ea --- /dev/null +++ b/src/Commerce/Dto/CommerceSearchQuery.php @@ -0,0 +1,29 @@ +enabled) { + return []; + } + + $query = $this->queryParser->parse($originalPrompt, $commerceIntent); + $criteria = $this->criteriaBuilder->build($query, $this->maxResults); + $response = $this->storeApiClient->searchProducts($criteria); + + return $this->mapProducts($response); + } + + /** + * @return ShopProductResult[] + */ + private function mapProducts(array $response): array + { + $elements = $response['elements'] ?? []; + if (!is_array($elements)) { + return []; + } + + $results = []; + + foreach ($elements as $row) { + if (!is_array($row)) { + continue; + } + + $results[] = new ShopProductResult( + id: (string)($row['id'] ?? ''), + name: trim((string)($row['translated']['name'] ?? '')), + productNumber: isset($row['productNumber']) ? (string)$row['productNumber'] : null, + price: $this->extractPrice($row), + available: isset($row['available']) ? (bool)$row['available'] : null, + url: $this->baseUrl . $this->extractUrl($row), + highlights: $this->extractHighlights($row), + description: $this->cleanUpDescription($row), + ); + } + + return array_values(array_filter( + $results, + static fn(ShopProductResult $product): bool => $product->name !== '' + )); + } + + private function cleanUpDescription($description): string + { + if (isset($description['translated']['description'])) { + $newDesc = strip_tags((string)$description['translated']['description']); + $newDesc = preg_replace('/^[ \t]*\R/m', '', $newDesc); // leere Zeilen weg + $newDesc = preg_replace('/[ \t]{2,}/', ' ', $newDesc); // mehrere Spaces zu einem + $result = trim($newDesc); + return substr($result, 0, 500); + } + + return ''; + } + + private function extractManufacturer(array $row): ?string + { + $manufacturer = $row['manufacturer'] ?? null; + + if (is_array($manufacturer) && isset($manufacturer['name']) && is_string($manufacturer['name'])) { + return trim($manufacturer['name']) !== '' ? trim($manufacturer['name']) : null; + } + + return null; + } + + private function extractPrice(array $row): ?string + { + $calculatedPrice = $row['calculatedPrice'] ?? null; + + if (!is_array($calculatedPrice)) { + return null; + } + + $unitPrice = $calculatedPrice['unitPrice'] ?? $calculatedPrice['totalPrice'] ?? $calculatedPrice['referencePrice'] ?? $calculatedPrice['listPrice'] ?? $calculatedPrice['regulationPrice'] ?? 0; + if (!is_numeric($unitPrice)) { + return null; + } + + return number_format((float)$unitPrice, 2, ',', '.') . ' €'; + } + + private function extractUrl(array $row): ?string + { + $seoUrls = $row['seoUrls'] ?? null; + + if (!is_array($seoUrls) || $seoUrls === []) { + return null; + } + + foreach ($seoUrls as $seoUrl) { + if (!is_array($seoUrl)) { + continue; + } + + $path = $seoUrl['seoPathInfo'] ?? null; + if (is_string($path) && trim($path) !== '') { + return '/' . ltrim($path, '/'); + } + } + + return null; + } + + /** + * @return string[] + */ + private function extractHighlights(array $row): array + { + $highlights = []; + + if (isset($row['available'])) { + $highlights[] = ((bool)$row['available']) ? 'Verfügbar' : 'Nicht verfügbar'; + } + + if (isset($row['productNumber']) && is_string($row['productNumber']) && trim($row['productNumber']) !== '') { + $highlights[] = 'Produktnummer: ' . trim($row['productNumber']); + } + + return array_values(array_unique($highlights)); + } +} \ No newline at end of file diff --git a/src/Intent/CommerceIntentLite.php b/src/Intent/CommerceIntentLite.php new file mode 100644 index 0000000..700036f --- /dev/null +++ b/src/Intent/CommerceIntentLite.php @@ -0,0 +1,120 @@ + self::NONE, + 'score' => 0, + 'signals' => [], + ]; + } + + $score = 0; + $signals = []; + + $strongSignals = [ + 'suche', + 'habt', + 'gibt', + 'zeig', + 'welche', + 'vergleich', + 'alternativ', + 'find', + 'shop', + 'sku', + 'Artikel', + 'Gerät' + ]; + + foreach ($strongSignals as $signal) { + if (str_contains($p, $signal)) { + $score += 2; + $signals[] = $signal; + } + } + + if(preg_match('#\d{3,10}#', $p)){ + $score += 2; + $signals[] = 'sku'; + } + + if (preg_match('/\b\d+(?:[.,]\d+)?\s*(euro|€|eur|teuer|preis|kosten)\b/u', $p) === 1) { + $score += 2; + $signals[] = 'price'; + } + + if (preg_match('/\b(größe|groesse|grösse)\s*[a-z0-9.-]+\b/u', $p) === 1) { + $score += 2; + $signals[] = 'size'; + } + + if (preg_match('/\b(xs|s|m|l|xl|xxl|xxxl)\b/u', $p) === 1) { + $score += 1; + $signals[] = 'size_token'; + } + + if (preg_match('/\b(schwarz|weiß|weiss|rot|blau|grün|gruen|gelb|grau|beige|rosa|pink|orange|braun)\b/u', $p) === 1) { + $score += 1; + $signals[] = 'color'; + } + + $advisorySignals = [ + 'passt', + 'eignet', + 'besser', + 'besten', + 'geeignet', + 'empfiehl', + 'empfehl', + ]; + + foreach ($advisorySignals as $signal) { + if (str_contains($p, $signal)) { + $score += 1; + $signals[] = 'advisory:' . $signal; + } + } + + $signals = array_values(array_unique($signals)); + + if ($score >= 3) { + return [ + 'intent' => self::PRODUCT_SEARCH, + 'score' => $score, + 'signals' => $signals, + ]; + } + + if ($score >= 2) { + return [ + 'intent' => self::ADVISORY_PRODUCT_SEARCH, + 'score' => $score, + 'signals' => $signals, + ]; + } + + return [ + 'intent' => self::NONE, + 'score' => $score, + 'signals' => $signals, + ]; + } + +} \ No newline at end of file diff --git a/src/Knowledge/Retrieval/CachedRetriever.php b/src/Knowledge/Retrieval/CachedRetriever.php new file mode 100644 index 0000000..d2ae610 --- /dev/null +++ b/src/Knowledge/Retrieval/CachedRetriever.php @@ -0,0 +1,50 @@ +buildCacheKey($prompt); + + $item = $this->cache->getItem($key); + if ($item->isHit()) { + $cached = $item->get(); + + return is_array($cached) ? $cached : []; + } + + $result = $this->inner->retrieve($prompt); + + $item->set($result); + $item->expiresAfter($this->ttlSeconds); + $this->cache->save($item); + + return $result; + } + + private function buildCacheKey(string $prompt): string + { + $normalized = mb_strtolower(trim($prompt)); + $normalized = preg_replace('/\s+/u', ' ', $normalized) ?? $normalized; + + return 'rag_retrieval_' . sha1($normalized); + } +} \ No newline at end of file diff --git a/src/Shopware/ShopwareCriteriaBuilder.php b/src/Shopware/ShopwareCriteriaBuilder.php new file mode 100644 index 0000000..d672ee4 --- /dev/null +++ b/src/Shopware/ShopwareCriteriaBuilder.php @@ -0,0 +1,93 @@ + 1, + 'limit' => max(1, $limit), + "grouping" => ["parentId"], + 'total-count-mode' => 0, + 'includes' => [ + 'product' => [ + 'id', + 'name', + 'description', + 'productNumber', + 'available', + 'calculatedPrice', + 'seoUrls', + 'manufacturer', + 'translated.name' + ], + 'product_manufacturer' => [ + 'name', + ], + 'calculated_price' => [ + 'unitPrice', + 'totalPrice', + 'referencePrice', + 'listPrice', + 'regulationPrice' + ], + 'seo_url' => [ + 'seoPathInfo', + ], + ], + 'associations' => [ + 'manufacturer' => new \stdClass(), + 'seoUrls' => new \stdClass(), + ], + 'sort' => [ + [ + 'field' => 'name', + 'order' => 'ASC', + 'naturalSorting' => true, + ], + ], + ]; + + if ($query->searchText !== '') { + $criteria['term'] = $query->searchText; + } + + $filters = [ + [ + 'type' => 'equals', + 'field' => 'active', + 'value' => true, + ], + [ + 'type' => 'equals', + 'field' => 'available', + 'value' => true, + ], + [ + 'type' => 'range', + 'field' => 'price.gross', + 'parameters' => [ + 'gt' => 0, + ], + ] + ]; + + if ($query->priceMin !== null) { + $criteria['min-price'] = $query->priceMin; + } + + if ($query->priceMax !== null) { + $criteria['max-price'] = $query->priceMax; + } + + $criteria['filter'] = $filters; + + return $criteria; + } +} \ No newline at end of file diff --git a/src/Shopware/StoreApiClient.php b/src/Shopware/StoreApiClient.php new file mode 100644 index 0000000..ccaa4eb --- /dev/null +++ b/src/Shopware/StoreApiClient.php @@ -0,0 +1,53 @@ +baseUrl, '/') . '/store-api/product'; + + $response = $this->httpClient->request('POST', $url, [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'sw-access-key' => $this->salesChannelAccessKey, + ], + 'json' => $criteria, + 'timeout' => $this->timeoutSeconds, + ]); + + $statusCode = $response->getStatusCode(); + if ($statusCode < 200 || $statusCode >= 300) { + return []; + } + + return $response->toArray(false); + } +} \ No newline at end of file diff --git a/templates/admin/security/login.html.twig b/templates/admin/security/login.html.twig index c12bd05..e7704fe 100644 --- a/templates/admin/security/login.html.twig +++ b/templates/admin/security/login.html.twig @@ -11,7 +11,7 @@

- mitho® KI RAG Login + Heyl-Neomeris KI/RAG Login

{% if error %}