diff --git a/src/Infrastructure/AI/Agent/OllamaVisionAgent.php b/src/Infrastructure/AI/Agent/OllamaVisionAgent.php index a52beb1..9927a5d 100644 --- a/src/Infrastructure/AI/Agent/OllamaVisionAgent.php +++ b/src/Infrastructure/AI/Agent/OllamaVisionAgent.php @@ -39,8 +39,9 @@ final class OllamaVisionAgent $value = trim($matches[1]); - // Strip any embedded field label the model mistakenly included (e.g. "SERIAL: PNV09SJZ") - $value = preg_replace('/\s+[A-Z_]+:.*$/i', '', $value) ?? $value; + // Strip any embedded field label the LLM mistakenly included + // Handles both leading labels ("SERIAL: xyz") and mid-value ones ("ThinkBook 14 SERIAL: xyz") + $value = preg_replace('/(?:^|\s+)[A-Z_]+:.*$/i', '', $value) ?? $value; return trim($value); } diff --git a/tests/Unit/Infrastructure/AI/Agent/JsonCodingAgentTest.php b/tests/Unit/Infrastructure/AI/Agent/JsonCodingAgentTest.php index 7f56d42..489ca93 100644 --- a/tests/Unit/Infrastructure/AI/Agent/JsonCodingAgentTest.php +++ b/tests/Unit/Infrastructure/AI/Agent/JsonCodingAgentTest.php @@ -7,8 +7,11 @@ namespace App\Tests\Unit\Infrastructure\AI\Agent; use App\Domain\Article\ArticleType; use App\Domain\Article\AttributeDefinition; use App\Domain\Article\AttributeType; +use App\Domain\AI\PromptTemplate; +use App\Domain\AI\Repository\PromptTemplateRepositoryInterface; use App\Infrastructure\AI\Agent\JsonCodingAgent; use App\Infrastructure\AI\OllamaClientInterface; +use App\Infrastructure\AI\PromptTemplateService; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -21,7 +24,12 @@ final class JsonCodingAgentTest extends TestCase protected function setUp(): void { $this->ollama = $this->createMock(OllamaClientInterface::class); - $this->agent = new JsonCodingAgent($this->ollama, 'llama3.2'); + + $repo = $this->createMock(PromptTemplateRepositoryInterface::class); + $repo->method('findByKey')->willReturn(new PromptTemplate('json_coding', '{{schema}}{{missingHint}}{{specsText}}')); + $prompts = new PromptTemplateService($repo); + + $this->agent = new JsonCodingAgent($this->ollama, $prompts, 'llama3.2'); $this->type = new ArticleType('Notebook'); $ramDef = new AttributeDefinition('RAM', AttributeType::String); $this->type->addAttributeDefinition($ramDef); diff --git a/tests/Unit/Infrastructure/AI/Agent/OllamaVisionAgentTest.php b/tests/Unit/Infrastructure/AI/Agent/OllamaVisionAgentTest.php index 5c8c93a..fc49c2f 100644 --- a/tests/Unit/Infrastructure/AI/Agent/OllamaVisionAgentTest.php +++ b/tests/Unit/Infrastructure/AI/Agent/OllamaVisionAgentTest.php @@ -4,8 +4,11 @@ declare(strict_types=1); namespace App\Tests\Unit\Infrastructure\AI\Agent; +use App\Domain\AI\PromptTemplate; +use App\Domain\AI\Repository\PromptTemplateRepositoryInterface; use App\Infrastructure\AI\Agent\OllamaVisionAgent; use App\Infrastructure\AI\OllamaClientInterface; +use App\Infrastructure\AI\PromptTemplateService; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -17,28 +20,63 @@ final class OllamaVisionAgentTest extends TestCase protected function setUp(): void { $this->ollama = $this->createMock(OllamaClientInterface::class); - $this->agent = new OllamaVisionAgent($this->ollama, 'llava'); + + $repo = $this->createMock(PromptTemplateRepositoryInterface::class); + $repo->method('findByKey')->willReturn(new PromptTemplate('vision_analyze', 'prompt')); + $prompts = new PromptTemplateService($repo); + + $this->agent = new OllamaVisionAgent($this->ollama, $prompts, 'llava'); } - public function testParsesModelAndSerialFromResponse(): void + public function test_parses_all_fields(): void { - $this->ollama->method('generateWithImage') - ->willReturn("MODEL: Dell Latitude 5520\nSERIAL: ABC12345"); + $this->ollama->method('generateWithImage')->willReturn( + "MANUFACTURER: Lenovo\nMODEL_NAME: ThinkBook 14 G6 IRL\nMODEL_NUMBER: 21KG00NQGE\nSERIAL: PNV09SJZ" + ); $result = $this->agent->analyze('/tmp/photo.jpg'); - self::assertSame('Dell Latitude 5520', $result['model']); - self::assertSame('ABC12345', $result['serial']); + $this->assertSame('Lenovo', $result['manufacturer']); + $this->assertSame('ThinkBook 14 G6 IRL', $result['modelName']); + $this->assertSame('21KG00NQGE', $result['modelNumber']); + $this->assertSame('PNV09SJZ', $result['serial']); } - public function testReturnsEmptyStringsWhenNotFound(): void + public function test_strips_serial_bleed_into_model_number_leading(): void { - $this->ollama->method('generateWithImage') - ->willReturn('I cannot read the nameplate clearly.'); + // LLM outputs "SERIAL: xyz" as the entire value for MODEL_NUMBER + $this->ollama->method('generateWithImage')->willReturn( + "MANUFACTURER: ELPIDA\nMODEL_NAME: 2GB 2Rx8 PC3-10600S-9-10-F1\nMODEL_NUMBER: SERIAL: 1005NK677594\nSERIAL: 1005NK677594" + ); $result = $this->agent->analyze('/tmp/photo.jpg'); - self::assertSame('', $result['model']); - self::assertSame('', $result['serial']); + $this->assertSame('', $result['modelNumber']); + $this->assertSame('1005NK677594', $result['serial']); + } + + public function test_strips_serial_bleed_into_model_name_mid_value(): void + { + // LLM appends serial after the model name + $this->ollama->method('generateWithImage')->willReturn( + "MANUFACTURER: Lenovo\nMODEL_NAME: ThinkBook 14 G6 IRL SERIAL: PNV09SJZ\nMODEL_NUMBER: 21KG00NQGE\nSERIAL: PNV09SJZ" + ); + + $result = $this->agent->analyze('/tmp/photo.jpg'); + + $this->assertSame('ThinkBook 14 G6 IRL', $result['modelName']); + $this->assertSame('PNV09SJZ', $result['serial']); + } + + public function test_returns_empty_strings_when_fields_missing(): void + { + $this->ollama->method('generateWithImage')->willReturn('I cannot read the nameplate.'); + + $result = $this->agent->analyze('/tmp/photo.jpg'); + + $this->assertSame('', $result['manufacturer']); + $this->assertSame('', $result['modelName']); + $this->assertSame('', $result['modelNumber']); + $this->assertSame('', $result['serial']); } }