Files
TicketAssistent/app/Services/SupportReplyService.php
SitiWeb f939133fe0 Add admin views for quick replies, settings, and ticket details
- Created `quick-replies.blade.php` for managing quick replies.
- Added `settings.blade.php` for admin settings management.
- Implemented `ticket-show.blade.php` to display ticket details.
- Introduced `timeline-card.blade.php` component for displaying timeline information.

Enhance quick reply management functionality

- Developed `quick-reply-manager.blade.php` for creating and editing quick replies.
- Integrated Livewire for dynamic interaction and validation.

Implement settings page for AI configuration

- Created `settings-page.blade.php` for managing AI settings, including prompts and provider instances.
- Added functionality for managing models and embeddings.

Add ticket show functionality with real-time updates

- Implemented ticket details view with processing status and tool call logs.
- Added support for displaying article suggestions and error messages.

Create unit tests for AI classifier and domain info tool

- Added `AIClassifierServiceTest.php` to validate AI classifier functionality.
- Implemented `DomainInfoToolTest.php` for domain parameter validation.
- Created `OxxaClientTest.php` to test API interactions and password hashing.
2026-04-30 01:50:21 +02:00

109 lines
4.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\Article;
use App\Models\Ticket;
use App\Services\Llm\LlmClientInterface;
use Illuminate\Support\Str;
class SupportReplyService
{
public function __construct(
private readonly AppSettingsService $settings,
private readonly LlmClientInterface $llmClient,
private readonly TicketProcessingLoggerService $logger,
) {}
public function build(Ticket $ticket, ?Article $bestArticle, string $explanation, ?array $toolCall = null): string
{
$basePrompt = $this->settings->getPrompt('support_reply', 'Give only direct advice in the requested output language.');
if ($bestArticle === null) {
return "Geen passend kennisbankartikel gevonden.\n\n1. Controleer of je vraag voldoende details bevat (product, domein, foutmelding).\n2. Escaleer naar support voor handmatige opvolging.\n3. Voeg een nieuw kennisbankartikel toe na afhandeling van deze case.";
}
if ((bool) config('services.llm.ranking_enabled', true)) {
$userInput = $ticket->normalized_message ?: $ticket->message;
$language = (string) ($ticket->redaction_report['language'] ?? 'nl');
$llmPrompt = $basePrompt."\n\n".
"Customer language: {$language}. Write the advice in this language.\n".
'Gebruikersvraag (genormaliseerd): '.$userInput."\n".
'Beste artikel titel: '.$bestArticle->title."\n".
'Beste artikel content: '.$bestArticle->content."\n".
'Interne artikelnotitie: '.($bestArticle->note ?: '-')."\n".
'Waarom dit artikel gekozen is: '.$explanation."\n\n".
"Toolresultaat, indien beschikbaar:\n".$this->formatToolCallForPrompt($toolCall)."\n\n".
"Write direct advice only in the customer language.\n".
"Vereisten:\n".
"- GEEN aanhef, GEEN afsluiting, GEEN bedanktekst.\n".
"- Begin direct met de oplossing.\n".
"- Geef 3-6 genummerde actiepunten.\n".
"- Voeg een korte controle-stap toe als laatste punt.\n".
'- Geen markdown, geen codeblokken.';
try {
$reply = trim($this->llmClient->generate($llmPrompt, ['task' => 'support_reply']));
if ($reply !== '') {
return $reply;
}
$this->logger->log($ticket, 'support_reply', 'warning', 'LLM supportantwoord was leeg; artikel-gebaseerde fallback gebruikt.', [
'article_id' => $bestArticle->id,
'article_title' => $bestArticle->title,
]);
} catch (\Throwable $e) {
$this->logger->log($ticket, 'support_reply', 'warning', 'LLM supportantwoord faalde; artikel-gebaseerde fallback gebruikt.', [
'article_id' => $bestArticle->id,
'article_title' => $bestArticle->title,
'error' => $e->getMessage(),
]);
}
}
$snippet = Str::limit(trim($bestArticle->content), 420);
$safeExplanation = $this->sanitizeCustomerExplanation($explanation);
return "1. Gebruik het kennisbankartikel '".$bestArticle->title."' als basis voor de vervolgstappen.\n".
"2. Controleer de situatie aan de hand van de kernpunten uit het artikel.\n".
"3. Voer de herstel- of controleacties uit die in het artikel worden genoemd.\n".
"4. Controleer daarna of het oorspronkelijke probleem niet meer optreedt.\n\n".
'Relevantie: '.$safeExplanation."\n".
'Bronsamenvatting: '.$snippet;
}
private function formatToolCallForPrompt(?array $toolCall): string
{
if ($toolCall === null) {
return 'Geen toolcall uitgevoerd.';
}
$safe = [
'action' => $toolCall['action'] ?? null,
'status' => $toolCall['status'] ?? null,
'parameters' => $toolCall['parameters'] ?? null,
'response' => $toolCall['response'] ?? null,
'error' => $toolCall['error'] ?? null,
];
return json_encode($safe, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: 'Toolresultaat kon niet worden geformatteerd.';
}
private function sanitizeCustomerExplanation(string $explanation): string
{
$lower = mb_strtolower($explanation);
$hasTechnicalFailure = str_contains($lower, 'llm unavailable')
|| str_contains($lower, 'invalid json')
|| str_contains($lower, 'fallback')
|| str_contains($lower, 'http request')
|| str_contains($lower, 'response_format')
|| str_contains($lower, 'curl error');
if ($hasTechnicalFailure) {
return 'Dit artikel sluit het beste aan op je vraag en bevat de meest relevante stappen om dit in te stellen.';
}
return $explanation;
}
}