feat: Enhance Support Reply Service with tone instructions and article details
- Added tone instruction retrieval to SupportReplyService. - Improved user feedback when no relevant article is found. - Included article URL and tone instruction in LLM prompt. - Updated response format to include source information. - Enhanced article management UI with search functionality and editing capabilities. - Introduced a new API endpoint for nearest articles based on vector search. - Added confidence badge component to display article confidence levels. - Implemented tests for article searching, editing, and nearest article API. - Removed obsolete .htaccess file.
This commit is contained in:
@@ -10,7 +10,7 @@ class FakeArticleRepository implements ArticleRepositoryInterface
|
||||
/** @var array<int, ArticleCandidateDTO> */
|
||||
public array $candidates = [];
|
||||
|
||||
public function findSimilarByEmbedding(array $embedding, int $limit = 5, array $embeddingContext = []): array
|
||||
public function findSimilarByEmbedding(array $embedding, int $limit = 5, array $embeddingContext = [], array $filters = []): array
|
||||
{
|
||||
return array_slice($this->candidates, 0, $limit);
|
||||
}
|
||||
|
||||
59
tests/Feature/ArticleManagerTest.php
Normal file
59
tests/Feature/ArticleManagerTest.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Admin\ArticleManager;
|
||||
use App\Models\Article;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArticleManagerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_can_search_articles(): void
|
||||
{
|
||||
Article::withoutEvents(function (): void {
|
||||
Article::query()->create([
|
||||
'title' => 'DNS instellen',
|
||||
'content' => 'Managed DNS handleiding',
|
||||
]);
|
||||
|
||||
Article::query()->create([
|
||||
'title' => 'E-mail wachtwoord wijzigen',
|
||||
'content' => 'Mailbox instellingen',
|
||||
]);
|
||||
});
|
||||
|
||||
Livewire::test(ArticleManager::class)
|
||||
->set('search', 'Managed DNS')
|
||||
->assertSee('DNS instellen')
|
||||
->assertDontSee('E-mail wachtwoord wijzigen');
|
||||
}
|
||||
|
||||
public function test_it_can_edit_article_title_and_content(): void
|
||||
{
|
||||
Queue::fake();
|
||||
config(['services.embedding.queue_embeddings' => true]);
|
||||
|
||||
$article = Article::withoutEvents(fn () => Article::query()->create([
|
||||
'title' => 'Oude titel',
|
||||
'content' => 'Oude inhoud',
|
||||
]));
|
||||
|
||||
Livewire::test(ArticleManager::class)
|
||||
->call('startEdit', $article->id)
|
||||
->assertSet('editingArticleId', $article->id)
|
||||
->set('editTitle', 'Nieuwe titel')
|
||||
->set('editContent', 'Nieuwe inhoud voor het artikel')
|
||||
->call('saveEdit')
|
||||
->assertSet('editingArticleId', null);
|
||||
|
||||
$article->refresh();
|
||||
|
||||
$this->assertSame('Nieuwe titel', $article->title);
|
||||
$this->assertSame('Nieuwe inhoud voor het artikel', $article->content);
|
||||
}
|
||||
}
|
||||
62
tests/Feature/NearestArticleApiTest.php
Normal file
62
tests/Feature/NearestArticleApiTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\DTOs\ArticleCandidateDTO;
|
||||
use App\Repositories\Contracts\ArticleRepositoryInterface;
|
||||
use App\Services\EmbeddingService;
|
||||
use Mockery;
|
||||
use Tests\Fakes\FakeArticleRepository;
|
||||
use Tests\TestCase;
|
||||
|
||||
class NearestArticleApiTest extends TestCase
|
||||
{
|
||||
public function test_it_returns_nearest_published_articles(): void
|
||||
{
|
||||
$embeddingService = Mockery::mock(EmbeddingService::class);
|
||||
$embeddingService
|
||||
->shouldReceive('embed')
|
||||
->once()
|
||||
->with('Hoe stel ik DNS in?')
|
||||
->andReturn([0.1, 0.2, 0.3]);
|
||||
$embeddingService
|
||||
->shouldReceive('context')
|
||||
->once()
|
||||
->andReturn([
|
||||
'provider_instance_id' => 'instance-1',
|
||||
'embedding_model' => 'embed-model',
|
||||
]);
|
||||
|
||||
$repository = new FakeArticleRepository;
|
||||
$repository->candidates = [
|
||||
new ArticleCandidateDTO(
|
||||
articleId: 10,
|
||||
title: 'DNS instellen',
|
||||
content: 'Open het DNS beheer en voeg de juiste records toe.',
|
||||
distance: 0.12,
|
||||
sourceUrl: 'https://example.test/articles/dns'
|
||||
),
|
||||
];
|
||||
|
||||
$this->app->instance(EmbeddingService::class, $embeddingService);
|
||||
$this->app->instance(ArticleRepositoryInterface::class, $repository);
|
||||
|
||||
$response = $this->getJson('/api/articles/nearest?query=Hoe%20stel%20ik%20DNS%20in%3F&limit=5');
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.article_id', 10)
|
||||
->assertJsonPath('data.0.title', 'DNS instellen')
|
||||
->assertJsonPath('data.0.similarity', 0.88)
|
||||
->assertJsonPath('data.0.content', null)
|
||||
->assertJsonPath('meta.published_only', true)
|
||||
->assertJsonPath('meta.embedding_model', 'embed-model');
|
||||
}
|
||||
|
||||
public function test_it_validates_the_search_query(): void
|
||||
{
|
||||
$response = $this->getJson('/api/articles/nearest?query=x');
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class TicketShowPageTest extends TestCase
|
||||
'message' => 'vraag',
|
||||
'status' => 'completed',
|
||||
'best_article_id' => $article->id,
|
||||
'confidence' => 0.9,
|
||||
'support_reply' => 'Gebruik deze stappen',
|
||||
'result_payload' => [
|
||||
'quick_reply' => ['id' => 1, 'title' => 'DNS Quick'],
|
||||
@@ -39,5 +40,23 @@ class TicketShowPageTest extends TestCase
|
||||
$response->assertOk();
|
||||
$response->assertSee('Snelantwoord gebruikt', false);
|
||||
$response->assertSee('Toolcalls', false);
|
||||
$response->assertSee('haalt drempel', false);
|
||||
}
|
||||
|
||||
public function test_ticket_show_marks_confidence_below_threshold(): void
|
||||
{
|
||||
$article = Article::query()->create(['title' => 'DNS', 'content' => 'x']);
|
||||
$ticket = Ticket::query()->create([
|
||||
'message' => 'vraag',
|
||||
'status' => 'completed',
|
||||
'best_article_id' => $article->id,
|
||||
'confidence' => 0.25,
|
||||
]);
|
||||
|
||||
$response = $this->get("/admin/tickets/{$ticket->id}");
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Confidence 0.25', false);
|
||||
$response->assertSee('onder drempel 0.45', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ namespace Tests\Unit;
|
||||
use App\Models\Article;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\AppSettingsService;
|
||||
use App\Services\Llm\LlmClientInterface;
|
||||
use App\Services\SupportReplyService;
|
||||
use App\Services\TicketProcessingLoggerService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Fakes\FakeLlmClient;
|
||||
use Tests\Fakes\FakeTicketProcessingLoggerService;
|
||||
@@ -35,6 +33,7 @@ class SupportReplyServiceTest extends TestCase
|
||||
|
||||
$this->assertSame('1. Doe X', $reply);
|
||||
$this->assertStringContainsString('Gebruikersvraag (genormaliseerd): vraag', $llm->generatedPrompts[0]['prompt']);
|
||||
$this->assertStringContainsString('spreek de klant consequent informeel aan met je/jij/jouw', $llm->generatedPrompts[0]['prompt']);
|
||||
}
|
||||
|
||||
public function test_it_falls_back_when_llm_returns_empty(): void
|
||||
@@ -56,9 +55,41 @@ class SupportReplyServiceTest extends TestCase
|
||||
$this->assertStringContainsString('Gebruik het kennisbankartikel', $reply);
|
||||
}
|
||||
|
||||
private function fakeSettings(): AppSettingsService
|
||||
public function test_it_includes_formal_addressing_instruction_when_configured(): void
|
||||
{
|
||||
return new class extends AppSettingsService {
|
||||
$llm = new FakeLlmClient;
|
||||
$llm->responses = ['1. Doe X'];
|
||||
|
||||
$service = new SupportReplyService(
|
||||
$this->fakeSettings('u'),
|
||||
$llm,
|
||||
new FakeTicketProcessingLoggerService
|
||||
);
|
||||
|
||||
$ticket = Ticket::query()->create(['message' => 'vraag', 'normalized_message' => 'vraag']);
|
||||
$article = Article::query()->create(['title' => 'DNS', 'content' => 'steps']);
|
||||
|
||||
$service->build($ticket, $article, 'relevant');
|
||||
|
||||
$this->assertStringContainsString('spreek de klant consequent formeel aan met u/uw', $llm->generatedPrompts[0]['prompt']);
|
||||
$this->assertStringContainsString('Gebruik geen je/jij/jouw', $llm->generatedPrompts[0]['prompt']);
|
||||
}
|
||||
|
||||
private function fakeSettings(string $tone = 'je'): AppSettingsService
|
||||
{
|
||||
return new class($tone) extends AppSettingsService
|
||||
{
|
||||
public function __construct(private readonly string $tone) {}
|
||||
|
||||
public function get(string $key, ?string $default = null): ?string
|
||||
{
|
||||
if ($key === 'tone_addressing') {
|
||||
return $this->tone;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getPrompt(string $key, ?string $default = null): ?string
|
||||
{
|
||||
return 'Prompt';
|
||||
|
||||
Reference in New Issue
Block a user