Refactor various services and models for improved type handling and configuration management
This commit is contained in:
@@ -12,7 +12,7 @@ class ArticleCandidateDTO
|
|||||||
public readonly string $content,
|
public readonly string $content,
|
||||||
public readonly float $distance,
|
public readonly float $distance,
|
||||||
public readonly ?string $sourceUrl = null,
|
public readonly ?string $sourceUrl = null,
|
||||||
public readonly ?string $sourceArticleId = null,
|
public readonly ?int $sourceArticleId = null,
|
||||||
public readonly ?string $note = null,
|
public readonly ?string $note = null,
|
||||||
public readonly array $allowedActions = []
|
public readonly array $allowedActions = []
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ class ProcessTicketJob implements ShouldQueue
|
|||||||
$ticket->save();
|
$ticket->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$embeddingVector = $ticket->embedding ?? [];
|
||||||
$logger->log($ticket, 'embedding', 'success', 'Embedding beschikbaar.', [
|
$logger->log($ticket, 'embedding', 'success', 'Embedding beschikbaar.', [
|
||||||
'vector_dimensions' => is_array($ticket->embedding) ? count($ticket->embedding) : 0,
|
'vector_dimensions' => count($embeddingVector),
|
||||||
'vector_preview' => is_array($ticket->embedding) ? array_slice($ticket->embedding, 0, 8) : [],
|
'vector_preview' => array_slice($embeddingVector, 0, 8),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$logger->log($ticket, 'retrieval_ranking', 'info', 'Semantic retrieval en AI ranking uitvoeren.');
|
$logger->log($ticket, 'retrieval_ranking', 'info', 'Semantic retrieval en AI ranking uitvoeren.');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
/** @property array<int, float>|null $embedding */
|
||||||
class Article extends Model
|
class Article extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
/** @property array<int, float>|null $embedding */
|
||||||
class Ticket extends Model
|
class Ticket extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|||||||
@@ -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.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.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.',
|
'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.provider' => (string) config('services.llm.provider', 'ollama'),
|
||||||
'llm.active_instance_id' => env('LLM_PROVIDER', 'ollama').'_default',
|
'llm.active_instance_id' => (string) config('services.llm.provider', 'ollama').'_default',
|
||||||
'llm.provider_instances' => json_encode($this->defaultProviderInstances()),
|
'llm.provider_instances' => json_encode($this->defaultProviderInstances()),
|
||||||
'llm.timeout' => (string) env('LLM_TIMEOUT', env('OLLAMA_TIMEOUT', 30)),
|
'llm.timeout' => (string) config('services.llm.timeout', 30),
|
||||||
'llm.providers.ollama.base_url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'),
|
'llm.providers.ollama.base_url' => (string) config('services.llm.base_url', 'http://localhost:11434'),
|
||||||
'llm.providers.ollama.chat_model' => env('OLLAMA_CHAT_MODEL', 'llama3'),
|
'llm.providers.ollama.chat_model' => (string) config('services.llm.chat_model', 'llama3'),
|
||||||
'llm.providers.ollama.embedding_model' => env('OLLAMA_EMBED_MODEL', 'nomic-embed-text'),
|
'llm.providers.ollama.embedding_model' => (string) config('services.llm.embedding_model', 'nomic-embed-text'),
|
||||||
'llm.providers.lmstudio.base_url' => env('LLM_BASE_URL', 'http://localhost:1234'),
|
'llm.providers.lmstudio.base_url' => (string) config('services.llm.base_url', 'http://localhost:1234'),
|
||||||
'llm.providers.lmstudio.chat_model' => env('LLM_CHAT_MODEL', 'local-model'),
|
'llm.providers.lmstudio.chat_model' => (string) config('services.llm.chat_model', 'local-model'),
|
||||||
'llm.providers.lmstudio.embedding_model' => env('LLM_EMBEDDING_MODEL', 'text-embedding-nomic-embed-text-v1.5@q6_k'),
|
'llm.providers.lmstudio.embedding_model' => (string) config('services.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.normalization' => (string) config('services.llm.chat_model', 'llama3'),
|
||||||
'llm.models.classifier' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')),
|
'llm.models.classifier' => (string) config('services.llm.chat_model', 'llama3'),
|
||||||
'llm.models.knowledge_gap' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')),
|
'llm.models.knowledge_gap' => (string) config('services.llm.chat_model', 'llama3'),
|
||||||
'llm.models.support_reply' => env('LLM_CHAT_MODEL', env('OLLAMA_CHAT_MODEL', 'llama3')),
|
'llm.models.support_reply' => (string) config('services.llm.chat_model', 'llama3'),
|
||||||
'llm.models.embedding' => env('LLM_EMBEDDING_MODEL', env('OLLAMA_EMBED_MODEL', 'nomic-embed-text')),
|
'llm.models.embedding' => (string) config('services.llm.embedding_model', 'nomic-embed-text'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,17 +62,17 @@ class AppSettingsService
|
|||||||
'id' => 'lmstudio_default',
|
'id' => 'lmstudio_default',
|
||||||
'name' => 'LM Studio',
|
'name' => 'LM Studio',
|
||||||
'type' => 'lmstudio',
|
'type' => 'lmstudio',
|
||||||
'base_url' => env('LLM_BASE_URL', 'http://localhost:1234'),
|
'base_url' => (string) config('services.llm.base_url', 'http://localhost:1234'),
|
||||||
'chat_model' => env('LLM_CHAT_MODEL', 'local-model'),
|
'chat_model' => (string) config('services.llm.chat_model', 'local-model'),
|
||||||
'embedding_model' => env('LLM_EMBEDDING_MODEL', 'text-embedding-nomic-embed-text-v1.5@q6_k'),
|
'embedding_model' => (string) config('services.llm.embedding_model', 'text-embedding-nomic-embed-text-v1.5@q6_k'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => 'ollama_default',
|
'id' => 'ollama_default',
|
||||||
'name' => 'Ollama',
|
'name' => 'Ollama',
|
||||||
'type' => 'ollama',
|
'type' => 'ollama',
|
||||||
'base_url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'),
|
'base_url' => (string) config('services.llm.base_url', 'http://localhost:11434'),
|
||||||
'chat_model' => env('OLLAMA_CHAT_MODEL', 'llama3'),
|
'chat_model' => (string) config('services.llm.chat_model', 'llama3'),
|
||||||
'embedding_model' => env('OLLAMA_EMBED_MODEL', 'nomic-embed-text'),
|
'embedding_model' => (string) config('services.llm.embedding_model', 'nomic-embed-text'),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class EmbeddingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$embedding = $this->llmClient->embed($text);
|
$embedding = $this->llmClient->embed($text);
|
||||||
if (! is_array($embedding) || $embedding === []) {
|
if ($embedding === []) {
|
||||||
throw new OllamaUnavailableException('LLM embedding response did not include a valid embedding');
|
throw new OllamaUnavailableException('llm', 'embedding', 'LLM embedding response did not include a valid embedding');
|
||||||
}
|
}
|
||||||
|
|
||||||
EmbeddingCache::query()->updateOrCreate(
|
EmbeddingCache::query()->updateOrCreate(
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class HelpdeskImportService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $dryRun && $parentId !== null) {
|
if (! $dryRun) {
|
||||||
$childModel = Category::query()->updateOrCreate(
|
$childModel = Category::query()->updateOrCreate(
|
||||||
['external_id' => (int) $child['id']],
|
['external_id' => (int) $child['id']],
|
||||||
['name' => (string) $child['title'], 'slug' => (string) $child['slug'], 'parent_id' => $parentId]
|
['name' => (string) $child['title'], 'slug' => (string) $child['slug'], 'parent_id' => $parentId]
|
||||||
@@ -179,13 +179,15 @@ class HelpdeskImportService
|
|||||||
|
|
||||||
foreach ($sources as $source) {
|
foreach ($sources as $source) {
|
||||||
try {
|
try {
|
||||||
$html = $source['html'] ?? $this->fetch($source['url']);
|
$html = array_key_exists('html', $source)
|
||||||
|
? (string) $source['html']
|
||||||
|
: $this->fetch((string) $source['url']);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
preg_match_all('/https:\/\/www\.internettoday\.nl\/helpdesk\/(\d+)-[a-z0-9\-]+/i', $html, $matches);
|
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);
|
$url = strtolower($match);
|
||||||
if (! isset($result[$url])) {
|
if (! isset($result[$url])) {
|
||||||
$result[$url] = [
|
$result[$url] = [
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class LmStudioClient implements LlmClientInterface
|
|||||||
->throw()
|
->throw()
|
||||||
->json();
|
->json();
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
$body = (string) ($e->response?->body() ?? '');
|
$body = (string) $e->response->body();
|
||||||
$isResponseFormatError = str_contains($body, 'response_format.type')
|
$isResponseFormatError = str_contains($body, 'response_format.type')
|
||||||
|| str_contains($body, 'json_schema');
|
|| str_contains($body, 'json_schema');
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ class LmStudioClient implements LlmClientInterface
|
|||||||
private function mapException(Throwable $e, string $operation): OllamaUnavailableException
|
private function mapException(Throwable $e, string $operation): OllamaUnavailableException
|
||||||
{
|
{
|
||||||
if ($e instanceof RequestException) {
|
if ($e instanceof RequestException) {
|
||||||
$body = $e->response?->body();
|
$body = $e->response->body();
|
||||||
$snippet = $body ? mb_substr($body, 0, 280) : null;
|
$snippet = $body ? mb_substr($body, 0, 280) : null;
|
||||||
|
|
||||||
return new OllamaUnavailableException('lmstudio', $operation, $e->getMessage(), $e, $snippet);
|
return new OllamaUnavailableException('lmstudio', $operation, $e->getMessage(), $e, $snippet);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class OllamaClient implements LlmClientInterface
|
|||||||
private function mapException(Throwable $e, string $operation): OllamaUnavailableException
|
private function mapException(Throwable $e, string $operation): OllamaUnavailableException
|
||||||
{
|
{
|
||||||
if ($e instanceof RequestException) {
|
if ($e instanceof RequestException) {
|
||||||
$body = $e->response?->body();
|
$body = $e->response->body();
|
||||||
$snippet = $body ? mb_substr($body, 0, 280) : null;
|
$snippet = $body ? mb_substr($body, 0, 280) : null;
|
||||||
|
|
||||||
return new OllamaUnavailableException('ollama', $operation, $e->getMessage(), $e, $snippet);
|
return new OllamaUnavailableException('ollama', $operation, $e->getMessage(), $e, $snippet);
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ class QuickReplyResolver
|
|||||||
$article->load('quickReplies');
|
$article->load('quickReplies');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $article->quickReplies
|
$quickReply = $article->quickReplies
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->sortBy('title')
|
->sortBy('title')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
return $quickReply instanceof QuickReply ? $quickReply : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class SemanticSearchService
|
|||||||
return $scoreB <=> $scoreA;
|
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
|
private function candidateScore(string $text, float $distance, bool $isHowTo): float
|
||||||
|
|||||||
Reference in New Issue
Block a user