Files
TicketAssistent/app/Services/SupportReplyService.php
your name 9244899f9b 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.
2026-05-13 22:25:45 +02:00

116 lines
5.4 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.');
$toneInstruction = $this->settings->toneInstruction();
if ($bestArticle === null) {
return "Geen passend kennisbankartikel gevonden.\n\n1. Controleer of de klantvraag 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');
$articleUrl = $bestArticle->source_url ?: 'niet beschikbaar';
$llmPrompt = $basePrompt."\n\n".
"Customer language: {$language}. Write the advice in this language.\n".
"Aanspreekvorm: {$toneInstruction}\n".
'Gebruikersvraag (genormaliseerd): '.$userInput."\n".
'Beste artikel titel: '.$bestArticle->title."\n".
'Beste artikel URL: '.$articleUrl."\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".
"- Volg de aanspreekvorm exact.\n".
"- Voeg een korte controle-stap toe als laatste punt.\n".
"- Eindig altijd met precies 1 bronregel: 'Bron: <url>' of 'Bron: niet beschikbaar'.\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."\n".
'Bron: '.($bestArticle->source_url ?: 'niet beschikbaar');
}
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 de klantvraag en bevat de meest relevante stappen om dit in te stellen.';
}
return $explanation;
}
}