109 lines
3.5 KiB
PHP
109 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Llm;
|
|
|
|
use App\Exceptions\OllamaUnavailableException;
|
|
use App\Services\AppSettingsService;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Throwable;
|
|
|
|
class OllamaClient implements LlmClientInterface
|
|
{
|
|
public function __construct(private readonly AppSettingsService $settings) {}
|
|
|
|
public function embed(string $text): array
|
|
{
|
|
$baseUrl = $this->baseUrl();
|
|
|
|
try {
|
|
$response = Http::timeout($this->timeout())
|
|
->post($baseUrl.'/api/embeddings', [
|
|
'model' => $this->model('embedding', (string) config('services.llm.embedding_model', 'nomic-embed-text')),
|
|
'prompt' => $text,
|
|
])
|
|
->throw()
|
|
->json();
|
|
} catch (Throwable $e) {
|
|
throw $this->mapException($e, 'embedding');
|
|
}
|
|
|
|
$embedding = $response['embedding'] ?? [];
|
|
if (! is_array($embedding) || $embedding === []) {
|
|
throw new OllamaUnavailableException('ollama', 'embedding', 'No embedding in response');
|
|
}
|
|
|
|
return $embedding;
|
|
}
|
|
|
|
public function generate(string $prompt, array $options = []): string
|
|
{
|
|
$baseUrl = $this->baseUrl();
|
|
$expectJson = (bool) ($options['expect_json'] ?? false);
|
|
$task = (string) ($options['task'] ?? 'chat');
|
|
|
|
$payload = [
|
|
'model' => $this->model($task, (string) config('services.llm.chat_model', 'llama3')),
|
|
'prompt' => $prompt,
|
|
'stream' => false,
|
|
];
|
|
|
|
if ($expectJson) {
|
|
$payload['format'] = 'json';
|
|
}
|
|
|
|
try {
|
|
$response = Http::timeout($this->timeout())
|
|
->post($baseUrl.'/api/generate', $payload)
|
|
->throw()
|
|
->json();
|
|
} catch (Throwable $e) {
|
|
throw $this->mapException($e, 'generation');
|
|
}
|
|
|
|
return (string) ($response['response'] ?? '');
|
|
}
|
|
|
|
private function baseUrl(): string
|
|
{
|
|
$instance = $this->settings->activeProviderInstance();
|
|
|
|
return rtrim((string) ($instance['base_url'] ?? $this->settings->get('llm.providers.ollama.base_url', (string) config('services.llm.base_url'))), '/');
|
|
}
|
|
|
|
private function timeout(): int
|
|
{
|
|
return (int) $this->settings->get('llm.timeout', (string) config('services.llm.timeout', 30));
|
|
}
|
|
|
|
private function model(string $task, string $fallback): string
|
|
{
|
|
$instance = $this->settings->activeProviderInstance();
|
|
$chatModel = (string) ($instance['chat_model'] ?? $this->settings->get('llm.providers.ollama.chat_model', $fallback));
|
|
$embeddingModel = (string) ($instance['embedding_model'] ?? $this->settings->get('llm.providers.ollama.embedding_model', $fallback));
|
|
|
|
if ($task === 'chat') {
|
|
return $chatModel;
|
|
}
|
|
|
|
$configured = trim((string) $this->settings->get('llm.models.'.$task, ''));
|
|
if ($configured !== '') {
|
|
return $configured;
|
|
}
|
|
|
|
return $task === 'embedding' ? $embeddingModel : $chatModel;
|
|
}
|
|
|
|
private function mapException(Throwable $e, string $operation): OllamaUnavailableException
|
|
{
|
|
if ($e instanceof RequestException) {
|
|
$body = $e->response->body();
|
|
$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);
|
|
}
|
|
}
|