fix: vision agent serial-bleed regex + fix broken agent unit tests
OllamaVisionAgent.extractField() now handles field labels at the start of a value (e.g. MODEL_NUMBER: "SERIAL: 1005NK677594" -> "") not just mid-value bleed. Both agent test files updated to mock PromptTemplateRepositoryInterface and construct a real PromptTemplateService, since the service is final and unmockable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c19637465b
commit
376171303e
3 changed files with 61 additions and 14 deletions
|
|
@ -39,8 +39,9 @@ final class OllamaVisionAgent
|
||||||
|
|
||||||
$value = trim($matches[1]);
|
$value = trim($matches[1]);
|
||||||
|
|
||||||
// Strip any embedded field label the model mistakenly included (e.g. "SERIAL: PNV09SJZ")
|
// Strip any embedded field label the LLM mistakenly included
|
||||||
$value = preg_replace('/\s+[A-Z_]+:.*$/i', '', $value) ?? $value;
|
// 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);
|
return trim($value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@ namespace App\Tests\Unit\Infrastructure\AI\Agent;
|
||||||
use App\Domain\Article\ArticleType;
|
use App\Domain\Article\ArticleType;
|
||||||
use App\Domain\Article\AttributeDefinition;
|
use App\Domain\Article\AttributeDefinition;
|
||||||
use App\Domain\Article\AttributeType;
|
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\Agent\JsonCodingAgent;
|
||||||
use App\Infrastructure\AI\OllamaClientInterface;
|
use App\Infrastructure\AI\OllamaClientInterface;
|
||||||
|
use App\Infrastructure\AI\PromptTemplateService;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
|
@ -21,7 +24,12 @@ final class JsonCodingAgentTest extends TestCase
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->ollama = $this->createMock(OllamaClientInterface::class);
|
$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');
|
$this->type = new ArticleType('Notebook');
|
||||||
$ramDef = new AttributeDefinition('RAM', AttributeType::String);
|
$ramDef = new AttributeDefinition('RAM', AttributeType::String);
|
||||||
$this->type->addAttributeDefinition($ramDef);
|
$this->type->addAttributeDefinition($ramDef);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Tests\Unit\Infrastructure\AI\Agent;
|
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\Agent\OllamaVisionAgent;
|
||||||
use App\Infrastructure\AI\OllamaClientInterface;
|
use App\Infrastructure\AI\OllamaClientInterface;
|
||||||
|
use App\Infrastructure\AI\PromptTemplateService;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
|
@ -17,28 +20,63 @@ final class OllamaVisionAgentTest extends TestCase
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->ollama = $this->createMock(OllamaClientInterface::class);
|
$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')
|
$this->ollama->method('generateWithImage')->willReturn(
|
||||||
->willReturn("MODEL: Dell Latitude 5520\nSERIAL: ABC12345");
|
"MANUFACTURER: Lenovo\nMODEL_NAME: ThinkBook 14 G6 IRL\nMODEL_NUMBER: 21KG00NQGE\nSERIAL: PNV09SJZ"
|
||||||
|
);
|
||||||
|
|
||||||
$result = $this->agent->analyze('/tmp/photo.jpg');
|
$result = $this->agent->analyze('/tmp/photo.jpg');
|
||||||
|
|
||||||
self::assertSame('Dell Latitude 5520', $result['model']);
|
$this->assertSame('Lenovo', $result['manufacturer']);
|
||||||
self::assertSame('ABC12345', $result['serial']);
|
$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')
|
// LLM outputs "SERIAL: xyz" as the entire value for MODEL_NUMBER
|
||||||
->willReturn('I cannot read the nameplate clearly.');
|
$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');
|
$result = $this->agent->analyze('/tmp/photo.jpg');
|
||||||
|
|
||||||
self::assertSame('', $result['model']);
|
$this->assertSame('', $result['modelNumber']);
|
||||||
self::assertSame('', $result['serial']);
|
$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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue