Skip to content

Commit 7bbc77d

Browse files
authored
Merge pull request #15 from utopia-php/feat-embeddings
Added ollama agent for embeddings
2 parents 667da99 + 083113d commit 7bbc77d

13 files changed

Lines changed: 709 additions & 147 deletions

File tree

composer.lock

Lines changed: 230 additions & 147 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docker-compose.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,29 @@ services:
1616
- LLM_KEY_XAI=${LLM_KEY_XAI}
1717
- LLM_KEY_PERPLEXITY=${LLM_KEY_PERPLEXITY}
1818
- LLM_KEY_GEMINI=${LLM_KEY_GEMINI}
19+
depends_on:
20+
- ollama
1921
networks:
2022
- utopia
2123

24+
ollama:
25+
build:
26+
context: .
27+
dockerfile: ollama.dockerfile
28+
args:
29+
MODELS: "embeddinggemma"
30+
container_name: ollama
31+
ports:
32+
- "11434:11434"
33+
restart: unless-stopped
34+
# persistent for caching models across restarts and preloading
35+
volumes:
36+
- ollama_models:/root/.ollama
37+
networks:
38+
- utopia
39+
40+
volumes:
41+
ollama_models:
42+
2243
networks:
2344
utopia:

ollama.dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM ollama/ollama:0.12.7
2+
3+
# Preload specific models
4+
ARG MODELS="embeddinggemma"
5+
ENV OLLAMA_KEEP_ALIVE=24h
6+
7+
# Pre-pull models at build time for Docker layer caching
8+
RUN ollama serve & \
9+
sleep 5 && \
10+
for m in $MODELS; do \
11+
echo "Pulling model $m..."; \
12+
ollama pull $m || exit 1; \
13+
done && \
14+
pkill ollama
15+
16+
# Expose Ollama default port
17+
EXPOSE 11434
18+
19+
# On container start, quickly ensure models exist (no re-download unless missing)
20+
ENTRYPOINT ["/bin/bash", "-c", "(sleep 2; for m in $MODELS; do ollama list | grep -q $m || ollama pull $m; done) & exec ollama $0"]
21+
CMD ["serve"]

src/Agents/Adapter.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,30 @@ abstract public function setModel(string $model): self;
9393
*/
9494
abstract public function isSchemaSupported(): bool;
9595

96+
/**
97+
* Does this adapter support embeddings?
98+
*
99+
* @return bool
100+
*/
101+
abstract public function getSupportForEmbeddings(): bool;
102+
103+
/**
104+
* Generate embedding for input text (must be implemented if getSupportForEmbeddings is true)
105+
*
106+
* @param string $text
107+
* @return array{
108+
* embedding: array<int, float>,
109+
* total_duration: int|null,
110+
* load_duration: int|null
111+
* }
112+
*/
113+
abstract public function embed(string $text): array;
114+
115+
/**
116+
* get embedding dimenion of the current model
117+
*/
118+
abstract public function getEmbeddingDimension(): int;
119+
96120
/**
97121
* Format error message
98122
*

src/Agents/Adapters/Anthropic.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,4 +457,27 @@ protected function formatErrorMessage($json): string
457457

458458
return '('.$errorType.') '.$errorMessage;
459459
}
460+
461+
public function getSupportForEmbeddings(): bool
462+
{
463+
return false;
464+
}
465+
466+
/**
467+
* @param string $text
468+
* @return array{
469+
* embedding: array<int, float>,
470+
* total_duration: int|null,
471+
* load_duration: int|null
472+
* }
473+
*/
474+
public function embed(string $text): array
475+
{
476+
throw new \Exception('Embeddings are not supported for this adapter.');
477+
}
478+
479+
public function getEmbeddingDimension(): int
480+
{
481+
throw new \Exception('Embeddings are not supported for this adapter.');
482+
}
460483
}

src/Agents/Adapters/Deepseek.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,27 @@ protected function formatErrorMessage($json): string
317317

318318
return '('.$errorType.') '.$errorMessage;
319319
}
320+
321+
public function getSupportForEmbeddings(): bool
322+
{
323+
return false;
324+
}
325+
326+
/**
327+
* @param string $text
328+
* @return array{
329+
* embedding: array<int, float>,
330+
* total_duration: int|null,
331+
* load_duration: int|null
332+
* }
333+
*/
334+
public function embed(string $text): array
335+
{
336+
throw new \Exception('Embeddings are not supported for this adapter.');
337+
}
338+
339+
public function getEmbeddingDimension(): int
340+
{
341+
throw new \Exception('Embeddings are not supported for this adapter.');
342+
}
320343
}

src/Agents/Adapters/Gemini.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,27 @@ protected function formatErrorMessage($json): string
348348

349349
return '('.$errorType.') '.$errorMessage.PHP_EOL.$errorDetails;
350350
}
351+
352+
public function getSupportForEmbeddings(): bool
353+
{
354+
return false;
355+
}
356+
357+
/**
358+
* @param string $text
359+
* @return array{
360+
* embedding: array<int, float>,
361+
* total_duration: int|null,
362+
* load_duration: int|null
363+
* }
364+
*/
365+
public function embed(string $text): array
366+
{
367+
throw new \Exception('Embeddings are not supported for this adapter.');
368+
}
369+
370+
public function getEmbeddingDimension(): int
371+
{
372+
throw new \Exception('Embeddings are not supported for this adapter.');
373+
}
351374
}

src/Agents/Adapters/Ollama.php

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<?php
2+
3+
namespace Utopia\Agents\Adapters;
4+
5+
use Utopia\Agents\Adapter;
6+
use Utopia\Agents\Message;
7+
use Utopia\Fetch\Client;
8+
9+
class Ollama extends Adapter
10+
{
11+
/**
12+
* EmbeddingGemma - Gemma embedding model for Ollama
13+
*/
14+
public const MODEL_EMBEDDING_GEMMA = 'embeddinggemma';
15+
16+
/**
17+
* @var string
18+
*/
19+
protected string $model;
20+
21+
private string $endpoint = 'http://ollama:11434/api/embed';
22+
23+
public const MODELS = [self::MODEL_EMBEDDING_GEMMA];
24+
25+
/**
26+
* Embedding dimensions of specific embedding model
27+
*/
28+
protected const DIMENSIONS = [
29+
self::MODEL_EMBEDDING_GEMMA => 768,
30+
];
31+
32+
/**
33+
* Create a new Ollama adapter (no API key required for local call)
34+
*
35+
* @param string $model
36+
* @param int $timeout
37+
*/
38+
public function __construct(
39+
string $model = self::MODEL_EMBEDDING_GEMMA,
40+
int $timeout = 90
41+
) {
42+
if (! in_array($model, self::MODELS, true)) {
43+
throw new \InvalidArgumentException("Invalid model: {$model}. Supported models: ".implode(', ', self::MODELS));
44+
}
45+
46+
$this->model = $model;
47+
$this->setTimeout($timeout);
48+
}
49+
50+
/**
51+
* Embedding generation (Ollama only supports embeddings, not chat)
52+
*
53+
* @param string $text
54+
* @return array{
55+
* embedding: array<int, float>,
56+
* total_duration: int|null,
57+
* load_duration: int|null
58+
* }
59+
*
60+
* @throws \Exception
61+
*/
62+
public function embed(string $text): array
63+
{
64+
$client = new Client();
65+
$client->setTimeout($this->timeout);
66+
$client->addHeader('Content-Type', 'application/json');
67+
$payload = [
68+
'model' => $this->model,
69+
'input' => $text,
70+
];
71+
$response = $client->fetch(
72+
$this->getEndpoint(),
73+
Client::METHOD_POST,
74+
$payload
75+
);
76+
$body = $response->getBody();
77+
$json = is_string($body) ? json_decode($body, true) : null;
78+
79+
if (! is_array($json)) {
80+
throw new \Exception('Invalid response format received from the API');
81+
}
82+
83+
if (isset($json['error'])) {
84+
throw new \Exception($json['error'], $response->getStatusCode());
85+
}
86+
87+
return [
88+
'embedding' => $json['embeddings'][0] ?? [],
89+
'total_duration' => $json['total_duration'] ?? null,
90+
'load_duration' => $json['load_duration'] ?? null,
91+
];
92+
}
93+
94+
/**
95+
* Get available models for embeddings (for now, only embeddinggemma)
96+
*
97+
* @return array<string>
98+
*/
99+
public function getModels(): array
100+
{
101+
return self::MODELS;
102+
}
103+
104+
/**
105+
* Get currently selected embedding model
106+
*
107+
* @return string
108+
*/
109+
public function getModel(): string
110+
{
111+
return $this->model;
112+
}
113+
114+
/**
115+
* get embedding dimenion of the current model
116+
*/
117+
public function getEmbeddingDimension(): int
118+
{
119+
return self::DIMENSIONS[$this->model];
120+
}
121+
122+
/**
123+
* Set model to use for embedding
124+
*
125+
* @param string $model
126+
* @return self
127+
*/
128+
public function setModel(string $model): self
129+
{
130+
if (! in_array($model, self::MODELS, true)) {
131+
throw new \InvalidArgumentException("Invalid model: {$model}. Supported models: ".implode(', ', self::MODELS));
132+
}
133+
$this->model = $model;
134+
135+
return $this;
136+
}
137+
138+
/**
139+
* Not applicable for embedding-only adapters.
140+
*
141+
* @param array<\Utopia\Agents\Message> $messages
142+
* @param callable|null $listener
143+
*
144+
* @throws \Exception
145+
*/
146+
public function send(array $messages, ?callable $listener = null): Message
147+
{
148+
throw new \Exception('OllamaAdapter does not support chat or messages. Use embed() instead.');
149+
}
150+
151+
/**
152+
* Embeddings do not support schema.
153+
*
154+
* @return bool
155+
*/
156+
public function isSchemaSupported(): bool
157+
{
158+
return false;
159+
}
160+
161+
/**
162+
* Get the adapter name
163+
*
164+
* @return string
165+
*/
166+
public function getName(): string
167+
{
168+
return 'ollama';
169+
}
170+
171+
/**
172+
* Error formatter (minimal)
173+
*
174+
* @param mixed $json
175+
* @return string
176+
*/
177+
protected function formatErrorMessage($json): string
178+
{
179+
if (! is_array($json)) {
180+
return '(unknown_error) Unknown error';
181+
}
182+
183+
return $json['error'] ?? ($json['message'] ?? 'Unknown error');
184+
}
185+
186+
/**
187+
* Get the API endpoint
188+
*
189+
* @return string
190+
*/
191+
public function getEndpoint(): string
192+
{
193+
return $this->endpoint;
194+
}
195+
196+
/**
197+
* Set the API endpoint
198+
*
199+
* @param string $endpoint
200+
* @return self
201+
*/
202+
public function setEndpoint(string $endpoint): self
203+
{
204+
$this->endpoint = $endpoint;
205+
206+
return $this;
207+
}
208+
209+
public function getSupportForEmbeddings(): bool
210+
{
211+
return true;
212+
}
213+
}

0 commit comments

Comments
 (0)