Files
TicketAssistent/app/Jobs/ProcessTicketJob.php

174 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();
}
$embeddingVector = $ticket->embedding ?? [];
$logger->log($ticket, 'embedding', 'success', 'Embedding beschikbaar.', [
'vector_dimensions' => count($embeddingVector),
'vector_preview' => array_slice($embeddingVector, 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;
}
}
}