Messages and handlers for the full AI pipeline: DraftArticle → Validation → SpecsResearch → PhotoUpload → EbayText → JsonCoding → PublishToChannel / DeactivateListingMessage / TrackingPush / UpdateStockOnChannels / OrderReceived. OllamaClient and OllamaClientInterface provide the base LLM backend. AI agents (EbayTextAgent, JsonCodingAgent, OllamaVisionAgent, SpecsResearchAgent) wrap the client with task-specific prompts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
94 lines
3 KiB
PHP
94 lines
3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Infrastructure\Messenger\Handler;
|
|
|
|
use App\Domain\Article\Repository\ArticleTypeRepositoryInterface;
|
|
use App\Domain\Pipeline\Repository\AIPipelineJobRepositoryInterface;
|
|
use App\Infrastructure\Messenger\Message\DraftArticleMessage;
|
|
use App\Infrastructure\Messenger\Message\JsonCodingMessage;
|
|
use App\Infrastructure\Messenger\Message\ValidationMessage;
|
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
|
use Symfony\Component\Messenger\MessageBusInterface;
|
|
use Symfony\Component\Uid\Uuid;
|
|
|
|
#[AsMessageHandler]
|
|
final class ValidationHandler
|
|
{
|
|
private const MAX_ATTEMPTS = 3;
|
|
|
|
public function __construct(
|
|
private readonly AIPipelineJobRepositoryInterface $jobRepository,
|
|
private readonly MessageBusInterface $bus,
|
|
private readonly ?ArticleTypeRepositoryInterface $articleTypeRepository = null,
|
|
) {
|
|
}
|
|
|
|
public function __invoke(ValidationMessage $message): void
|
|
{
|
|
$job = $this->jobRepository->findById(Uuid::fromString($message->jobId));
|
|
if (null === $job) {
|
|
return;
|
|
}
|
|
|
|
$missing = $this->findMissingFields($message);
|
|
|
|
if ([] === $missing) {
|
|
$this->bus->dispatch(new DraftArticleMessage(
|
|
jobId: $message->jobId,
|
|
articleTypeId: $message->articleTypeId,
|
|
attributes: $message->attributes,
|
|
condition: 'good',
|
|
inventoryNumber: null,
|
|
serialNumber: null,
|
|
));
|
|
|
|
return;
|
|
}
|
|
|
|
if ($job->getAttemptCount() >= self::MAX_ATTEMPTS) {
|
|
$job->markNeedsReview('Validation failed after '.self::MAX_ATTEMPTS.' attempts. Missing: '.implode(', ', $missing));
|
|
$this->jobRepository->save($job);
|
|
|
|
return;
|
|
}
|
|
|
|
$job->incrementAttempt($missing);
|
|
$this->jobRepository->save($job);
|
|
|
|
$this->bus->dispatch(new JsonCodingMessage(
|
|
jobId: $message->jobId,
|
|
articleTypeId: $message->articleTypeId,
|
|
specsText: $message->specsText,
|
|
missingFields: $missing,
|
|
));
|
|
}
|
|
|
|
/** @return list<string> attribute names that are required but not present */
|
|
private function findMissingFields(ValidationMessage $message): array
|
|
{
|
|
// Empty attributes always means retry
|
|
if ([] === $message->attributes) {
|
|
return ['(no attributes returned by LLM)'];
|
|
}
|
|
|
|
if (null === $this->articleTypeRepository) {
|
|
return [];
|
|
}
|
|
|
|
$articleType = $this->articleTypeRepository->findById(Uuid::fromString($message->articleTypeId));
|
|
if (null === $articleType) {
|
|
return [];
|
|
}
|
|
|
|
$missing = [];
|
|
foreach ($articleType->getRequiredAttributeDefinitions() as $def) {
|
|
if (!\array_key_exists($def->getId()->toRfc4122(), $message->attributes)) {
|
|
$missing[] = $def->getName();
|
|
}
|
|
}
|
|
|
|
return $missing;
|
|
}
|
|
}
|