fix stream error handling

This commit is contained in:
team 1
2026-04-25 12:19:20 +02:00
parent 2f28ad0416
commit fa65417efe
9 changed files with 435 additions and 62 deletions

View File

@@ -10,30 +10,44 @@ use App\Commerce\Dto\ShopProductResult;
use App\Config\ShopServiceConfig;
use App\Shopware\ShopwareCriteriaBuilder;
use App\Shopware\StoreApiClient;
use App\Shopware\StoreApiException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
final readonly class ShopSearchService
final class ShopSearchService
{
private const FOCUS_NEUTRAL = 'neutral';
private const FOCUS_DEVICE = 'device';
private const FOCUS_ACCESSORY = 'accessory';
private bool $lastSearchHadSystemFailure = false;
private ?string $lastSearchFailureReason = null;
public function __construct(
private CommerceQueryParser $queryParser,
private ShopwareCriteriaBuilder $criteriaBuilder,
private StoreApiClient $storeApiClient,
private ShopServiceConfig $shopConfig,
private LoggerInterface $logger,
private bool $enabled = true,
private int $maxResults = 25,
private string $baseUrl = ''
private readonly CommerceQueryParser $queryParser,
private readonly ShopwareCriteriaBuilder $criteriaBuilder,
private readonly StoreApiClient $storeApiClient,
private readonly ShopServiceConfig $shopConfig,
private readonly LoggerInterface $logger,
private readonly bool $enabled = true,
private readonly int $maxResults = 25,
private readonly string $baseUrl = ''
) {
}
public function hadLastSearchSystemFailure(): bool
{
return $this->lastSearchHadSystemFailure;
}
public function getLastSearchFailureReason(): ?string
{
return $this->lastSearchFailureReason;
}
/**
* @return ShopProductResult[]
*/
@@ -43,6 +57,8 @@ final readonly class ShopSearchService
string $commerceHistoryContext = '',
?CommerceReferenceContext $referenceContext = null
): array {
$this->resetLastSearchFailure();
if (!$this->enabled) {
$this->logger->info('Shop search skipped because commerce search is disabled', [
'commerceIntent' => $commerceIntent,
@@ -335,30 +351,114 @@ final readonly class ShopSearchService
try {
$response = $this->storeApiClient->searchProducts($criteria);
return $this->mapAndLogSearchResponse(
response: $response,
query: $query,
commerceIntent: $commerceIntent,
originalPrompt: $originalPrompt,
usesHistoryContext: $usesHistoryContext,
usedSafeCriteria: false
);
} catch (StoreApiException $e) {
if ($e->isSafeCriteriaRetryRecommended()) {
$safeResults = $this->retryWithSafeCriteria(
query: $query,
commerceIntent: $commerceIntent,
originalPrompt: $originalPrompt,
usesHistoryContext: $usesHistoryContext,
previousException: $e
);
if ($safeResults !== null) {
return $safeResults;
}
}
$this->recordFailedSearch($e);
$this->logShopSearchFailure($query, $commerceIntent, $originalPrompt, $usesHistoryContext, $e);
return [];
} catch (
ClientExceptionInterface |
RedirectionExceptionInterface |
ServerExceptionInterface |
TransportExceptionInterface |
\RuntimeException $e
ClientExceptionInterface |
RedirectionExceptionInterface |
ServerExceptionInterface |
TransportExceptionInterface |
\RuntimeException $e
) {
$this->logger->warning('Shop search request failed', [
$this->recordFailedSearch($e);
$this->logShopSearchFailure($query, $commerceIntent, $originalPrompt, $usesHistoryContext, $e);
return [];
}
}
/**
* @return ShopProductResult[]|null
*/
private function retryWithSafeCriteria(
CommerceSearchQuery $query,
string $commerceIntent,
string $originalPrompt,
bool $usesHistoryContext,
StoreApiException $previousException
): ?array {
$this->logger->warning('Shop search retrying with safe criteria', [
'commerceIntent' => $commerceIntent,
'originalPrompt' => $originalPrompt,
'normalizedPrompt' => $query->normalizedPrompt,
'searchText' => $query->searchText,
'usesHistoryContext' => $usesHistoryContext,
'previousStatusCode' => $previousException->getStatusCode(),
'previousUtf8Failure' => $previousException->isUtf8Failure(),
'previousExceptionMessage' => $previousException->getMessage(),
]);
try {
$safeCriteria = $this->criteriaBuilder->buildSafe($query, $this->maxResults);
$response = $this->storeApiClient->searchProducts($safeCriteria);
return $this->mapAndLogSearchResponse(
response: $response,
query: $query,
commerceIntent: $commerceIntent,
originalPrompt: $originalPrompt,
usesHistoryContext: $usesHistoryContext,
usedSafeCriteria: true
);
} catch (
ClientExceptionInterface |
RedirectionExceptionInterface |
ServerExceptionInterface |
TransportExceptionInterface |
\RuntimeException $safeException
) {
$this->recordFailedSearch($safeException);
$this->logger->warning('Shop search safe criteria retry failed', [
'commerceIntent' => $commerceIntent,
'originalPrompt' => $originalPrompt,
'normalizedPrompt' => $query->normalizedPrompt,
'searchText' => $query->searchText,
'brand' => $query->brand,
'sizes' => $query->sizes,
'priceMin' => $query->priceMin,
'priceMax' => $query->priceMax,
'usesHistoryContext' => $usesHistoryContext,
'exceptionClass' => $e::class,
'exceptionMessage' => $e->getMessage(),
'exceptionClass' => $safeException::class,
'exceptionMessage' => $safeException->getMessage(),
]);
return [];
return null;
}
}
/**
* @param array<mixed> $response
* @return ShopProductResult[]
*/
private function mapAndLogSearchResponse(
array $response,
CommerceSearchQuery $query,
string $commerceIntent,
string $originalPrompt,
bool $usesHistoryContext,
bool $usedSafeCriteria
): array {
$mappedProducts = $this->mapProducts($response);
$rankedProducts = $this->rerankProducts($mappedProducts, $query);
@@ -372,6 +472,7 @@ final readonly class ShopSearchService
'priceMin' => $query->priceMin,
'priceMax' => $query->priceMax,
'usesHistoryContext' => $usesHistoryContext,
'usedSafeCriteria' => $usedSafeCriteria,
'rawElementsCount' => is_array($response['elements'] ?? null) ? count($response['elements']) : 0,
'mappedProductsCount' => count($mappedProducts),
'rankedProductsCount' => count($rankedProducts),
@@ -380,6 +481,50 @@ final readonly class ShopSearchService
return $rankedProducts;
}
private function logShopSearchFailure(
CommerceSearchQuery $query,
string $commerceIntent,
string $originalPrompt,
bool $usesHistoryContext,
\Throwable $e
): void {
$this->logger->warning('Shop search request failed', [
'commerceIntent' => $commerceIntent,
'originalPrompt' => $originalPrompt,
'normalizedPrompt' => $query->normalizedPrompt,
'searchText' => $query->searchText,
'brand' => $query->brand,
'sizes' => $query->sizes,
'priceMin' => $query->priceMin,
'priceMax' => $query->priceMax,
'usesHistoryContext' => $usesHistoryContext,
'systemFailure' => $this->lastSearchHadSystemFailure,
'failureReason' => $this->lastSearchFailureReason,
'exceptionClass' => $e::class,
'exceptionMessage' => $e->getMessage(),
]);
}
private function resetLastSearchFailure(): void
{
$this->lastSearchHadSystemFailure = false;
$this->lastSearchFailureReason = null;
}
private function recordFailedSearch(\Throwable $e): void
{
$isSystemFailure = $e instanceof StoreApiException
? $e->isSystemFailure()
: $e instanceof ServerExceptionInterface || $e instanceof TransportExceptionInterface;
if (!$isSystemFailure) {
return;
}
$this->lastSearchHadSystemFailure = true;
$this->lastSearchFailureReason = $e->getMessage();
}
/**
* @param ShopProductResult[] $referenceProbeResults
* @param ShopProductResult[] $rankedProducts