Skip to content

Commit 67085ed

Browse files
feat(store): Integrate supabase into the store package
- Adds support for supabase
1 parent d8dfb30 commit 67085ed

File tree

4 files changed

+503
-38
lines changed

4 files changed

+503
-38
lines changed

examples/.env

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -105,41 +105,9 @@ LMSTUDIO_HOST_URL=http://127.0.0.1:1234
105105
QDRANT_HOST=http://127.0.0.1:6333
106106
QDRANT_SERVICE_API_KEY=changeMe
107107

108-
# SurrealDB (store)
109-
SURREALDB_HOST=http://127.0.0.1:8000
110-
SURREALDB_USER=symfony
111-
SURREALDB_PASS=symfony
112-
113-
# Neo4J (store)
114-
NEO4J_HOST=http://127.0.0.1:7474
115-
NEO4J_DATABASE=neo4j
116-
NEO4J_USERNAME=neo4j
117-
NEO4J_PASSWORD=symfonyai
118-
119-
# Typesense (store)
120-
TYPESENSE_HOST=http://127.0.0.1:8108
121-
TYPESENSE_API_KEY=changeMe
122-
123-
# Milvus (store)
124-
MILVUS_HOST=http://127.0.0.1:19530
125-
MILVUS_API_KEY=root:Milvus
126-
MILVUS_DATABASE=symfony
127-
128-
# Cloudflare (store)
129-
CLOUDFLARE_ACCOUNT_ID=
130-
CLOUDFLARE_API_KEY=
131-
132-
# Cerebras
133-
CEREBRAS_API_KEY=
134-
135-
CHROMADB_HOST=http://127.0.0.1
136-
CHROMADB_PORT=8001
137-
138-
# For using Clickhouse (store)
139-
CLICKHOUSE_HOST=http://symfony:[email protected]:8123
140-
CLICKHOUSE_DATABASE=symfony
141-
CLICKHOUSE_TABLE=symfony
142-
143-
# Weaviate (store)
144-
WEAVIATE_HOST=http://127.0.0.1:8080
145-
WEAVIATE_API_KEY=symfony
108+
SUPABASE_URL=
109+
SUPABASE_API_KEY=
110+
SUPABASE_TABLE=
111+
SUPABASE_VECTOR_FIELD=
112+
SUPABASE_VECTOR_DIMENSION=
113+
SUPABASE_MATCH_FUNCTION=

examples/rag/supabase.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
14+
use Symfony\AI\Agent\Toolbox\Tool\SimilaritySearch;
15+
use Symfony\AI\Agent\Toolbox\Toolbox;
16+
use Symfony\AI\Fixtures\Movies;
17+
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
18+
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
19+
use Symfony\AI\Platform\Message\Message;
20+
use Symfony\AI\Platform\Message\MessageBag;
21+
use Symfony\AI\Store\Bridge\Supabase\Store;
22+
use Symfony\AI\Store\Document\Loader\InMemoryLoader;
23+
use Symfony\AI\Store\Document\Metadata;
24+
use Symfony\AI\Store\Document\TextDocument;
25+
use Symfony\AI\Store\Document\Vectorizer;
26+
use Symfony\AI\Store\Indexer;
27+
use Symfony\Component\Uid\Uuid;
28+
29+
require_once dirname(__DIR__).'/bootstrap.php';
30+
31+
echo "Make sure you've run the SQL setup from SUPABASE_SETUP.md first!\n\n";
32+
33+
$store = new Store(
34+
http: http_client(),
35+
url: env('SUPABASE_URL'),
36+
apiKey: env('SUPABASE_API_KEY'),
37+
table: env('SUPABASE_TABLE'),
38+
vectorFieldName: env('SUPABASE_VECTOR_FIELD'),
39+
vectorDimension: (int) env('SUPABASE_VECTOR_DIMENSION'),
40+
functionName: env('SUPABASE_MATCH_FUNCTION')
41+
);
42+
43+
$documents = [];
44+
45+
foreach (Movies::all() as $movie) {
46+
$documents[] = new TextDocument(
47+
id: Uuid::v4(),
48+
content: 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description'],
49+
metadata: new Metadata($movie),
50+
);
51+
}
52+
53+
$platform = PlatformFactory::create(
54+
env('OLLAMA_HOST_URL') ?? 'http://localhost:11434',
55+
http_client()
56+
);
57+
58+
$embeddingModel = new Ollama('mxbai-embed-large');
59+
$vectorizer = new Vectorizer($platform, $embeddingModel);
60+
$loader = new InMemoryLoader($documents);
61+
$indexer = new Indexer($loader, $vectorizer, $store, logger: logger());
62+
$indexer->index();
63+
64+
$chatModel = new Ollama('llama3.2:3b');
65+
66+
$similaritySearch = new SimilaritySearch($vectorizer, $store);
67+
$toolbox = new Toolbox([$similaritySearch], logger: logger());
68+
$processor = new AgentProcessor($toolbox);
69+
$agent = new Agent($platform, $chatModel, [$processor], [$processor], logger: logger());
70+
71+
$messages = new MessageBag(
72+
Message::forSystem('Please answer all user questions only using SimilaritySearch function.'),
73+
Message::ofUser('Which movie fits the theme of technology?')
74+
);
75+
76+
echo "Query: Which movie fits the theme of technology?\n";
77+
echo "Processing...\n";
78+
79+
try {
80+
$result = $agent->call($messages);
81+
echo '✅ Response: '.$result->getContent()."\n\n";
82+
} catch (Exception $e) {
83+
echo '❌ Error: '.$e->getMessage()."\n\n";
84+
}
85+
86+
$messages2 = new MessageBag(
87+
Message::forSystem('Please answer all user questions only using SimilaritySearch function.'),
88+
Message::ofUser('What are some good action movies?')
89+
);
90+
91+
echo "Query: What are some good action movies?\n";
92+
echo "Processing...\n";
93+
94+
try {
95+
$result2 = $agent->call($messages2);
96+
echo '✅ Response: '.$result2->getContent()."\n\n";
97+
} catch (Exception $e) {
98+
echo '❌ Error: '.$e->getMessage()."\n\n";
99+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Store\Bridge\Supabase;
13+
14+
use Symfony\AI\Platform\Vector\Vector;
15+
use Symfony\AI\Store\Document\Metadata;
16+
use Symfony\AI\Store\Document\VectorDocument;
17+
use Symfony\AI\Store\Exception\InvalidArgumentException;
18+
use Symfony\AI\Store\Exception\RuntimeException;
19+
use Symfony\AI\Store\StoreInterface;
20+
use Symfony\Component\Uid\Uuid;
21+
use Symfony\Contracts\HttpClient\HttpClientInterface;
22+
23+
/**
24+
* @author Junaid Farooq <[email protected]>
25+
*
26+
* Requires pgvector extension to be enabled and a pre-configured table/function.
27+
*
28+
* @see https://github.com/pgvector/pgvector
29+
* @see https://supabase.com/docs/guides/ai/vector-columns
30+
*
31+
* Supabase store using REST & RPC.
32+
*
33+
* Note: Unlike Postgres Store, this store requires manual setup of:
34+
* 1. pgvector extension
35+
* 2. Table with vector column
36+
* 3. RPC function for similarity search
37+
*
38+
* This is because Supabase doesn't allow arbitrary SQL execution via REST API.
39+
*/
40+
final readonly class Store implements StoreInterface
41+
{
42+
public function __construct(
43+
private HttpClientInterface $http,
44+
private string $url,
45+
private string $apiKey,
46+
private string $table = 'documents',
47+
private string $vectorFieldName = 'embedding',
48+
private int $vectorDimension = 1536,
49+
private string $functionName = 'match_documents',
50+
) {
51+
}
52+
53+
public function add(VectorDocument ...$documents): void
54+
{
55+
if (0 === \count($documents)) {
56+
return;
57+
}
58+
59+
$rows = [];
60+
61+
foreach ($documents as $document) {
62+
if (\count($document->vector->getData()) !== $this->vectorDimension) {
63+
continue;
64+
}
65+
66+
$rows[] = [
67+
'id' => $document->id->toRfc4122(),
68+
$this->vectorFieldName => $document->vector->getData(),
69+
'metadata' => $document->metadata->getArrayCopy(),
70+
];
71+
}
72+
73+
$chunkSize = 200;
74+
75+
foreach (array_chunk($rows, $chunkSize) as $chunk) {
76+
$response = $this->http->request(
77+
'POST',
78+
\sprintf('%s/rest/v1/%s', $this->url, $this->table),
79+
[
80+
'headers' => [
81+
'apikey' => $this->apiKey,
82+
'Authorization' => 'Bearer '.$this->apiKey,
83+
'Content-Type' => 'application/json',
84+
'Prefer' => 'resolution=merge-duplicates',
85+
],
86+
'json' => $chunk,
87+
]
88+
);
89+
90+
if ($response->getStatusCode() >= 400) {
91+
throw new RuntimeException('Supabase insert failed: '.$response->getContent(false));
92+
}
93+
}
94+
}
95+
96+
/**
97+
* @param array{
98+
* max_items?: int,
99+
* limit?: int,
100+
* min_score?: float
101+
* } $options
102+
*/
103+
public function query(Vector $vector, array $options = []): array
104+
{
105+
if (\count($vector->getData()) !== $this->vectorDimension) {
106+
throw new InvalidArgumentException("Vector dimension mismatch: expected {$this->vectorDimension}");
107+
}
108+
109+
$matchCount = $options['max_items'] ?? ($options['limit'] ?? 10);
110+
$threshold = $options['min_score'] ?? 0.0;
111+
112+
$response = $this->http->request(
113+
'POST',
114+
\sprintf('%s/rest/v1/rpc/%s', $this->url, $this->functionName),
115+
[
116+
'headers' => [
117+
'apikey' => $this->apiKey,
118+
'Authorization' => 'Bearer '.$this->apiKey,
119+
'Content-Type' => 'application/json',
120+
],
121+
'json' => [
122+
'query_embedding' => $vector->getData(),
123+
'match_count' => $matchCount,
124+
'match_threshold' => $threshold,
125+
],
126+
]
127+
);
128+
129+
if ($response->getStatusCode() >= 400) {
130+
throw new RuntimeException('Supabase query failed: '.$response->getContent(false));
131+
}
132+
133+
$records = json_decode($response->getContent(), true, 512, \JSON_THROW_ON_ERROR);
134+
$documents = [];
135+
136+
foreach ($records as $record) {
137+
if (
138+
!isset($record['id'], $record[$this->vectorFieldName], $record['metadata'], $record['score'])
139+
|| !\is_string($record['id'])
140+
) {
141+
continue;
142+
}
143+
144+
$embedding = \is_array($record[$this->vectorFieldName]) ? $record[$this->vectorFieldName] : json_decode($record[$this->vectorFieldName] ?? '{}', true, 512, \JSON_THROW_ON_ERROR);
145+
$metadata = \is_array($record['metadata']) ? $record['metadata'] : json_decode($record['metadata'] ?? '{}', true, 512, \JSON_THROW_ON_ERROR);
146+
147+
$documents[] = new VectorDocument(
148+
id: Uuid::fromString($record['id']),
149+
vector: new Vector($embedding),
150+
metadata: new Metadata($metadata),
151+
score: (float) $record['score'],
152+
);
153+
}
154+
155+
return $documents;
156+
}
157+
}

0 commit comments

Comments
 (0)