Skip to content

Commit e55333d

Browse files
committed
feat(store): cloudflare
1 parent c9bbb95 commit e55333d

File tree

10 files changed

+525
-0
lines changed

10 files changed

+525
-0
lines changed

examples/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ MILVUS_HOST=http://127.0.0.1:19530
118118
MILVUS_API_KEY=root:Milvus
119119
MILVUS_DATABASE=symfony
120120

121+
# Cloudflare (store)
122+
CLOUDFLARE_ACCOUNT_ID=
123+
CLOUDFLARE_API_KEY=
124+
121125
# Cerebras
122126
CEREBRAS_API_KEY=
123127

examples/rag/cloudflare.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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\OpenAi\Embeddings;
18+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
19+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
20+
use Symfony\AI\Platform\Message\Message;
21+
use Symfony\AI\Platform\Message\MessageBag;
22+
use Symfony\AI\Store\Bridge\Cloudflare\Store;
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+
// initialize the store
32+
$store = new Store(
33+
httpClient: http_client(),
34+
accountId: env('CLOUDFLARE_ACCOUNT_ID'),
35+
apiKey: env('CLOUDFLARE_API_KEY'),
36+
index: 'movies',
37+
);
38+
39+
// initialize the index
40+
$store->setup();
41+
42+
// create embeddings and documents
43+
$documents = [];
44+
foreach (Movies::all() as $i => $movie) {
45+
$documents[] = new TextDocument(
46+
id: Uuid::v4(),
47+
content: 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description'],
48+
metadata: new Metadata($movie),
49+
);
50+
}
51+
52+
// create embeddings for documents (keep in mind that upserting vectors is asynchronous)
53+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
54+
$vectorizer = new Vectorizer($platform, $embeddings = new Embeddings());
55+
$indexer = new Indexer($vectorizer, $store, logger());
56+
$indexer->index($documents);
57+
58+
$model = new Gpt(Gpt::GPT_4O_MINI);
59+
60+
$similaritySearch = new SimilaritySearch($platform, $embeddings, $store);
61+
$toolbox = new Toolbox([$similaritySearch], logger: logger());
62+
$processor = new AgentProcessor($toolbox);
63+
$agent = new Agent($platform, $model, [$processor], [$processor], logger());
64+
65+
$messages = new MessageBag(
66+
Message::forSystem('Please answer all user questions only using SimilaritySearch function.'),
67+
Message::ofUser('Which movie fits the theme of technology?')
68+
);
69+
$result = $agent->call($messages);
70+
71+
echo $result->getContent().\PHP_EOL;

src/ai-bundle/config/options.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,19 @@
227227
->end()
228228
->end()
229229
->end()
230+
->arrayNode('cloudflare')
231+
->useAttributeAsKey('name')
232+
->arrayPrototype()
233+
->children()
234+
->scalarNode('account_id')->cannotBeEmpty()->end()
235+
->scalarNode('api_key')->cannotBeEmpty()->end()
236+
->scalarNode('index_name')->cannotBeEmpty()->end()
237+
->integerNode('dimensions')->end()
238+
->scalarNode('metric')->end()
239+
->scalarNode('endpoint_url')->end()
240+
->end()
241+
->end()
242+
->end()
230243
->arrayNode('meilisearch')
231244
->useAttributeAsKey('name')
232245
->arrayPrototype()

src/ai-bundle/src/AiBundle.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureSearchStore;
5151
use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore;
5252
use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore;
53+
use Symfony\AI\Store\Bridge\Cloudflare\Store as CloudflareStore;
5354
use Symfony\AI\Store\Bridge\Local\CacheStore;
5455
use Symfony\AI\Store\Bridge\Local\DistanceCalculator;
5556
use Symfony\AI\Store\Bridge\Local\DistanceStrategy;
@@ -685,6 +686,36 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde
685686
}
686687
}
687688

689+
if ('cloudflare' === $type) {
690+
foreach ($stores as $name => $store) {
691+
$arguments = [
692+
new Reference('http_client'),
693+
$store['account_id'],
694+
$store['api_key'],
695+
$store['index_name'],
696+
];
697+
698+
if (\array_key_exists('dimensions', $store)) {
699+
$arguments[4] = $store['dimensions'];
700+
}
701+
702+
if (\array_key_exists('metric', $store)) {
703+
$arguments[5] = $store['metric'];
704+
}
705+
706+
if (\array_key_exists('endpoint', $store)) {
707+
$arguments[6] = $store['endpoint'];
708+
}
709+
710+
$definition = new Definition(CloudflareStore::class);
711+
$definition
712+
->addTag('ai.store')
713+
->setArguments($arguments);
714+
715+
$container->setDefinition('ai.store.'.$type.'.'.$name, $definition);
716+
}
717+
}
718+
688719
if ('meilisearch' === $type) {
689720
foreach ($stores as $name => $store) {
690721
$arguments = [

src/ai-bundle/tests/DependencyInjection/AiBundleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,16 @@ private function getFullConfig(): array
691691
'table' => 'my_table',
692692
],
693693
],
694+
'cloudflare' => [
695+
'my_cloudflare_store' => [
696+
'account_id' => 'foo',
697+
'api_key' => 'bar',
698+
'index_name' => 'random',
699+
'dimensions' => 1536,
700+
'metric' => 'cosine',
701+
'endpoint_url' => 'https://api.cloudflare.com/client/v5/accounts',
702+
],
703+
],
694704
'meilisearch' => [
695705
'my_meilisearch_store' => [
696706
'endpoint' => 'http://127.0.0.1:7700',

src/store/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ CHANGELOG
3737
- Azure AI Search
3838
- ChromaDB
3939
- ClickHouse
40+
- Cloudflare
4041
- MariaDB
4142
- Meilisearch
4243
- MongoDB

src/store/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"azure",
88
"chromadb",
99
"clickhouse",
10+
"cloudflare",
1011
"mariadb",
1112
"meilisearch",
1213
"milvus",

src/store/doc/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ used vector store::
3737

3838
You can find more advanced usage in combination with an Agent using the store for RAG in the examples folder:
3939

40+
* `Similarity Search with Cloudflare (RAG)`_
4041
* `Similarity Search with MariaDB (RAG)`_
4142
* `Similarity Search with Meilisearch (RAG)`_
4243
* `Similarity Search with memory storage (RAG)`_
@@ -61,6 +62,7 @@ Supported Stores
6162

6263
* `Azure AI Search`_
6364
* `Chroma`_ (requires `codewithkyrian/chromadb-php` as additional dependency)
65+
* `Cloudflare`_
6466
* `InMemory`_
6567
* `MariaDB`_ (requires `ext-pdo`)
6668
* `Meilisearch`_
@@ -104,6 +106,7 @@ This leads to a store implementing two methods::
104106
}
105107

106108
.. _`Retrieval Augmented Generation`: https://de.wikipedia.org/wiki/Retrieval-Augmented_Generation
109+
.. _`Similarity Search with Cloudflare (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/cloudflare.php
107110
.. _`Similarity Search with MariaDB (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/mariadb-gemini.php
108111
.. _`Similarity Search with Meilisearch (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/meilisearch.php
109112
.. _`Similarity Search with memory storage (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/in-memory.php
@@ -118,6 +121,7 @@ This leads to a store implementing two methods::
118121
.. _`Similarity Search with Weaviate (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/weaviate.php
119122
.. _`Azure AI Search`: https://azure.microsoft.com/products/ai-services/ai-search
120123
.. _`Chroma`: https://www.trychroma.com/
124+
.. _`Cloudflare`: https://developers.cloudflare.com/vectorize/
121125
.. _`MariaDB`: https://mariadb.org/projects/mariadb-vector/
122126
.. _`Pinecone`: https://www.pinecone.io/
123127
.. _`Postgres`: https://www.postgresql.org/about/news/pgvector-070-released-2852/
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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\Cloudflare;
13+
14+
use Symfony\AI\Platform\Vector\NullVector;
15+
use Symfony\AI\Platform\Vector\Vector;
16+
use Symfony\AI\Store\Document\Metadata;
17+
use Symfony\AI\Store\Document\VectorDocument;
18+
use Symfony\AI\Store\Exception\InvalidArgumentException;
19+
use Symfony\AI\Store\ManagedStoreInterface;
20+
use Symfony\AI\Store\StoreInterface;
21+
use Symfony\Component\Uid\Uuid;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
24+
/**
25+
* @author Guillaume Loulier <[email protected]>
26+
*/
27+
final readonly class Store implements ManagedStoreInterface, StoreInterface
28+
{
29+
public function __construct(
30+
private HttpClientInterface $httpClient,
31+
private string $accountId,
32+
#[\SensitiveParameter] private string $apiKey,
33+
private string $index,
34+
private int $dimensions = 1536,
35+
private string $metric = 'cosine',
36+
private string $endpointUrl = 'https://api.cloudflare.com/client/v4/accounts',
37+
) {
38+
}
39+
40+
public function setup(array $options = []): void
41+
{
42+
if ([] !== $options) {
43+
throw new InvalidArgumentException('No supported options.');
44+
}
45+
46+
$this->request('POST', 'vectorize/v2/indexes', [
47+
'config' => [
48+
'dimensions' => $this->dimensions,
49+
'metric' => $this->metric,
50+
],
51+
'name' => $this->index,
52+
]);
53+
}
54+
55+
public function drop(): void
56+
{
57+
$this->request('DELETE', \sprintf('vectorize/v2/indexes/%s', $this->index));
58+
}
59+
60+
public function add(VectorDocument ...$documents): void
61+
{
62+
$payload = array_map(
63+
$this->convertToIndexableArray(...),
64+
$documents,
65+
);
66+
67+
$this->request('POST', \sprintf('vectorize/v2/indexes/%s/upsert', $this->index), function () use ($payload) {
68+
foreach ($payload as $entry) {
69+
yield json_encode($entry).\PHP_EOL;
70+
}
71+
});
72+
}
73+
74+
public function query(Vector $vector, array $options = []): array
75+
{
76+
$results = $this->request('POST', \sprintf('vectorize/v2/indexes/%s/query', $this->index), [
77+
'vector' => $vector->getData(),
78+
'returnValues' => true,
79+
'returnMetadata' => 'all',
80+
]);
81+
82+
return array_map($this->convertToVectorDocument(...), $results['result']['matches']);
83+
}
84+
85+
/**
86+
* @param array<string, mixed> $payload
87+
*
88+
* @return array<string, mixed>
89+
*/
90+
private function request(string $method, string $endpoint, \Closure|array $payload = []): array
91+
{
92+
$url = \sprintf('%s/%s/%s', $this->endpointUrl, $this->accountId, $endpoint);
93+
94+
$options = [
95+
'auth_bearer' => $this->apiKey,
96+
];
97+
98+
if ($payload instanceof \Closure) {
99+
$options['headers'] = [
100+
'Content-Type' => 'application/x-ndjson',
101+
];
102+
103+
$options['body'] = $payload();
104+
}
105+
106+
if (\is_array($payload)) {
107+
$options['json'] = $payload;
108+
}
109+
110+
$response = $this->httpClient->request($method, $url, $options);
111+
112+
return $response->toArray();
113+
}
114+
115+
/**
116+
* @return array<string, mixed>
117+
*/
118+
private function convertToIndexableArray(VectorDocument $document): array
119+
{
120+
return [
121+
'id' => $document->id->toRfc4122(),
122+
'values' => $document->vector->getData(),
123+
'metadata' => $document->metadata->getArrayCopy(),
124+
];
125+
}
126+
127+
/**
128+
* @param array<string, mixed> $data
129+
*/
130+
private function convertToVectorDocument(array $data): VectorDocument
131+
{
132+
$id = $data['id'] ?? throw new InvalidArgumentException('Missing "id" field in the document data.');
133+
134+
$vector = !\array_key_exists('values', $data) || null === $data['values']
135+
? new NullVector()
136+
: new Vector($data['values']);
137+
138+
return new VectorDocument(
139+
id: Uuid::fromString($id),
140+
vector: $vector,
141+
metadata: new Metadata($data['metadata']),
142+
score: $data['score'] ?? null
143+
);
144+
}
145+
}

0 commit comments

Comments
 (0)