From 39bdba2dfbf23590eb61312597add41b551ab82a Mon Sep 17 00:00:00 2001 From: SitiWeb <70724099+SitiWeb@users.noreply.github.com> Date: Thu, 30 Apr 2026 01:54:49 +0200 Subject: [PATCH] Refactor various services and models for improved type handling and configuration management --- app/DTOs/ArticleCandidateDTO.php | 2 +- app/Jobs/ProcessTicketJob.php | 5 ++-- app/Models/Article.php | 1 + app/Models/Ticket.php | 1 + app/Services/AppSettingsService.php | 40 +++++++++++++------------- app/Services/EmbeddingService.php | 4 +-- app/Services/HelpdeskImportService.php | 8 ++++-- app/Services/Llm/LmStudioClient.php | 4 +-- app/Services/Llm/OllamaClient.php | 2 +- app/Services/QuickReplyResolver.php | 4 ++- app/Services/SemanticSearchService.php | 2 +- 11 files changed, 40 insertions(+), 33 deletions(-) diff --git a/app/DTOs/ArticleCandidateDTO.php b/app/DTOs/ArticleCandidateDTO.php index 952c4c4..ee45259 100644 --- a/app/DTOs/ArticleCandidateDTO.php +++ b/app/DTOs/ArticleCandidateDTO.php @@ -12,7 +12,7 @@ class ArticleCandidateDTO public readonly string $content, public readonly float $distance, public readonly ?string $sourceUrl = null, - public readonly ?string $sourceArticleId = null, + public readonly ?int $sourceArticleId = null, public readonly ?string $note = null, public readonly array $allowedActions = [] ) {} diff --git a/app/Jobs/ProcessTicketJob.php b/app/Jobs/ProcessTicketJob.php index 87d89dc..430678a 100644 --- a/app/Jobs/ProcessTicketJob.php +++ b/app/Jobs/ProcessTicketJob.php @@ -68,9 +68,10 @@ class ProcessTicketJob implements ShouldQueue $ticket->save(); } + $embeddingVector = $ticket->embedding ?? []; $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) : [], + 'vector_dimensions' => count($embeddingVector), + 'vector_preview' => array_slice($embeddingVector, 0, 8), ]); $logger->log($ticket, 'retrieval_ranking', 'info', 'Semantic retrieval en AI ranking uitvoeren.'); diff --git a/app/Models/Article.php b/app/Models/Article.php index c8a8df0..d4f51ca 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +/** @property array|null $embedding */ class Article extends Model { protected $fillable = [ diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 3c12358..4bd0875 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +/** @property array|null $embedding */ class Ticket extends Model { protected $fillable = [ diff --git a/app/Services/AppSettingsService.php b/app/Services/AppSettingsService.php index 6fbb540..7a0bf47 100644 --- a/app/Services/AppSettingsService.php +++ b/app/Services/AppSettingsService.php @@ -15,21 +15,21 @@ class AppSettingsService 'prompt.classifier' => 'You are a support assistant. Select best article and return JSON. Include tool_call only when the selected article explicitly allows that action and all required parameters are present.', 'prompt.knowledge_gap' => 'Create a draft knowledge base article suggestion based on the customer question. Use the requested output language passed in the prompt. Return JSON only with keys: title, content.', 'prompt.support_reply' => 'Give only direct advice in the requested output language. No greeting, no closing, no thank-you text. Start directly with the solution. Give 3-6 numbered action points and end with a verification step.', - 'llm.provider' => env('LLM_PROVIDER', 'ollama'), - 'llm.active_instance_id' => env('LLM_PROVIDER', 'ollama').'_default', + 'llm.provider' => (string) config('services.llm.provider', 'ollama'), + 'llm.active_instance_id' => (string) config('services.llm.provider', 'ollama').'_default', 'llm.provider_instances' => json_encode($this->defaultProviderInstances()), - 'llm.timeout' => (string) env('LLM_TIMEOUT', env('OLLAMA_TIMEOUT', 30)), - 'llm.providers.ollama.base_url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'), - 'llm.providers.ollama.chat_model' => env('OLLAMA_CHAT_MODEL', 'llama3'), - 'llm.providers.ollama.embedding_model' => env('OLLAMA_EMBED_MODEL', 'nomic-embed-text'), - 'llm.providers.lmstudio.base_url' => env('LLM_BASE_URL', 'http://localhost:1234'), - 'llm.providers.lmstudio.chat_model' => env('LLM_CHAT_MODEL', 'local-model'), - 'llm.providers.lmstudio.embedding_model' => env('LLM_EMBEDDING_MODEL', 'text-embedding-nomic-embed-text-v1.5@q6_k'), - 'llm.models.normalization' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')), - 'llm.models.classifier' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')), - 'llm.models.knowledge_gap' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')), - 'llm.models.support_reply' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')), - 'llm.models.embedding' => env('LLM_EMBEDDING_MODEL', env('OLLAMA_EMBED_MODEL', 'nomic-embed-text')), + 'llm.timeout' => (string) config('services.llm.timeout', 30), + 'llm.providers.ollama.base_url' => (string) config('services.llm.base_url', 'http://localhost:11434'), + 'llm.providers.ollama.chat_model' => (string) config('services.llm.chat_model', 'llama3'), + 'llm.providers.ollama.embedding_model' => (string) config('services.llm.embedding_model', 'nomic-embed-text'), + 'llm.providers.lmstudio.base_url' => (string) config('services.llm.base_url', 'http://localhost:1234'), + 'llm.providers.lmstudio.chat_model' => (string) config('services.llm.chat_model', 'local-model'), + 'llm.providers.lmstudio.embedding_model' => (string) config('services.llm.embedding_model', 'text-embedding-nomic-embed-text-v1.5@q6_k'), + 'llm.models.normalization' => (string) config('services.llm.chat_model', 'llama3'), + 'llm.models.classifier' => (string) config('services.llm.chat_model', 'llama3'), + 'llm.models.knowledge_gap' => (string) config('services.llm.chat_model', 'llama3'), + 'llm.models.support_reply' => (string) config('services.llm.chat_model', 'llama3'), + 'llm.models.embedding' => (string) config('services.llm.embedding_model', 'nomic-embed-text'), ]; } @@ -62,17 +62,17 @@ class AppSettingsService 'id' => 'lmstudio_default', 'name' => 'LM Studio', 'type' => 'lmstudio', - 'base_url' => env('LLM_BASE_URL', 'http://localhost:1234'), - 'chat_model' => env('LLM_CHAT_MODEL', 'local-model'), - 'embedding_model' => env('LLM_EMBEDDING_MODEL', 'text-embedding-nomic-embed-text-v1.5@q6_k'), + 'base_url' => (string) config('services.llm.base_url', 'http://localhost:1234'), + 'chat_model' => (string) config('services.llm.chat_model', 'local-model'), + 'embedding_model' => (string) config('services.llm.embedding_model', 'text-embedding-nomic-embed-text-v1.5@q6_k'), ], [ 'id' => 'ollama_default', 'name' => 'Ollama', 'type' => 'ollama', - 'base_url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'), - 'chat_model' => env('OLLAMA_CHAT_MODEL', 'llama3'), - 'embedding_model' => env('OLLAMA_EMBED_MODEL', 'nomic-embed-text'), + 'base_url' => (string) config('services.llm.base_url', 'http://localhost:11434'), + 'chat_model' => (string) config('services.llm.chat_model', 'llama3'), + 'embedding_model' => (string) config('services.llm.embedding_model', 'nomic-embed-text'), ], ]; } diff --git a/app/Services/EmbeddingService.php b/app/Services/EmbeddingService.php index 0eb6884..321e595 100644 --- a/app/Services/EmbeddingService.php +++ b/app/Services/EmbeddingService.php @@ -28,8 +28,8 @@ class EmbeddingService } $embedding = $this->llmClient->embed($text); - if (! is_array($embedding) || $embedding === []) { - throw new OllamaUnavailableException('LLM embedding response did not include a valid embedding'); + if ($embedding === []) { + throw new OllamaUnavailableException('llm', 'embedding', 'LLM embedding response did not include a valid embedding'); } EmbeddingCache::query()->updateOrCreate( diff --git a/app/Services/HelpdeskImportService.php b/app/Services/HelpdeskImportService.php index a1e2460..eb5bdb8 100644 --- a/app/Services/HelpdeskImportService.php +++ b/app/Services/HelpdeskImportService.php @@ -125,7 +125,7 @@ class HelpdeskImportService continue; } - if (! $dryRun && $parentId !== null) { + if (! $dryRun) { $childModel = Category::query()->updateOrCreate( ['external_id' => (int) $child['id']], ['name' => (string) $child['title'], 'slug' => (string) $child['slug'], 'parent_id' => $parentId] @@ -179,13 +179,15 @@ class HelpdeskImportService foreach ($sources as $source) { try { - $html = $source['html'] ?? $this->fetch($source['url']); + $html = array_key_exists('html', $source) + ? (string) $source['html'] + : $this->fetch((string) $source['url']); } catch (\Throwable) { continue; } preg_match_all('/https:\/\/www\.internettoday\.nl\/helpdesk\/(\d+)-[a-z0-9\-]+/i', $html, $matches); - foreach (($matches[0] ?? []) as $match) { + foreach ($matches[0] as $match) { $url = strtolower($match); if (! isset($result[$url])) { $result[$url] = [ diff --git a/app/Services/Llm/LmStudioClient.php b/app/Services/Llm/LmStudioClient.php index 695387f..936acbe 100644 --- a/app/Services/Llm/LmStudioClient.php +++ b/app/Services/Llm/LmStudioClient.php @@ -67,7 +67,7 @@ class LmStudioClient implements LlmClientInterface ->throw() ->json(); } catch (RequestException $e) { - $body = (string) ($e->response?->body() ?? ''); + $body = (string) $e->response->body(); $isResponseFormatError = str_contains($body, 'response_format.type') || str_contains($body, 'json_schema'); @@ -131,7 +131,7 @@ class LmStudioClient implements LlmClientInterface private function mapException(Throwable $e, string $operation): OllamaUnavailableException { if ($e instanceof RequestException) { - $body = $e->response?->body(); + $body = $e->response->body(); $snippet = $body ? mb_substr($body, 0, 280) : null; return new OllamaUnavailableException('lmstudio', $operation, $e->getMessage(), $e, $snippet); diff --git a/app/Services/Llm/OllamaClient.php b/app/Services/Llm/OllamaClient.php index dfdd6a1..028fd94 100644 --- a/app/Services/Llm/OllamaClient.php +++ b/app/Services/Llm/OllamaClient.php @@ -97,7 +97,7 @@ class OllamaClient implements LlmClientInterface private function mapException(Throwable $e, string $operation): OllamaUnavailableException { if ($e instanceof RequestException) { - $body = $e->response?->body(); + $body = $e->response->body(); $snippet = $body ? mb_substr($body, 0, 280) : null; return new OllamaUnavailableException('ollama', $operation, $e->getMessage(), $e, $snippet); diff --git a/app/Services/QuickReplyResolver.php b/app/Services/QuickReplyResolver.php index fa0e244..a9b1d4c 100644 --- a/app/Services/QuickReplyResolver.php +++ b/app/Services/QuickReplyResolver.php @@ -17,9 +17,11 @@ class QuickReplyResolver $article->load('quickReplies'); } - return $article->quickReplies + $quickReply = $article->quickReplies ->where('is_active', true) ->sortBy('title') ->first(); + + return $quickReply instanceof QuickReply ? $quickReply : null; } } diff --git a/app/Services/SemanticSearchService.php b/app/Services/SemanticSearchService.php index a6161c5..b65e1e1 100644 --- a/app/Services/SemanticSearchService.php +++ b/app/Services/SemanticSearchService.php @@ -91,7 +91,7 @@ class SemanticSearchService return $scoreB <=> $scoreA; }); - return array_values(array_slice($unique, 0, $limit)); + return array_slice($unique, 0, $limit); } private function candidateScore(string $text, float $distance, bool $isHowTo): float