Refactor various services and models for improved type handling and configuration management

This commit is contained in:
SitiWeb
2026-04-30 01:54:49 +02:00
parent f939133fe0
commit 39bdba2dfb
11 changed files with 40 additions and 33 deletions

View File

@@ -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 = []
) {}

View File

@@ -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.');

View File

@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/** @property array<int, float>|null $embedding */
class Article extends Model
{
protected $fillable = [

View File

@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/** @property array<int, float>|null $embedding */
class Ticket extends Model
{
protected $fillable = [

View File

@@ -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'),
],
];
}

View File

@@ -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(

View File

@@ -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] = [

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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