Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 230 additions & 147 deletions composer.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,29 @@ services:
- LLM_KEY_XAI=${LLM_KEY_XAI}
- LLM_KEY_PERPLEXITY=${LLM_KEY_PERPLEXITY}
- LLM_KEY_GEMINI=${LLM_KEY_GEMINI}
depends_on:
- ollama
networks:
- utopia

ollama:
build:
context: .
dockerfile: ollama.dockerfile
args:
MODELS: "embeddinggemma"
container_name: ollama
ports:
- "11434:11434"
restart: unless-stopped
# persistent for caching models across restarts and preloading
volumes:
- ollama_models:/root/.ollama
networks:
- utopia

volumes:
ollama_models:

networks:
utopia:
21 changes: 21 additions & 0 deletions ollama.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM ollama/ollama:0.12.7

# Preload specific models
ARG MODELS="embeddinggemma"
ENV OLLAMA_KEEP_ALIVE=24h

# Pre-pull models at build time for Docker layer caching
RUN ollama serve & \
sleep 5 && \
for m in $MODELS; do \
echo "Pulling model $m..."; \
ollama pull $m || exit 1; \
done && \
pkill ollama

# Expose Ollama default port
EXPOSE 11434

# On container start, quickly ensure models exist (no re-download unless missing)
ENTRYPOINT ["/bin/bash", "-c", "(sleep 2; for m in $MODELS; do ollama list | grep -q $m || ollama pull $m; done) & exec ollama $0"]
CMD ["serve"]
24 changes: 24 additions & 0 deletions src/Agents/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ abstract public function setModel(string $model): self;
*/
abstract public function isSchemaSupported(): bool;

/**
* Does this adapter support embeddings?
*
* @return bool
*/
abstract public function getSupportForEmbeddings(): bool;

/**
* Generate embedding for input text (must be implemented if getSupportForEmbeddings is true)
*
* @param string $text
* @return array{
* embedding: array<int, float>,
* total_duration: int|null,
* load_duration: int|null
* }
*/
abstract public function embed(string $text): array;

/**
* get embedding dimenion of the current model
*/
abstract public function getEmbeddingDimension(): int;

/**
* Format error message
*
Expand Down
23 changes: 23 additions & 0 deletions src/Agents/Adapters/Anthropic.php
Original file line number Diff line number Diff line change
Expand Up @@ -457,4 +457,27 @@ protected function formatErrorMessage($json): string

return '('.$errorType.') '.$errorMessage;
}

public function getSupportForEmbeddings(): bool
{
return false;
}

/**
* @param string $text
* @return array{
* embedding: array<int, float>,
* total_duration: int|null,
* load_duration: int|null
* }
*/
public function embed(string $text): array
{
throw new \Exception('Embeddings are not supported for this adapter.');
}

public function getEmbeddingDimension(): int
{
throw new \Exception('Embeddings are not supported for this adapter.');
}
}
23 changes: 23 additions & 0 deletions src/Agents/Adapters/Deepseek.php
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,27 @@ protected function formatErrorMessage($json): string

return '('.$errorType.') '.$errorMessage;
}

public function getSupportForEmbeddings(): bool
{
return false;
}

/**
* @param string $text
* @return array{
* embedding: array<int, float>,
* total_duration: int|null,
* load_duration: int|null
* }
*/
public function embed(string $text): array
{
throw new \Exception('Embeddings are not supported for this adapter.');
}

public function getEmbeddingDimension(): int
{
throw new \Exception('Embeddings are not supported for this adapter.');
}
}
23 changes: 23 additions & 0 deletions src/Agents/Adapters/Gemini.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,27 @@ protected function formatErrorMessage($json): string

return '('.$errorType.') '.$errorMessage.PHP_EOL.$errorDetails;
}

public function getSupportForEmbeddings(): bool
{
return false;
}

/**
* @param string $text
* @return array{
* embedding: array<int, float>,
* total_duration: int|null,
* load_duration: int|null
* }
*/
public function embed(string $text): array
{
throw new \Exception('Embeddings are not supported for this adapter.');
}

public function getEmbeddingDimension(): int
{
throw new \Exception('Embeddings are not supported for this adapter.');
}
}
213 changes: 213 additions & 0 deletions src/Agents/Adapters/Ollama.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php

namespace Utopia\Agents\Adapters;

use Utopia\Agents\Adapter;
use Utopia\Agents\Message;
use Utopia\Fetch\Client;

class Ollama extends Adapter
{
/**
* EmbeddingGemma - Gemma embedding model for Ollama
*/
public const MODEL_EMBEDDING_GEMMA = 'embeddinggemma';

/**
* @var string
*/
protected string $model;

private string $endpoint = 'http://ollama:11434/api/embed';

public const MODELS = [self::MODEL_EMBEDDING_GEMMA];

/**
* Embedding dimensions of specific embedding model
*/
protected const DIMENSIONS = [
self::MODEL_EMBEDDING_GEMMA => 768,
];

/**
* Create a new Ollama adapter (no API key required for local call)
*
* @param string $model
* @param int $timeout
*/
public function __construct(
string $model = self::MODEL_EMBEDDING_GEMMA,
int $timeout = 90
) {
if (! in_array($model, self::MODELS, true)) {
throw new \InvalidArgumentException("Invalid model: {$model}. Supported models: ".implode(', ', self::MODELS));
}

$this->model = $model;
$this->setTimeout($timeout);
}

/**
* Embedding generation (Ollama only supports embeddings, not chat)
*
* @param string $text
* @return array{
* embedding: array<int, float>,
* total_duration: int|null,
* load_duration: int|null
* }
*
* @throws \Exception
*/
public function embed(string $text): array
{
$client = new Client();
$client->setTimeout($this->timeout);
$client->addHeader('Content-Type', 'application/json');
$payload = [
'model' => $this->model,
'input' => $text,
];
$response = $client->fetch(
$this->getEndpoint(),
Client::METHOD_POST,
$payload
);
$body = $response->getBody();
$json = is_string($body) ? json_decode($body, true) : null;

if (! is_array($json)) {
throw new \Exception('Invalid response format received from the API');
}

if (isset($json['error'])) {
throw new \Exception($json['error'], $response->getStatusCode());
}

return [
'embedding' => $json['embeddings'][0] ?? [],
'total_duration' => $json['total_duration'] ?? null,
'load_duration' => $json['load_duration'] ?? null,
];
}

/**
* Get available models for embeddings (for now, only embeddinggemma)
*
* @return array<string>
*/
public function getModels(): array
{
return self::MODELS;
}

/**
* Get currently selected embedding model
*
* @return string
*/
public function getModel(): string
{
return $this->model;
}

/**
* get embedding dimenion of the current model
*/
public function getEmbeddingDimension(): int
{
return self::DIMENSIONS[$this->model];
}

/**
* Set model to use for embedding
*
* @param string $model
* @return self
*/
public function setModel(string $model): self
{
if (! in_array($model, self::MODELS, true)) {
throw new \InvalidArgumentException("Invalid model: {$model}. Supported models: ".implode(', ', self::MODELS));
}
$this->model = $model;

return $this;
}

/**
* Not applicable for embedding-only adapters.
*
* @param array<\Utopia\Agents\Message> $messages
* @param callable|null $listener
*
* @throws \Exception
*/
public function send(array $messages, ?callable $listener = null): Message
{
throw new \Exception('OllamaAdapter does not support chat or messages. Use embed() instead.');
}

/**
* Embeddings do not support schema.
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return false;
}

/**
* Get the adapter name
*
* @return string
*/
public function getName(): string
{
return 'ollama';
}

/**
* Error formatter (minimal)
*
* @param mixed $json
* @return string
*/
protected function formatErrorMessage($json): string
{
if (! is_array($json)) {
return '(unknown_error) Unknown error';
}

return $json['error'] ?? ($json['message'] ?? 'Unknown error');
}

/**
* Get the API endpoint
*
* @return string
*/
public function getEndpoint(): string
{
return $this->endpoint;
}

/**
* Set the API endpoint
*
* @param string $endpoint
* @return self
*/
public function setEndpoint(string $endpoint): self
{
$this->endpoint = $endpoint;

return $this;
}

public function getSupportForEmbeddings(): bool
{
return true;
}
}
Loading