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
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,23 @@ services:
networks:
- utopia

appwrite-embedding:
image: appwrite/embedding
environment:
EMBEDDING_MODELS: "nomic"
container_name: appwrite-embedding
ports:
- "11430:11434"
restart: unless-stopped
# persistent for caching models across restarts and preloading
volumes:
- embedding_models:/home/embedder/models
networks:
- utopia

volumes:
ollama_models:
embedding_models:

networks:
utopia:
16 changes: 15 additions & 1 deletion src/Agents/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,25 @@ abstract public function getSupportForEmbeddings(): bool;
* embedding: array<int, float>,
* tokensProcessed: int|null,
* totalDuration: int|null ,
* modelLoadingDuration: int|null
* modelLoadingDuration?: int|null
* }
*/
abstract public function embed(string $text): array;

/**
* Generate embeddings for a batch of texts (must be implemented if getSupportForEmbeddings is true).
*
* @param array<int, string> $texts
* @return array{
* embeddings: array<int, array<int, float>>,
* tokensProcessed: int|null,
* totalDuration: int|null
* }
*
* @throws \Exception
*/
abstract public function bulkEmbed(array $texts): array;

/**
* get embedding dimenion of the current model
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Agents/Adapters/Anthropic.php
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,19 @@ public function embed(string $text): array
throw new \Exception('Embeddings are not supported for this adapter.');
}

/**
* @param array<int, string> $texts
* @return array{
* embeddings: array<int, array<int, float>>,
* tokensProcessed: int|null,
* totalDuration: int|null
* }
*/
public function bulkEmbed(array $texts): 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.');
Expand Down
256 changes: 256 additions & 0 deletions src/Agents/Adapters/Appwrite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<?php

namespace Utopia\Agents\Adapters;

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

class Appwrite extends Adapter
{
/**
* NomicEmbedTextV15 - default general purpose text embedding model
*/
public const MODEL_NOMIC_EMBED_TEXT = 'nomic-embed-text';

/**
* EmbeddingGemma300M - Gemma embedding model
*/
public const MODEL_EMBEDDING_GEMMA = 'embedding-gemma';

/**
* AllMiniLML6V2 - small, fast sentence embedding model
*/
public const MODEL_ALL_MINILM = 'all-minilm';

/**
* BGESmallENV15 - small English embedding model
*/
public const MODEL_BGE_SMALL = 'bge-small';

protected string $model;

private string $endpoint = 'http://appwrite-embedding:11434/embed';

public const MODELS = [
self::MODEL_NOMIC_EMBED_TEXT,
self::MODEL_EMBEDDING_GEMMA,
self::MODEL_ALL_MINILM,
self::MODEL_BGE_SMALL,
];

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

/**
* Create a new Appwrite embedding adapter (no API key required for local call)
*/
public function __construct(
string $model = self::MODEL_NOMIC_EMBED_TEXT,
int $timeout = 90000
) {
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 (the embedding service only supports embeddings, not chat)
*
* @return array{
* embedding: array<int, float>,
* tokensProcessed: int|null,
* totalDuration: int|null
* }
*
* @throws \Exception
*/
public function embed(string $text): array
{
$result = $this->bulkEmbed([$text]);

return [
'embedding' => $result['embeddings'][0],
'tokensProcessed' => $result['tokensProcessed'],
'totalDuration' => $result['totalDuration'],
];
}

/**
* Bulk embedding generation — sends multiple texts in a single request.
*
* @param array<int, string> $texts
* @return array{
* embeddings: array<int, array<int, float>>,
* tokensProcessed: int|null,
* totalDuration: int|null
* }
*
* @throws \Exception
*/
public function bulkEmbed(array $texts): array
{
if (empty($texts)) {
throw new \InvalidArgumentException('bulkEmbed requires at least one text');
}

$client = new Client();
$client->setTimeout($this->timeout);
$client->addHeader('Content-Type', 'application/json');
$payload = [
'model' => $this->model,
'texts' => array_values($texts),
];
$response = $client->fetch(
$this->getEndpoint(),
Client::METHOD_POST,
$payload
);
Comment thread
abnegate marked this conversation as resolved.
$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(is_string($json['error']) ? $json['error'] : 'Unknown error', $response->getStatusCode());
}

if (! isset($json['embeddings']) || ! is_array($json['embeddings']) || count($json['embeddings']) !== count($texts)) {
throw new \Exception('Embedding response missing or count mismatch', $response->getStatusCode());
}

/** @var array<int, array<int, float>> $embeddings */
$embeddings = [];
foreach ($json['embeddings'] as $i => $vec) {
if (! is_array($vec) || $vec === []) {
throw new \Exception("Embedding row {$i} missing or empty", $response->getStatusCode());
}
/** @var array<int, float> $vec */
$embeddings[] = $vec;
}

return [
'embeddings' => $embeddings,
'tokensProcessed' => isset($json['tokens']) && is_int($json['tokens']) ? $json['tokens'] : null,
'totalDuration' => isset($json['total_duration']) && is_int($json['total_duration']) ? $json['total_duration'] : null,
];
}

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

/**
* Get currently selected embedding model
*/
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
*/
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
*
* @throws \Exception
*/
public function send(array $messages, ?callable $listener = null): Message
{
throw new \Exception('Appwrite does not support chat or messages. Use embed() instead.');
}

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

/**
* Get the adapter name
*/
public function getName(): string
{
return 'appwrite-embedding';
}

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

$errorValue = $json['error'] ?? ($json['message'] ?? 'Unknown error');

return is_string($errorValue) ? $errorValue : 'Unknown error';
}

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

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

return $this;
}

public function getSupportForEmbeddings(): bool
{
return true;
}
}
13 changes: 13 additions & 0 deletions src/Agents/Adapters/Deepseek.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,19 @@ public function embed(string $text): array
throw new \Exception('Embeddings are not supported for this adapter.');
}

/**
* @param array<int, string> $texts
* @return array{
* embeddings: array<int, array<int, float>>,
* tokensProcessed: int|null,
* totalDuration: int|null
* }
*/
public function bulkEmbed(array $texts): 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.');
Expand Down
13 changes: 13 additions & 0 deletions src/Agents/Adapters/Gemini.php
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,19 @@ public function embed(string $text): array
throw new \Exception('Embeddings are not supported for this adapter.');
}

/**
* @param array<int, string> $texts
* @return array{
* embeddings: array<int, array<int, float>>,
* tokensProcessed: int|null,
* totalDuration: int|null
* }
*/
public function bulkEmbed(array $texts): 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.');
Expand Down
Loading
Loading