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.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Services\EmbeddingService;
|
||||
use App\Services\ArticleIndexingService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -14,18 +14,15 @@ class GenerateArticleEmbeddingJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public readonly int $articleId)
|
||||
{
|
||||
}
|
||||
public function __construct(public readonly int $articleId) {}
|
||||
|
||||
public function handle(EmbeddingService $embeddingService): void
|
||||
public function handle(ArticleIndexingService $indexingService): void
|
||||
{
|
||||
$article = Article::find($this->articleId);
|
||||
if ($article === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$article->embedding = $embeddingService->embed($article->title."\n".$article->content);
|
||||
$article->save();
|
||||
$indexingService->indexArticle($article);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
172
app/Jobs/ProcessTicketJob.php
Normal file
172
app/Jobs/ProcessTicketJob.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user