improve interacting with ollama api and models

This commit is contained in:
2026-05-10 17:40:24 +03:30
parent b89ae98524
commit 4c910cb8e8
11 changed files with 265 additions and 225 deletions

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Concerns;
use App\Models\EmbeddingEntry;
use App\Services\EmbeddingWorkbench;
use Illuminate\Support\Facades\Schema;
trait InteractsWithEmbeddingWorkbench
{
public string $embeddingModel = '';
/** @var array<int, array{name: string, dimensions: int}> */
public array $modelOptions = [];
public string $providerSummary = '';
public int $storedCount = 0;
public bool $databaseReady = true;
public bool $ollamaAvailable = false;
public string $ollamaStatusMessage = '';
public string $ollamaUrl = '';
public function refreshOllama(EmbeddingWorkbench $workbench): void
{
$this->initializeWorkbench($workbench);
}
protected function initializeWorkbench(EmbeddingWorkbench $workbench): void
{
$status = $workbench->ollamaStatus();
$this->ollamaAvailable = $status['available'];
$this->ollamaStatusMessage = $status['message'];
$this->ollamaUrl = $status['url'];
$this->modelOptions = $workbench->modelOptions();
if (! $this->ollamaAvailable) {
$this->embeddingModel = '';
} elseif ($this->embeddingModel === '' || ! collect($this->modelOptions)->pluck('name')->contains($this->embeddingModel)) {
$this->embeddingModel = $workbench->modelName();
}
$this->refreshSummary($workbench);
}
protected function ensureWorkbenchAvailable(EmbeddingWorkbench $workbench, string $databaseErrorField): bool
{
$this->initializeWorkbench($workbench);
if (! $this->ollamaAvailable) {
$this->addError('embeddingModel', $this->ollamaStatusMessage);
}
if (! $this->databaseReady) {
$this->addError($databaseErrorField, __('The embedding table is not ready.'));
}
return $this->ollamaAvailable && $this->databaseReady && $this->embeddingModel !== '';
}
protected function refreshSummary(EmbeddingWorkbench $workbench): void
{
$provider = $workbench->providerName();
$this->databaseReady = Schema::hasTable('embedding_entries');
if (! $this->ollamaAvailable || $this->embeddingModel === '') {
$this->providerSummary = "{$provider} / unavailable";
$this->storedCount = 0;
return;
}
$model = $workbench->modelName($this->embeddingModel);
$dimensions = $workbench->dimensions($model);
$this->providerSummary = "{$provider} / {$model} / {$dimensions}d";
if (! $this->databaseReady) {
$this->storedCount = 0;
return;
}
$this->storedCount = EmbeddingEntry::query()
->where('provider', $provider)
->where('model', $model)
->where('embedding_dimensions', $dimensions)
->count();
}
}

View File

@@ -7,7 +7,6 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use InvalidArgumentException;
use Laravel\Ai\Ai;
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use RuntimeException;
@@ -20,6 +19,8 @@ class EmbeddingWorkbench
*/
protected ?Collection $ollamaModelNames = null;
protected ?string $ollamaErrorMessage = null;
public function providerName(): string
{
$provider = config('ai.default_for_embeddings', 'ollama');
@@ -35,45 +36,55 @@ class EmbeddingWorkbench
return $this->validateModel($model);
}
$default = Ai::embeddingProvider($this->providerName())->defaultEmbeddingsModel();
$availableModels = $this->availableModelNames();
$model = $this->availableModelNames()->first();
if ($availableModels->contains($default)) {
return $default;
if ($model === null) {
throw new InvalidArgumentException($this->unavailableMessage());
}
$firstAvailableModel = $availableModels->first();
if ($firstAvailableModel === null) {
throw new InvalidArgumentException('No embedding models are available.');
}
return $firstAvailableModel;
return $model;
}
public function dimensions(?string $model = null): int
{
$this->modelName($model);
if (trim((string) $model) !== '') {
$this->validateModel((string) $model);
}
return Ai::embeddingProvider($this->providerName())->defaultEmbeddingsDimensions();
return (int) config('ai.providers.'.$this->providerName().'.models.embeddings.dimensions', 768);
}
/**
* @return array<int, array{name: string, dimensions: int, default: bool}>
* @return array<int, array{name: string, dimensions: int}>
*/
public function modelOptions(): array
{
$default = Ai::embeddingProvider($this->providerName())->defaultEmbeddingsModel();
return $this->availableModelNames()
->map(fn (string $model): array => [
'name' => $model,
'dimensions' => $this->dimensions($model),
'default' => $model === $default,
])
->all();
}
/**
* @return array{available: bool, message: string, url: string, model_count: int}
*/
public function ollamaStatus(): array
{
$models = $this->availableModelNames();
$modelCount = $models->count();
return [
'available' => $modelCount > 0,
'message' => $modelCount > 0
? 'Ollama is available and returned '.$modelCount.' model'.($modelCount === 1 ? '' : 's').'.'
: $this->unavailableMessage(),
'url' => $this->ollamaUrl(),
'model_count' => $modelCount,
];
}
/**
* @param list<string> $phrases
* @return Collection<int, EmbeddingEntry>
@@ -318,34 +329,11 @@ class EmbeddingWorkbench
*/
protected function availableModelNames(): Collection
{
if ($this->providerName() === 'ollama') {
$ollamaModels = $this->ollamaModelNames();
if ($ollamaModels->isNotEmpty()) {
return $ollamaModels;
}
if ($this->providerName() !== 'ollama') {
return collect();
}
return $this->configuredModelNames();
}
/**
* @return Collection<int, string>
*/
protected function configuredModelNames(): Collection
{
$models = config('ai.providers.'.$this->providerName().'.models.embeddings.available', []);
if (! is_array($models)) {
$models = [];
}
return collect($models)
->push(Ai::embeddingProvider($this->providerName())->defaultEmbeddingsModel())
->map(fn (mixed $model): string => trim((string) $model))
->filter()
->unique()
->values();
return $this->ollamaModelNames();
}
/**
@@ -357,6 +345,8 @@ class EmbeddingWorkbench
return $this->ollamaModelNames;
}
$this->ollamaErrorMessage = null;
try {
$models = Http::baseUrl($this->ollamaUrl())
->acceptJson()
@@ -365,19 +355,29 @@ class EmbeddingWorkbench
->get('api/tags')
->throw()
->json('models', []);
} catch (Throwable) {
} catch (Throwable $exception) {
$this->ollamaErrorMessage = 'Ollama is unavailable at '.$this->ollamaUrl().'. Start Ollama and refresh the model list. '.$exception->getMessage();
return $this->ollamaModelNames = collect();
}
if (! is_array($models)) {
$this->ollamaErrorMessage = 'Ollama responded, but /api/tags did not return a valid model list.';
return $this->ollamaModelNames = collect();
}
return $this->ollamaModelNames = collect($models)
$modelNames = collect($models)
->map(fn (mixed $model): string => is_array($model) ? trim((string) ($model['name'] ?? '')) : '')
->filter()
->unique()
->values();
if ($modelNames->isEmpty()) {
$this->ollamaErrorMessage = 'Ollama is reachable at '.$this->ollamaUrl().', but it did not return any models from /api/tags. Pull an embedding model, then refresh the model list.';
}
return $this->ollamaModelNames = $modelNames;
}
protected function ollamaUrl(): string
@@ -395,10 +395,20 @@ class EmbeddingWorkbench
$availableModels = $this->availableModelNames();
if ($availableModels->isEmpty()) {
throw new InvalidArgumentException($this->unavailableMessage());
}
if ($availableModels->doesntContain($model)) {
throw new InvalidArgumentException("The embedding model [{$model}] is not available.");
throw new InvalidArgumentException("The embedding model [{$model}] was not returned by Ollama.");
}
return $model;
}
protected function unavailableMessage(): string
{
return $this->ollamaErrorMessage
?: 'Ollama is unavailable or returned no models. Start Ollama, pull an embedding model, then refresh the model list.';
}
}