feat: extract modelName and modelNumber separately in vision pipeline

Vision prompt now distinguishes MODEL_NAME (human-readable product name,
e.g. "ThinkPad T490s") from MODEL_NUMBER (part/product code, e.g.
"20NXS0BA00"). Both fields flow through the pipeline and are written to
the article. Specs research uses model number as search subject when
available, falling back to model name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kuehn 2026-05-18 10:09:26 +00:00
parent c10c306a5a
commit 525424a6a1
6 changed files with 24 additions and 12 deletions

View file

@ -16,7 +16,7 @@ final class OllamaVisionAgent
) { ) {
} }
/** @return array{manufacturer: string, model: string, serial: string} */ /** @return array{manufacturer: string, modelName: string, modelNumber: string, serial: string} */
public function analyze(string $imagePath): array public function analyze(string $imagePath): array
{ {
$prompt = $this->prompts->render('vision_analyze'); $prompt = $this->prompts->render('vision_analyze');
@ -25,7 +25,8 @@ final class OllamaVisionAgent
return [ return [
'manufacturer' => $this->extractField($response, 'MANUFACTURER'), 'manufacturer' => $this->extractField($response, 'MANUFACTURER'),
'model' => $this->extractField($response, 'MODEL'), 'modelName' => $this->extractField($response, 'MODEL_NAME'),
'modelNumber' => $this->extractField($response, 'MODEL_NUMBER'),
'serial' => $this->extractField($response, 'SERIAL'), 'serial' => $this->extractField($response, 'SERIAL'),
]; ];
} }

View file

@ -43,11 +43,14 @@ PROMPT,
'vision_analyze' => <<<'PROMPT' 'vision_analyze' => <<<'PROMPT'
Look at this nameplate/label photo of IT hardware. Look at this nameplate/label photo of IT hardware.
Extract the manufacturer (brand), model number/designation, and serial number visible on the label. Extract the manufacturer, model name, model number, and serial number visible on the label.
Do not guess or add information not on the label. - MODEL_NAME is the human-readable product name (e.g. "ThinkPad T490s", "EliteBook 840 G6", "Galaxy S24 Ultra").
- MODEL_NUMBER is the part/product code or designation (e.g. "20NXS0BA00", "5SS67UC", "SM-S928B"). Often printed as "Model No.", "Part No.", "Type", or "P/N".
Do not guess or add information not visible on the label.
Respond in exactly this format (use empty string if not visible): Respond in exactly this format (use empty string if not visible):
MANUFACTURER: <brand name, e.g. Dell, HP, Lenovo, Medion> MANUFACTURER: <brand name, e.g. Dell, HP, Lenovo, Medion>
MODEL: <model number or designation> MODEL_NAME: <human-readable product name>
MODEL_NUMBER: <part/product code or model number>
SERIAL: <serial number> SERIAL: <serial number>
PROMPT, PROMPT,

View file

@ -67,8 +67,11 @@ final class DraftArticleHandler
if (isset($vision['manufacturer']) && '' !== $vision['manufacturer']) { if (isset($vision['manufacturer']) && '' !== $vision['manufacturer']) {
$article->setManufacturer((string) $vision['manufacturer']); $article->setManufacturer((string) $vision['manufacturer']);
} }
if (isset($vision['model']) && '' !== $vision['model']) { if (isset($vision['modelNumber']) && '' !== $vision['modelNumber']) {
$article->setModelNumber((string) $vision['model']); $article->setModelNumber((string) $vision['modelNumber']);
}
if (isset($vision['modelName']) && '' !== $vision['modelName']) {
$article->setModelName((string) $vision['modelName']);
} }
if ([] !== $message->attributes) { if ([] !== $message->attributes) {

View file

@ -37,13 +37,14 @@ final class PhotoUploadHandler
$job->recordStep('vision', [ $job->recordStep('vision', [
'manufacturer' => $result['manufacturer'], 'manufacturer' => $result['manufacturer'],
'model' => $result['model'], 'modelName' => $result['modelName'],
'modelNumber' => $result['modelNumber'],
'serial' => $result['serial'], 'serial' => $result['serial'],
]); ]);
$this->jobRepository->save($job); $this->jobRepository->save($job);
if ('' === $result['model']) { if ('' === $result['modelNumber'] && '' === $result['modelName']) {
$job->markNeedsReview('OllamaVisionAgent: no model name detected on nameplate'); $job->markNeedsReview('OllamaVisionAgent: no model detected on nameplate');
$this->jobRepository->save($job); $this->jobRepository->save($job);
return; return;
@ -52,7 +53,8 @@ final class PhotoUploadHandler
$this->bus->dispatch(new SpecsResearchMessage( $this->bus->dispatch(new SpecsResearchMessage(
jobId: $message->jobId, jobId: $message->jobId,
articleTypeId: $message->articleTypeId, articleTypeId: $message->articleTypeId,
modelName: $result['model'], modelNumber: $result['modelNumber'],
modelName: $result['modelName'],
serialNumber: $result['serial'], serialNumber: $result['serial'],
manufacturer: $result['manufacturer'], manufacturer: $result['manufacturer'],
)); ));

View file

@ -39,9 +39,11 @@ final class SpecsResearchHandler
return; return;
} }
$searchSubject = $message->modelNumber !== '' ? $message->modelNumber : $message->modelName;
try { try {
$specsText = $this->specsAgent->research( $specsText = $this->specsAgent->research(
$message->modelName, $searchSubject,
$articleType->getName(), $articleType->getName(),
$message->manufacturer, $message->manufacturer,
); );

View file

@ -9,6 +9,7 @@ final readonly class SpecsResearchMessage
public function __construct( public function __construct(
public string $jobId, public string $jobId,
public string $articleTypeId, public string $articleTypeId,
public string $modelNumber,
public string $modelName, public string $modelName,
public string $serialNumber, public string $serialNumber,
public string $manufacturer = '', public string $manufacturer = '',