- 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.
173 lines
7.6 KiB
PHP
173 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\Ticket;
|
|
use App\Services\EmbeddingService;
|
|
use App\Services\KnowledgeGapService;
|
|
use App\Services\QuickReplyResolver;
|
|
use App\Services\SemanticSearchService;
|
|
use App\Services\SupportReplyService;
|
|
use App\Services\TicketNormalizationService;
|
|
use App\Services\TicketProcessingLoggerService;
|
|
use App\Services\TicketResultPayloadBuilder;
|
|
use App\Services\Tools\TicketToolCallService;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ProcessTicketJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public function __construct(public readonly int $ticketId) {}
|
|
|
|
public function handle(
|
|
EmbeddingService $embeddingService,
|
|
SemanticSearchService $semanticSearchService,
|
|
TicketNormalizationService $normalizer,
|
|
TicketProcessingLoggerService $logger,
|
|
KnowledgeGapService $knowledgeGapService,
|
|
TicketToolCallService $toolCallService,
|
|
QuickReplyResolver $quickReplyResolver,
|
|
SupportReplyService $supportReplyService,
|
|
TicketResultPayloadBuilder $payloadBuilder,
|
|
): void {
|
|
$ticket = Ticket::query()->find($this->ticketId);
|
|
if ($ticket === null) {
|
|
return;
|
|
}
|
|
|
|
$ticket->update(['status' => 'processing']);
|
|
$logger->log($ticket, 'start', 'info', 'Ticket processing gestart.', [
|
|
'original_question' => $ticket->message,
|
|
]);
|
|
|
|
try {
|
|
$logger->log($ticket, 'normalize_question', 'info', 'Vraag normaliseren en PII redacteren.');
|
|
$normalized = $normalizer->normalize($ticket->message);
|
|
$ticket->normalized_message = $normalized['normalized_message'];
|
|
$ticket->redaction_report = $normalized['redaction_report'] ?? null;
|
|
$ticket->save();
|
|
|
|
$logger->log($ticket, 'normalize_question', 'success', 'Vraag genormaliseerd.', [
|
|
'normalized_message' => $ticket->normalized_message,
|
|
'redaction_report' => $ticket->redaction_report,
|
|
]);
|
|
|
|
if ($ticket->embedding === null) {
|
|
$logger->log($ticket, 'embedding', 'info', 'Embedding genereren.');
|
|
$ticket->embedding = $embeddingService->embed($ticket->normalized_message ?: $ticket->message);
|
|
$embeddingContext = $embeddingService->context();
|
|
$ticket->embedding_provider_instance_id = $embeddingContext['provider_instance_id'];
|
|
$ticket->embedding_model = $embeddingContext['embedding_model'];
|
|
$ticket->embedded_at = now();
|
|
$ticket->save();
|
|
}
|
|
|
|
$logger->log($ticket, 'embedding', 'success', 'Embedding beschikbaar.', [
|
|
'vector_dimensions' => is_array($ticket->embedding) ? count($ticket->embedding) : 0,
|
|
'vector_preview' => is_array($ticket->embedding) ? array_slice($ticket->embedding, 0, 8) : [],
|
|
]);
|
|
|
|
$logger->log($ticket, 'retrieval_ranking', 'info', 'Semantic retrieval en AI ranking uitvoeren.');
|
|
$result = $semanticSearchService->findBestArticle($ticket);
|
|
|
|
$logger->log($ticket, 'retrieval', 'success', 'Top kandidaten uit vector search bepaald.', [
|
|
'candidates' => $result['top_5_candidates'] ?? [],
|
|
'retrieval_meta' => $result['retrieval_meta'] ?? null,
|
|
]);
|
|
|
|
$logger->log($ticket, 'ranking', 'success', 'Classificatie afgerond.', [
|
|
'selected_article_id' => $result['best_article']?->id,
|
|
'confidence' => $result['confidence'],
|
|
'explanation' => $result['explanation'],
|
|
'classifier_raw_response' => $result['classifier_raw_response'] ?? null,
|
|
]);
|
|
|
|
$isKnowledgeGap = $knowledgeGapService->shouldCreateDraft($ticket, $result);
|
|
$draftSuggestion = null;
|
|
$supportReply = null;
|
|
$toolCallRecord = null;
|
|
$quickReply = null;
|
|
|
|
if ($isKnowledgeGap) {
|
|
$logger->log($ticket, 'knowledge_gap', 'warning', 'Onvoldoende match gevonden; geen passend artikel in kennisbank.');
|
|
$draftSuggestion = $knowledgeGapService->suggestArticleDraft($ticket, $result);
|
|
$logger->log($ticket, 'knowledge_gap', 'success', 'Voorstel voor nieuw kennisbankartikel gegenereerd (niet opgeslagen).', [
|
|
'suggested_title' => $draftSuggestion['title'] ?? null,
|
|
]);
|
|
} else {
|
|
$quickReply = $quickReplyResolver->resolveForArticle($result['best_article'] ?? null);
|
|
|
|
if ($quickReply !== null) {
|
|
$supportReply = $quickReply->content;
|
|
$logger->log($ticket, 'quick_reply', 'success', 'Snelantwoord gebruikt; AI antwoordgeneratie overgeslagen.', [
|
|
'quick_reply_id' => $quickReply->id,
|
|
'quick_reply_title' => $quickReply->title,
|
|
'article_id' => $result['best_article']?->id,
|
|
]);
|
|
} else {
|
|
$toolCallRecord = $toolCallService->executeRequestedTool(
|
|
$ticket,
|
|
$result['best_article'] ?? null,
|
|
$result['requested_tool_call'] ?? null
|
|
);
|
|
|
|
$logger->log($ticket, 'quick_reply', 'info', 'Geen actief snelantwoord gekoppeld; AI maakt conceptantwoord.', [
|
|
'article_id' => $result['best_article']?->id,
|
|
]);
|
|
|
|
$supportReply = $supportReplyService->build(
|
|
$ticket,
|
|
$result['best_article'] ?? null,
|
|
(string) $result['explanation'],
|
|
$toolCallRecord?->toArray()
|
|
);
|
|
}
|
|
}
|
|
|
|
$ticket->update([
|
|
'status' => 'completed',
|
|
'best_article_id' => $result['best_article']?->id,
|
|
'confidence' => $result['confidence'],
|
|
'explanation' => $result['explanation'],
|
|
'support_reply' => $supportReply,
|
|
'needs_article_draft' => $isKnowledgeGap,
|
|
'draft_article_id' => null,
|
|
'result_payload' => $payloadBuilder->build($result, $toolCallRecord, $quickReply, $isKnowledgeGap, $draftSuggestion),
|
|
'error_message' => null,
|
|
'processed_at' => now(),
|
|
]);
|
|
|
|
if ($supportReply !== null) {
|
|
$logger->log($ticket, 'support_reply', 'success', 'Concept supportantwoord opgebouwd.', [
|
|
'support_reply_preview' => Str::limit($supportReply, 220),
|
|
]);
|
|
} else {
|
|
$logger->log($ticket, 'support_reply', 'info', 'Geen klantreactie gegenereerd wegens knowledge gap; eerst artikelreview nodig.');
|
|
}
|
|
|
|
$logger->log($ticket, 'completed', 'success', 'Ticket processing afgerond.', [
|
|
'best_article_id' => $result['best_article']?->id,
|
|
'confidence' => $result['confidence'],
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$ticket->update([
|
|
'status' => 'failed',
|
|
'error_message' => $e->getMessage(),
|
|
'processed_at' => now(),
|
|
]);
|
|
|
|
$logger->log($ticket, 'failed', 'error', 'Ticket processing gefaald.', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|