- Implement Fake repositories and services for testing purposes. - Create tests for Article API including creation, validation, and listing. - Develop ProcessTicketJobFlowTest to validate ticket processing logic. - Add QuickReplyAdminTest for creating and updating quick replies. - Implement TicketAndArticleModelTest to ensure proper cascading deletes and credential encryption. - Create TicketIngestionTest for ticket creation and job dispatching. - Add TicketShowPageTest to verify rendering of quick replies and tool calls. - Implement unit tests for ClassifierPromptBuilder, EmbeddingService, LlmJsonDecoder, QuickReplyResolver, SupportReplyService, TicketResultPayloadBuilder, TicketToolCallService, and ToolCallRequestValidator.
190 lines
5.7 KiB
PHP
190 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\DTOs\ArticleCandidateDTO;
|
|
use App\Services\AIClassifierService;
|
|
use App\Services\AppSettingsService;
|
|
use App\Services\ClassifierPromptBuilder;
|
|
use App\Services\Llm\LlmClientInterface;
|
|
use App\Services\LlmJsonDecoder;
|
|
use App\Services\ToolCallRequestValidator;
|
|
use Tests\TestCase;
|
|
|
|
class AIClassifierServiceTest extends TestCase
|
|
{
|
|
public function test_it_returns_validated_domain_tool_call_from_llm_json(): void
|
|
{
|
|
$client = new class implements LlmClientInterface
|
|
{
|
|
public string $prompt = '';
|
|
|
|
public function embed(string $text): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function generate(string $prompt, array $options = []): string
|
|
{
|
|
$this->prompt = $prompt;
|
|
|
|
return json_encode([
|
|
'article_id' => 42,
|
|
'confidence' => 0.91,
|
|
'explanation' => 'Past bij domeininformatie.',
|
|
'tool_call' => [
|
|
'action' => 'domain_inf',
|
|
'parameters' => ['sld' => 'Example', 'tld' => 'NL'],
|
|
'reason' => 'Domeinstatus is nodig.',
|
|
],
|
|
]);
|
|
}
|
|
};
|
|
|
|
$service = new AIClassifierService(
|
|
$client,
|
|
$this->fakeSettings(),
|
|
new ClassifierPromptBuilder,
|
|
new LlmJsonDecoder,
|
|
new ToolCallRequestValidator
|
|
);
|
|
$result = $service->rank('Hoe staat example.nl ingesteld?', [
|
|
new ArticleCandidateDTO(
|
|
articleId: 42,
|
|
title: 'Domein controleren',
|
|
content: 'Controleer domeininformatie.',
|
|
distance: 0.12,
|
|
note: 'Gebruik domain_inf wanneer een volledig domein genoemd wordt.',
|
|
allowedActions: ['domain_inf'],
|
|
),
|
|
]);
|
|
|
|
$this->assertSame(42, $result->articleId);
|
|
$this->assertSame([
|
|
'action' => 'domain_inf',
|
|
'parameters' => ['sld' => 'example', 'tld' => 'nl'],
|
|
'reason' => 'Domeinstatus is nodig.',
|
|
], $result->toolCall);
|
|
$this->assertStringContainsString('Allowed actions: ["domain_inf"]', $client->prompt);
|
|
$this->assertStringContainsString('Internal note for support assistant', $client->prompt);
|
|
}
|
|
|
|
public function test_it_falls_back_when_ranking_disabled(): void
|
|
{
|
|
config()->set('services.llm.ranking_enabled', false);
|
|
|
|
$client = new class implements LlmClientInterface
|
|
{
|
|
public function embed(string $text): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function generate(string $prompt, array $options = []): string
|
|
{
|
|
return '{}';
|
|
}
|
|
};
|
|
|
|
$service = new AIClassifierService(
|
|
$client,
|
|
$this->fakeSettings(),
|
|
new ClassifierPromptBuilder,
|
|
new LlmJsonDecoder,
|
|
new ToolCallRequestValidator
|
|
);
|
|
|
|
$result = $service->rank('vraag', [$this->candidate(15)]);
|
|
$this->assertSame(15, $result->articleId);
|
|
$this->assertSame(0.2, $result->confidence);
|
|
}
|
|
|
|
public function test_it_falls_back_when_llm_json_is_invalid(): void
|
|
{
|
|
config()->set('services.llm.ranking_enabled', true);
|
|
|
|
$client = new class implements LlmClientInterface
|
|
{
|
|
public function embed(string $text): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function generate(string $prompt, array $options = []): string
|
|
{
|
|
return 'not-json';
|
|
}
|
|
};
|
|
|
|
$service = new AIClassifierService(
|
|
$client,
|
|
$this->fakeSettings(),
|
|
new ClassifierPromptBuilder,
|
|
new LlmJsonDecoder,
|
|
new ToolCallRequestValidator
|
|
);
|
|
|
|
$result = $service->rank('vraag', [$this->candidate(21)]);
|
|
$this->assertSame(21, $result->articleId);
|
|
$this->assertStringContainsString('invalid JSON', $result->explanation);
|
|
}
|
|
|
|
public function test_it_falls_back_when_article_id_not_in_candidates(): void
|
|
{
|
|
$client = new class implements LlmClientInterface
|
|
{
|
|
public function embed(string $text): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function generate(string $prompt, array $options = []): string
|
|
{
|
|
return json_encode([
|
|
'article_id' => 999,
|
|
'confidence' => 0.9,
|
|
'explanation' => 'x',
|
|
]);
|
|
}
|
|
};
|
|
|
|
$service = new AIClassifierService(
|
|
$client,
|
|
$this->fakeSettings(),
|
|
new ClassifierPromptBuilder,
|
|
new LlmJsonDecoder,
|
|
new ToolCallRequestValidator
|
|
);
|
|
|
|
$result = $service->rank('vraag', [$this->candidate(33)]);
|
|
$this->assertSame(33, $result->articleId);
|
|
$this->assertStringContainsString('schema invalid', $result->explanation);
|
|
}
|
|
|
|
private function fakeSettings(): AppSettingsService
|
|
{
|
|
return new class extends AppSettingsService
|
|
{
|
|
public function getPrompt(string $key, ?string $default = null): ?string
|
|
{
|
|
return 'Select best article.';
|
|
}
|
|
|
|
public function get(string $key, ?string $default = null): ?string
|
|
{
|
|
return $default;
|
|
}
|
|
};
|
|
}
|
|
|
|
private function candidate(int $id): ArticleCandidateDTO
|
|
{
|
|
return new ArticleCandidateDTO(
|
|
articleId: $id,
|
|
title: 'A',
|
|
content: 'B',
|
|
distance: 0.2
|
|
);
|
|
}
|
|
}
|