improve interacting with ollama api and models
This commit is contained in:
@@ -41,8 +41,6 @@ CACHE_STORE=database
|
|||||||
# CACHE_PREFIX=
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
OLLAMA_URL=http://localhost:11434
|
OLLAMA_URL=http://localhost:11434
|
||||||
OLLAMA_EMBEDDINGS_MODEL=nomic-embed-text
|
|
||||||
OLLAMA_EMBEDDINGS_MODELS=nomic-embed-text
|
|
||||||
OLLAMA_EMBEDDINGS_DIMENSIONS=768
|
OLLAMA_EMBEDDINGS_DIMENSIONS=768
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|||||||
96
app/Concerns/InteractsWithEmbeddingWorkbench.php
Normal file
96
app/Concerns/InteractsWithEmbeddingWorkbench.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Laravel\Ai\Ai;
|
|
||||||
use Laravel\Ai\Embeddings;
|
use Laravel\Ai\Embeddings;
|
||||||
use Laravel\Ai\Enums\Lab;
|
use Laravel\Ai\Enums\Lab;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -20,6 +19,8 @@ class EmbeddingWorkbench
|
|||||||
*/
|
*/
|
||||||
protected ?Collection $ollamaModelNames = null;
|
protected ?Collection $ollamaModelNames = null;
|
||||||
|
|
||||||
|
protected ?string $ollamaErrorMessage = null;
|
||||||
|
|
||||||
public function providerName(): string
|
public function providerName(): string
|
||||||
{
|
{
|
||||||
$provider = config('ai.default_for_embeddings', 'ollama');
|
$provider = config('ai.default_for_embeddings', 'ollama');
|
||||||
@@ -35,45 +36,55 @@ class EmbeddingWorkbench
|
|||||||
return $this->validateModel($model);
|
return $this->validateModel($model);
|
||||||
}
|
}
|
||||||
|
|
||||||
$default = Ai::embeddingProvider($this->providerName())->defaultEmbeddingsModel();
|
$model = $this->availableModelNames()->first();
|
||||||
$availableModels = $this->availableModelNames();
|
|
||||||
|
|
||||||
if ($availableModels->contains($default)) {
|
if ($model === null) {
|
||||||
return $default;
|
throw new InvalidArgumentException($this->unavailableMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$firstAvailableModel = $availableModels->first();
|
return $model;
|
||||||
|
|
||||||
if ($firstAvailableModel === null) {
|
|
||||||
throw new InvalidArgumentException('No embedding models are available.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $firstAvailableModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dimensions(?string $model = null): int
|
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
|
public function modelOptions(): array
|
||||||
{
|
{
|
||||||
$default = Ai::embeddingProvider($this->providerName())->defaultEmbeddingsModel();
|
|
||||||
|
|
||||||
return $this->availableModelNames()
|
return $this->availableModelNames()
|
||||||
->map(fn (string $model): array => [
|
->map(fn (string $model): array => [
|
||||||
'name' => $model,
|
'name' => $model,
|
||||||
'dimensions' => $this->dimensions($model),
|
'dimensions' => $this->dimensions($model),
|
||||||
'default' => $model === $default,
|
|
||||||
])
|
])
|
||||||
->all();
|
->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
|
* @param list<string> $phrases
|
||||||
* @return Collection<int, EmbeddingEntry>
|
* @return Collection<int, EmbeddingEntry>
|
||||||
@@ -318,34 +329,11 @@ class EmbeddingWorkbench
|
|||||||
*/
|
*/
|
||||||
protected function availableModelNames(): Collection
|
protected function availableModelNames(): Collection
|
||||||
{
|
{
|
||||||
if ($this->providerName() === 'ollama') {
|
if ($this->providerName() !== 'ollama') {
|
||||||
$ollamaModels = $this->ollamaModelNames();
|
return collect();
|
||||||
|
|
||||||
if ($ollamaModels->isNotEmpty()) {
|
|
||||||
return $ollamaModels;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->configuredModelNames();
|
return $this->ollamaModelNames();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -357,6 +345,8 @@ class EmbeddingWorkbench
|
|||||||
return $this->ollamaModelNames;
|
return $this->ollamaModelNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->ollamaErrorMessage = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$models = Http::baseUrl($this->ollamaUrl())
|
$models = Http::baseUrl($this->ollamaUrl())
|
||||||
->acceptJson()
|
->acceptJson()
|
||||||
@@ -365,19 +355,29 @@ class EmbeddingWorkbench
|
|||||||
->get('api/tags')
|
->get('api/tags')
|
||||||
->throw()
|
->throw()
|
||||||
->json('models', []);
|
->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();
|
return $this->ollamaModelNames = collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! is_array($models)) {
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->ollamaModelNames = collect($models)
|
$modelNames = collect($models)
|
||||||
->map(fn (mixed $model): string => is_array($model) ? trim((string) ($model['name'] ?? '')) : '')
|
->map(fn (mixed $model): string => is_array($model) ? trim((string) ($model['name'] ?? '')) : '')
|
||||||
->filter()
|
->filter()
|
||||||
->unique()
|
->unique()
|
||||||
->values();
|
->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
|
protected function ollamaUrl(): string
|
||||||
@@ -395,10 +395,20 @@ class EmbeddingWorkbench
|
|||||||
|
|
||||||
$availableModels = $this->availableModelNames();
|
$availableModels = $this->availableModelNames();
|
||||||
|
|
||||||
|
if ($availableModels->isEmpty()) {
|
||||||
|
throw new InvalidArgumentException($this->unavailableMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if ($availableModels->doesntContain($model)) {
|
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;
|
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.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,11 +118,6 @@ return [
|
|||||||
'url' => env('OLLAMA_URL', 'http://localhost:11434'),
|
'url' => env('OLLAMA_URL', 'http://localhost:11434'),
|
||||||
'models' => [
|
'models' => [
|
||||||
'embeddings' => [
|
'embeddings' => [
|
||||||
'default' => env('OLLAMA_EMBEDDINGS_MODEL', 'nomic-embed-text'),
|
|
||||||
'available' => array_values(array_filter(array_map(
|
|
||||||
'trim',
|
|
||||||
explode(',', env('OLLAMA_EMBEDDINGS_MODELS', env('OLLAMA_EMBEDDINGS_MODEL', 'nomic-embed-text')))
|
|
||||||
))),
|
|
||||||
'dimensions' => (int) env('OLLAMA_EMBEDDINGS_DIMENSIONS', 768),
|
'dimensions' => (int) env('OLLAMA_EMBEDDINGS_DIMENSIONS', 768),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class EmbeddingEntryFactory extends Factory
|
|||||||
'embedding' => $embedding,
|
'embedding' => $embedding,
|
||||||
'embedding_dimensions' => $dimensions,
|
'embedding_dimensions' => $dimensions,
|
||||||
'provider' => 'ollama',
|
'provider' => 'ollama',
|
||||||
'model' => config('ai.providers.ollama.models.embeddings.default', 'nomic-embed-text'),
|
'model' => 'nomic-embed-text',
|
||||||
'tokens' => fake()->numberBetween(1, 100),
|
'tokens' => fake()->numberBetween(1, 100),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class EmbeddingUsageEventFactory extends Factory
|
|||||||
'user_id' => User::factory(),
|
'user_id' => User::factory(),
|
||||||
'tool' => $tool,
|
'tool' => $tool,
|
||||||
'provider' => 'ollama',
|
'provider' => 'ollama',
|
||||||
'model' => config('ai.providers.ollama.models.embeddings.default', 'nomic-embed-text'),
|
'model' => 'nomic-embed-text',
|
||||||
'embedding_dimensions' => (int) config('ai.providers.ollama.models.embeddings.dimensions', 768),
|
'embedding_dimensions' => (int) config('ai.providers.ollama.models.embeddings.dimensions', 768),
|
||||||
'input_count' => $inputCount,
|
'input_count' => $inputCount,
|
||||||
'result_count' => $tool === 'search' ? fake()->numberBetween(0, 10) : $inputCount,
|
'result_count' => $tool === 'search' ? fake()->numberBetween(0, 10) : $inputCount,
|
||||||
|
|||||||
@@ -5,27 +5,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<flux:badge :color="$ollamaAvailable ? 'emerald' : 'red'" :icon="$ollamaAvailable ? 'check-circle' : 'x-circle'">
|
||||||
|
{{ $ollamaAvailable ? __('Ollama online') : __('Ollama offline') }}
|
||||||
|
</flux:badge>
|
||||||
<flux:badge icon="cpu-chip">{{ $providerSummary }}</flux:badge>
|
<flux:badge icon="cpu-chip">{{ $providerSummary }}</flux:badge>
|
||||||
<flux:badge icon="circle-stack">{{ __(':count stored', ['count' => $storedCount]) }}</flux:badge>
|
<flux:badge icon="circle-stack">{{ __(':count stored', ['count' => $storedCount]) }}</flux:badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-end">
|
||||||
<div class="w-full max-w-sm">
|
<div class="w-full max-w-sm">
|
||||||
<flux:select wire:model="embeddingModel" :label="__('Embedding model')">
|
<flux:select wire:model="embeddingModel" :label="__('Ollama model')" :disabled="! $ollamaAvailable">
|
||||||
@foreach ($modelOptions as $option)
|
@forelse ($modelOptions as $option)
|
||||||
<flux:select.option value="{{ $option['name'] }}">
|
<flux:select.option value="{{ $option['name'] }}">
|
||||||
{{ $option['name'] }} ({{ $option['dimensions'] }}d)
|
{{ $option['name'] }} ({{ $option['dimensions'] }}d)
|
||||||
</flux:select.option>
|
</flux:select.option>
|
||||||
@endforeach
|
@empty
|
||||||
|
<flux:select.option value="" disabled>{{ __('No models returned') }}</flux:select.option>
|
||||||
|
@endforelse
|
||||||
</flux:select>
|
</flux:select>
|
||||||
<flux:error name="embeddingModel" />
|
<flux:error name="embeddingModel" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<flux:button type="button" variant="filled" icon="arrow-path" wire:click="refreshOllama" class="w-full lg:w-auto data-loading:opacity-60">
|
||||||
|
<span wire:loading.remove wire:target="refreshOllama">{{ __('Refresh') }}</span>
|
||||||
|
<span wire:loading wire:target="refreshOllama">{{ __('Checking') }}</span>
|
||||||
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (! $ollamaAvailable)
|
||||||
|
<flux:callout variant="danger" icon="x-circle" :heading="__('Ollama is unavailable')" :text="$ollamaStatusMessage" />
|
||||||
|
@endif
|
||||||
|
|
||||||
@if (! $databaseReady)
|
@if (! $databaseReady)
|
||||||
<div class="rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-100">
|
<flux:callout variant="warning" icon="exclamation-circle" :heading="__('Embedding storage is unavailable')" :text="__('Embedding storage is waiting for the pgvector migration.')" />
|
||||||
{{ __('Embedding storage is waiting for the pgvector migration.') }}
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
||||||
|
use App\Concerns\InteractsWithEmbeddingWorkbench;
|
||||||
use App\Models\EmbeddingEntry;
|
use App\Models\EmbeddingEntry;
|
||||||
use App\Services\EmbeddingVector;
|
use App\Services\EmbeddingVector;
|
||||||
use App\Services\EmbeddingWorkbench;
|
use App\Services\EmbeddingWorkbench;
|
||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
new #[Title('Embedding comparison')] class extends Component
|
new #[Title('Embedding comparison')] class extends Component
|
||||||
{
|
{
|
||||||
|
use InteractsWithEmbeddingWorkbench;
|
||||||
|
|
||||||
/** @var array<int, string> */
|
/** @var array<int, string> */
|
||||||
public array $comparisonPhrases = ['', '', ''];
|
public array $comparisonPhrases = ['', '', ''];
|
||||||
|
|
||||||
public string $embeddingModel = '';
|
|
||||||
|
|
||||||
/** @var array<int, array{name: string, dimensions: int, default: bool}> */
|
|
||||||
public array $modelOptions = [];
|
|
||||||
|
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $comparisonEntries = [];
|
public array $comparisonEntries = [];
|
||||||
|
|
||||||
@@ -32,12 +29,6 @@ new #[Title('Embedding comparison')] class extends Component
|
|||||||
/** @var array<string, mixed>|null */
|
/** @var array<string, mixed>|null */
|
||||||
public ?array $closestPair = null;
|
public ?array $closestPair = null;
|
||||||
|
|
||||||
public string $providerSummary = '';
|
|
||||||
|
|
||||||
public int $storedCount = 0;
|
|
||||||
|
|
||||||
public bool $databaseReady = true;
|
|
||||||
|
|
||||||
public function mount(EmbeddingWorkbench $workbench): void
|
public function mount(EmbeddingWorkbench $workbench): void
|
||||||
{
|
{
|
||||||
$this->initializeWorkbench($workbench);
|
$this->initializeWorkbench($workbench);
|
||||||
@@ -45,6 +36,10 @@ new #[Title('Embedding comparison')] class extends Component
|
|||||||
|
|
||||||
public function comparePhrases(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
public function comparePhrases(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
||||||
{
|
{
|
||||||
|
if (! $this->ensureWorkbenchAvailable($workbench, 'comparisonPhrases')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'comparisonPhrases' => ['required', 'array', 'min:2', 'max:12'],
|
'comparisonPhrases' => ['required', 'array', 'min:2', 'max:12'],
|
||||||
'comparisonPhrases.*' => ['nullable', 'string', 'max:1000'],
|
'comparisonPhrases.*' => ['nullable', 'string', 'max:1000'],
|
||||||
@@ -59,12 +54,6 @@ new #[Title('Embedding comparison')] class extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->databaseReady) {
|
|
||||||
$this->addError('comparisonPhrases', __('The embedding table is not ready.'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$comparison = $workbench->compare($phrases, $this->embeddingModel);
|
$comparison = $workbench->compare($phrases, $this->embeddingModel);
|
||||||
|
|
||||||
@@ -125,36 +114,6 @@ new #[Title('Embedding comparison')] class extends Component
|
|||||||
$this->resetComparisonResults();
|
$this->resetComparisonResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function initializeWorkbench(EmbeddingWorkbench $workbench): void
|
|
||||||
{
|
|
||||||
$this->modelOptions = $workbench->modelOptions();
|
|
||||||
$this->embeddingModel = $this->embeddingModel ?: $workbench->modelName();
|
|
||||||
|
|
||||||
$this->refreshSummary($workbench);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function refreshSummary(EmbeddingWorkbench $workbench): void
|
|
||||||
{
|
|
||||||
$provider = $workbench->providerName();
|
|
||||||
$model = $workbench->modelName($this->embeddingModel);
|
|
||||||
$dimensions = $workbench->dimensions($model);
|
|
||||||
|
|
||||||
$this->providerSummary = "{$provider} / {$model} / {$dimensions}d";
|
|
||||||
$this->databaseReady = Schema::hasTable('embedding_entries');
|
|
||||||
|
|
||||||
if (! $this->databaseReady) {
|
|
||||||
$this->storedCount = 0;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storedCount = EmbeddingEntry::query()
|
|
||||||
->where('provider', $provider)
|
|
||||||
->where('model', $model)
|
|
||||||
->where('embedding_dimensions', $dimensions)
|
|
||||||
->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
@@ -230,7 +189,7 @@ new #[Title('Embedding comparison')] class extends Component
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<flux:button type="submit" variant="primary" icon="arrows-right-left" :disabled="! $databaseReady" class="data-loading:opacity-60">
|
<flux:button type="submit" variant="primary" icon="arrows-right-left" :disabled="! $databaseReady || ! $ollamaAvailable" class="data-loading:opacity-60">
|
||||||
<span wire:loading.remove wire:target="comparePhrases">{{ __('Compare') }}</span>
|
<span wire:loading.remove wire:target="comparePhrases">{{ __('Compare') }}</span>
|
||||||
<span wire:loading wire:target="comparePhrases">{{ __('Comparing') }}</span>
|
<span wire:loading wire:target="comparePhrases">{{ __('Comparing') }}</span>
|
||||||
</flux:button>
|
</flux:button>
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
||||||
|
use App\Concerns\InteractsWithEmbeddingWorkbench;
|
||||||
use App\Models\EmbeddingEntry;
|
use App\Models\EmbeddingEntry;
|
||||||
use App\Services\EmbeddingVector;
|
use App\Services\EmbeddingVector;
|
||||||
use App\Services\EmbeddingWorkbench;
|
use App\Services\EmbeddingWorkbench;
|
||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
new #[Title('Embed')] class extends Component
|
new #[Title('Embed')] class extends Component
|
||||||
{
|
{
|
||||||
|
use InteractsWithEmbeddingWorkbench;
|
||||||
|
|
||||||
public string $embeddingInput = '';
|
public string $embeddingInput = '';
|
||||||
|
|
||||||
public string $embeddingModel = '';
|
|
||||||
|
|
||||||
/** @var array<int, array{name: string, dimensions: int, default: bool}> */
|
|
||||||
public array $modelOptions = [];
|
|
||||||
|
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $embeddingResults = [];
|
public array $embeddingResults = [];
|
||||||
|
|
||||||
public string $providerSummary = '';
|
|
||||||
|
|
||||||
public int $storedCount = 0;
|
|
||||||
|
|
||||||
public bool $databaseReady = true;
|
|
||||||
|
|
||||||
public function mount(EmbeddingWorkbench $workbench): void
|
public function mount(EmbeddingWorkbench $workbench): void
|
||||||
{
|
{
|
||||||
$this->initializeWorkbench($workbench);
|
$this->initializeWorkbench($workbench);
|
||||||
@@ -34,6 +25,10 @@ new #[Title('Embed')] class extends Component
|
|||||||
|
|
||||||
public function generateEmbeddings(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
public function generateEmbeddings(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
||||||
{
|
{
|
||||||
|
if (! $this->ensureWorkbenchAvailable($workbench, 'embeddingInput')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'embeddingInput' => ['required', 'string', 'max:12000'],
|
'embeddingInput' => ['required', 'string', 'max:12000'],
|
||||||
'embeddingModel' => ['required', 'string'],
|
'embeddingModel' => ['required', 'string'],
|
||||||
@@ -47,12 +42,6 @@ new #[Title('Embed')] class extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->databaseReady) {
|
|
||||||
$this->addError('embeddingInput', __('The embedding table is not ready.'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$entries = $workbench->embed($phrases, $this->embeddingModel);
|
$entries = $workbench->embed($phrases, $this->embeddingModel);
|
||||||
$tokens = (int) $entries->sum(fn (EmbeddingEntry $entry): int => (int) ($entry->tokens ?? 0));
|
$tokens = (int) $entries->sum(fn (EmbeddingEntry $entry): int => (int) ($entry->tokens ?? 0));
|
||||||
@@ -78,37 +67,6 @@ new #[Title('Embed')] class extends Component
|
|||||||
$this->addError('embeddingModel', $exception->getMessage());
|
$this->addError('embeddingModel', $exception->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function initializeWorkbench(EmbeddingWorkbench $workbench): void
|
|
||||||
{
|
|
||||||
$this->modelOptions = $workbench->modelOptions();
|
|
||||||
$this->embeddingModel = $this->embeddingModel ?: $workbench->modelName();
|
|
||||||
|
|
||||||
$this->refreshSummary($workbench);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function refreshSummary(EmbeddingWorkbench $workbench): void
|
|
||||||
{
|
|
||||||
$provider = $workbench->providerName();
|
|
||||||
$model = $workbench->modelName($this->embeddingModel);
|
|
||||||
$dimensions = $workbench->dimensions($model);
|
|
||||||
|
|
||||||
$this->providerSummary = "{$provider} / {$model} / {$dimensions}d";
|
|
||||||
$this->databaseReady = Schema::hasTable('embedding_entries');
|
|
||||||
|
|
||||||
if (! $this->databaseReady) {
|
|
||||||
$this->storedCount = 0;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storedCount = EmbeddingEntry::query()
|
|
||||||
->where('provider', $provider)
|
|
||||||
->where('model', $model)
|
|
||||||
->where('embedding_dimensions', $dimensions)
|
|
||||||
->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -124,7 +82,7 @@ new #[Title('Embed')] class extends Component
|
|||||||
</flux:field>
|
</flux:field>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<flux:button type="submit" variant="primary" icon="sparkles" :disabled="! $databaseReady" class="data-loading:opacity-60">
|
<flux:button type="submit" variant="primary" icon="sparkles" :disabled="! $databaseReady || ! $ollamaAvailable" class="data-loading:opacity-60">
|
||||||
<span wire:loading.remove wire:target="generateEmbeddings">{{ __('Generate') }}</span>
|
<span wire:loading.remove wire:target="generateEmbeddings">{{ __('Generate') }}</span>
|
||||||
<span wire:loading wire:target="generateEmbeddings">{{ __('Generating') }}</span>
|
<span wire:loading wire:target="generateEmbeddings">{{ __('Generating') }}</span>
|
||||||
</flux:button>
|
</flux:button>
|
||||||
|
|||||||
@@ -1,34 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
||||||
use App\Models\EmbeddingEntry;
|
use App\Concerns\InteractsWithEmbeddingWorkbench;
|
||||||
use App\Services\EmbeddingWorkbench;
|
use App\Services\EmbeddingWorkbench;
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
new #[Title('Embedding search')] class extends Component
|
new #[Title('Embedding search')] class extends Component
|
||||||
{
|
{
|
||||||
|
use InteractsWithEmbeddingWorkbench;
|
||||||
|
|
||||||
public string $similarityQuery = '';
|
public string $similarityQuery = '';
|
||||||
|
|
||||||
public int $similarityLimit = 10;
|
public int $similarityLimit = 10;
|
||||||
|
|
||||||
public string $minimumSimilarity = '0.30';
|
public string $minimumSimilarity = '0.30';
|
||||||
|
|
||||||
public string $embeddingModel = '';
|
|
||||||
|
|
||||||
/** @var array<int, array{name: string, dimensions: int, default: bool}> */
|
|
||||||
public array $modelOptions = [];
|
|
||||||
|
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $similarityResults = [];
|
public array $similarityResults = [];
|
||||||
|
|
||||||
public string $providerSummary = '';
|
|
||||||
|
|
||||||
public int $storedCount = 0;
|
|
||||||
|
|
||||||
public bool $databaseReady = true;
|
|
||||||
|
|
||||||
public function mount(EmbeddingWorkbench $workbench): void
|
public function mount(EmbeddingWorkbench $workbench): void
|
||||||
{
|
{
|
||||||
$this->initializeWorkbench($workbench);
|
$this->initializeWorkbench($workbench);
|
||||||
@@ -36,6 +26,10 @@ new #[Title('Embedding search')] class extends Component
|
|||||||
|
|
||||||
public function searchSimilar(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
public function searchSimilar(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
||||||
{
|
{
|
||||||
|
if (! $this->ensureWorkbenchAvailable($workbench, 'similarityQuery')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'similarityQuery' => ['required', 'string', 'max:1000'],
|
'similarityQuery' => ['required', 'string', 'max:1000'],
|
||||||
'similarityLimit' => ['required', 'integer', 'min:1', 'max:50'],
|
'similarityLimit' => ['required', 'integer', 'min:1', 'max:50'],
|
||||||
@@ -43,12 +37,6 @@ new #[Title('Embedding search')] class extends Component
|
|||||||
'embeddingModel' => ['required', 'string'],
|
'embeddingModel' => ['required', 'string'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! $this->databaseReady) {
|
|
||||||
$this->addError('similarityQuery', __('The embedding table is not ready.'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->similarityResults = $workbench
|
$this->similarityResults = $workbench
|
||||||
->search($this->similarityQuery, $this->similarityLimit, (float) $this->minimumSimilarity, $this->embeddingModel)
|
->search($this->similarityQuery, $this->similarityLimit, (float) $this->minimumSimilarity, $this->embeddingModel)
|
||||||
@@ -75,37 +63,6 @@ new #[Title('Embedding search')] class extends Component
|
|||||||
$this->addError('embeddingModel', $exception->getMessage());
|
$this->addError('embeddingModel', $exception->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function initializeWorkbench(EmbeddingWorkbench $workbench): void
|
|
||||||
{
|
|
||||||
$this->modelOptions = $workbench->modelOptions();
|
|
||||||
$this->embeddingModel = $this->embeddingModel ?: $workbench->modelName();
|
|
||||||
|
|
||||||
$this->refreshSummary($workbench);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function refreshSummary(EmbeddingWorkbench $workbench): void
|
|
||||||
{
|
|
||||||
$provider = $workbench->providerName();
|
|
||||||
$model = $workbench->modelName($this->embeddingModel);
|
|
||||||
$dimensions = $workbench->dimensions($model);
|
|
||||||
|
|
||||||
$this->providerSummary = "{$provider} / {$model} / {$dimensions}d";
|
|
||||||
$this->databaseReady = Schema::hasTable('embedding_entries');
|
|
||||||
|
|
||||||
if (! $this->databaseReady) {
|
|
||||||
$this->storedCount = 0;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storedCount = EmbeddingEntry::query()
|
|
||||||
->where('provider', $provider)
|
|
||||||
->where('model', $model)
|
|
||||||
->where('embedding_dimensions', $dimensions)
|
|
||||||
->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -126,7 +83,7 @@ new #[Title('Embedding search')] class extends Component
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<flux:button type="submit" variant="primary" icon="magnifying-glass" :disabled="! $databaseReady" class="data-loading:opacity-60">
|
<flux:button type="submit" variant="primary" icon="magnifying-glass" :disabled="! $databaseReady || ! $ollamaAvailable" class="data-loading:opacity-60">
|
||||||
<span wire:loading.remove wire:target="searchSimilar">{{ __('Search') }}</span>
|
<span wire:loading.remove wire:target="searchSimilar">{{ __('Search') }}</span>
|
||||||
<span wire:loading wire:target="searchSimilar">{{ __('Searching') }}</span>
|
<span wire:loading wire:target="searchSimilar">{{ __('Searching') }}</span>
|
||||||
</flux:button>
|
</flux:button>
|
||||||
|
|||||||
@@ -8,16 +8,8 @@ use Laravel\Ai\Embeddings;
|
|||||||
use Laravel\Ai\Prompts\EmbeddingsPrompt;
|
use Laravel\Ai\Prompts\EmbeddingsPrompt;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
beforeEach(function () {
|
function fakeEmbeddingToolOllamaModels(): void
|
||||||
config([
|
{
|
||||||
'ai.default_for_embeddings' => 'ollama',
|
|
||||||
'ai.providers.ollama.url' => 'http://ollama.test',
|
|
||||||
'ai.providers.ollama.models.embeddings.default' => 'test-embed',
|
|
||||||
'ai.providers.ollama.models.embeddings.available' => ['test-embed'],
|
|
||||||
'ai.providers.ollama.models.embeddings.dimensions' => 3,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Http::preventStrayRequests();
|
|
||||||
Http::fake([
|
Http::fake([
|
||||||
'http://ollama.test/api/tags' => Http::response([
|
'http://ollama.test/api/tags' => Http::response([
|
||||||
'models' => [
|
'models' => [
|
||||||
@@ -26,6 +18,23 @@ beforeEach(function () {
|
|||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeUnavailableEmbeddingToolOllama(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'http://ollama.test/api/tags' => Http::response(['error' => 'offline'], 500),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config([
|
||||||
|
'ai.default_for_embeddings' => 'ollama',
|
||||||
|
'ai.providers.ollama.url' => 'http://ollama.test',
|
||||||
|
'ai.providers.ollama.models.embeddings.dimensions' => 3,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::preventStrayRequests();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('guests are redirected from embedding pages', function (string $route) {
|
test('guests are redirected from embedding pages', function (string $route) {
|
||||||
@@ -44,6 +53,8 @@ test('embeddings index redirects to embed page', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('authenticated users can open embedding pages', function (string $route, string $text) {
|
test('authenticated users can open embedding pages', function (string $route, string $text) {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
$this->get(route($route))
|
$this->get(route($route))
|
||||||
@@ -56,6 +67,8 @@ test('authenticated users can open embedding pages', function (string $route, st
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
test('comparison page renders separate phrase inputs', function () {
|
test('comparison page renders separate phrase inputs', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
$this->get(route('embeddings.compare'))
|
$this->get(route('embeddings.compare'))
|
||||||
@@ -67,6 +80,8 @@ test('comparison page renders separate phrase inputs', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('comparison phrase inputs can be added and removed', function () {
|
test('comparison phrase inputs can be added and removed', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
Livewire::test('pages::embeddings.compare')
|
Livewire::test('pages::embeddings.compare')
|
||||||
@@ -90,6 +105,8 @@ test('app header has a menu item for each embedding tool', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('embedding pages do not duplicate tool navigation below the header', function () {
|
test('embedding pages do not duplicate tool navigation below the header', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
$response = $this->get(route('embeddings.embed'))->assertOk();
|
$response = $this->get(route('embeddings.embed'))->assertOk();
|
||||||
@@ -100,6 +117,8 @@ test('embedding pages do not duplicate tool navigation below the header', functi
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('embedding model options are fetched from ollama', function () {
|
test('embedding model options are fetched from ollama', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
$component = Livewire::test('pages::embeddings.embed');
|
$component = Livewire::test('pages::embeddings.embed');
|
||||||
@@ -110,16 +129,48 @@ test('embedding model options are fetched from ollama', function () {
|
|||||||
Http::assertSent(fn ($request): bool => $request->url() === 'http://ollama.test/api/tags');
|
Http::assertSent(fn ($request): bool => $request->url() === 'http://ollama.test/api/tags');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('first ollama model is selected when configured default is unavailable', function () {
|
test('first ollama model is selected from the api model list', function () {
|
||||||
config(['ai.providers.ollama.models.embeddings.default' => 'missing-default']);
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
Livewire::test('pages::embeddings.embed')
|
Livewire::test('pages::embeddings.embed')
|
||||||
|
->assertSet('ollamaAvailable', true)
|
||||||
->assertSet('embeddingModel', 'test-embed');
|
->assertSet('embeddingModel', 'test-embed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('embedding pages show when ollama is unavailable', function () {
|
||||||
|
fakeUnavailableEmbeddingToolOllama();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
Livewire::test('pages::embeddings.embed')
|
||||||
|
->assertSet('ollamaAvailable', false)
|
||||||
|
->assertSet('embeddingModel', '')
|
||||||
|
->assertSee(__('Ollama is unavailable'))
|
||||||
|
->assertSee(__('Ollama offline'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('embedding actions stop when ollama model list is unavailable', function () {
|
||||||
|
fakeUnavailableEmbeddingToolOllama();
|
||||||
|
|
||||||
|
Embeddings::fake();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
Livewire::test('pages::embeddings.embed')
|
||||||
|
->set('embeddingInput', 'apple')
|
||||||
|
->call('generateEmbeddings')
|
||||||
|
->assertHasErrors('embeddingModel');
|
||||||
|
|
||||||
|
expect(EmbeddingEntry::query()->count())->toBe(0);
|
||||||
|
|
||||||
|
Embeddings::assertNothingGenerated();
|
||||||
|
});
|
||||||
|
|
||||||
test('phrases can be embedded and stored', function () {
|
test('phrases can be embedded and stored', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
Embeddings::fake([
|
Embeddings::fake([
|
||||||
[
|
[
|
||||||
[1, 0, 0],
|
[1, 0, 0],
|
||||||
@@ -155,6 +206,8 @@ test('phrases can be embedded and stored', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('similarity search ranks stored embeddings', function () {
|
test('similarity search ranks stored embeddings', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
Embeddings::fake([
|
Embeddings::fake([
|
||||||
[
|
[
|
||||||
[1, 0, 0],
|
[1, 0, 0],
|
||||||
@@ -196,6 +249,8 @@ test('similarity search ranks stored embeddings', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('comparison builds a pairwise similarity matrix', function () {
|
test('comparison builds a pairwise similarity matrix', function () {
|
||||||
|
fakeEmbeddingToolOllamaModels();
|
||||||
|
|
||||||
Embeddings::fake([
|
Embeddings::fake([
|
||||||
[
|
[
|
||||||
[1, 0, 0],
|
[1, 0, 0],
|
||||||
|
|||||||
Reference in New Issue
Block a user