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
4 changes: 4 additions & 0 deletions examples/.env
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ MILVUS_HOST=http://127.0.0.1:19530
MILVUS_API_KEY=root:Milvus
MILVUS_DATABASE=symfony

# Cloudflare (store)
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_KEY=

# Cerebras
CEREBRAS_API_KEY=

Expand Down
71 changes: 71 additions & 0 deletions examples/rag/cloudflare.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\Toolbox\AgentProcessor;
use Symfony\AI\Agent\Toolbox\Tool\SimilaritySearch;
use Symfony\AI\Agent\Toolbox\Toolbox;
use Symfony\AI\Fixtures\Movies;
use Symfony\AI\Platform\Bridge\OpenAi\Embeddings;
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Store\Bridge\Cloudflare\Store;
use Symfony\AI\Store\Document\Metadata;
use Symfony\AI\Store\Document\TextDocument;
use Symfony\AI\Store\Document\Vectorizer;
use Symfony\AI\Store\Indexer;
use Symfony\Component\Uid\Uuid;

require_once dirname(__DIR__).'/bootstrap.php';

// initialize the store
$store = new Store(
httpClient: http_client(),
accountId: env('CLOUDFLARE_ACCOUNT_ID'),
apiKey: env('CLOUDFLARE_API_KEY'),
index: 'movies',
);

// initialize the index
$store->setup();

// create embeddings and documents
$documents = [];
foreach (Movies::all() as $i => $movie) {
$documents[] = new TextDocument(
id: Uuid::v4(),
content: 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description'],
metadata: new Metadata($movie),
);
}

// create embeddings for documents (keep in mind that upserting vectors is asynchronous)
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
$vectorizer = new Vectorizer($platform, $embeddings = new Embeddings());
$indexer = new Indexer($vectorizer, $store, logger());
$indexer->index($documents);

$model = new Gpt(Gpt::GPT_4O_MINI);

$similaritySearch = new SimilaritySearch($platform, $embeddings, $store);
$toolbox = new Toolbox([$similaritySearch], logger: logger());
$processor = new AgentProcessor($toolbox);
$agent = new Agent($platform, $model, [$processor], [$processor], logger());

$messages = new MessageBag(
Message::forSystem('Please answer all user questions only using SimilaritySearch function.'),
Message::ofUser('Which movie fits the theme of technology?')
);
$result = $agent->call($messages);

echo $result->getContent().\PHP_EOL;
13 changes: 13 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,19 @@
->end()
->end()
->end()
->arrayNode('cloudflare')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('account_id')->cannotBeEmpty()->end()
->scalarNode('api_key')->cannotBeEmpty()->end()
->scalarNode('index_name')->cannotBeEmpty()->end()
->integerNode('dimensions')->end()
->scalarNode('metric')->end()
->scalarNode('endpoint_url')->end()
->end()
->end()
->end()
->arrayNode('meilisearch')
->useAttributeAsKey('name')
->arrayPrototype()
Expand Down
31 changes: 31 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureSearchStore;
use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore;
use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore;
use Symfony\AI\Store\Bridge\Cloudflare\Store as CloudflareStore;
use Symfony\AI\Store\Bridge\Local\CacheStore;
use Symfony\AI\Store\Bridge\Local\DistanceCalculator;
use Symfony\AI\Store\Bridge\Local\DistanceStrategy;
Expand Down Expand Up @@ -685,6 +686,36 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde
}
}

if ('cloudflare' === $type) {
foreach ($stores as $name => $store) {
$arguments = [
new Reference('http_client'),
$store['account_id'],
$store['api_key'],
$store['index_name'],
];

if (\array_key_exists('dimensions', $store)) {
$arguments[4] = $store['dimensions'];
}

if (\array_key_exists('metric', $store)) {
$arguments[5] = $store['metric'];
}

if (\array_key_exists('endpoint', $store)) {
$arguments[6] = $store['endpoint'];
}

$definition = new Definition(CloudflareStore::class);
$definition
->addTag('ai.store')
->setArguments($arguments);

$container->setDefinition('ai.store.'.$type.'.'.$name, $definition);
}
}

if ('meilisearch' === $type) {
foreach ($stores as $name => $store) {
$arguments = [
Expand Down
10 changes: 10 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,16 @@ private function getFullConfig(): array
'table' => 'my_table',
],
],
'cloudflare' => [
'my_cloudflare_store' => [
'account_id' => 'foo',
'api_key' => 'bar',
'index_name' => 'random',
'dimensions' => 1536,
'metric' => 'cosine',
'endpoint_url' => 'https://api.cloudflare.com/client/v5/accounts',
],
],
'meilisearch' => [
'my_meilisearch_store' => [
'endpoint' => 'http://127.0.0.1:7700',
Expand Down
1 change: 1 addition & 0 deletions src/store/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ CHANGELOG
- Azure AI Search
- ChromaDB
- ClickHouse
- Cloudflare
- MariaDB
- Meilisearch
- MongoDB
Expand Down
1 change: 1 addition & 0 deletions src/store/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"azure",
"chromadb",
"clickhouse",
"cloudflare",
"mariadb",
"meilisearch",
"milvus",
Expand Down
4 changes: 4 additions & 0 deletions src/store/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ used vector store::

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

* `Similarity Search with Cloudflare (RAG)`_
* `Similarity Search with MariaDB (RAG)`_
* `Similarity Search with Meilisearch (RAG)`_
* `Similarity Search with memory storage (RAG)`_
Expand All @@ -61,6 +62,7 @@ Supported Stores

* `Azure AI Search`_
* `Chroma`_ (requires `codewithkyrian/chromadb-php` as additional dependency)
* `Cloudflare`_
* `InMemory`_
* `MariaDB`_ (requires `ext-pdo`)
* `Meilisearch`_
Expand Down Expand Up @@ -104,6 +106,7 @@ This leads to a store implementing two methods::
}

.. _`Retrieval Augmented Generation`: https://de.wikipedia.org/wiki/Retrieval-Augmented_Generation
.. _`Similarity Search with Cloudflare (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/cloudflare.php
.. _`Similarity Search with MariaDB (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/mariadb-gemini.php
.. _`Similarity Search with Meilisearch (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/meilisearch.php
.. _`Similarity Search with memory storage (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/in-memory.php
Expand All @@ -118,6 +121,7 @@ This leads to a store implementing two methods::
.. _`Similarity Search with Weaviate (RAG)`: https://github.com/symfony/ai/blob/main/examples/rag/weaviate.php
.. _`Azure AI Search`: https://azure.microsoft.com/products/ai-services/ai-search
.. _`Chroma`: https://www.trychroma.com/
.. _`Cloudflare`: https://developers.cloudflare.com/vectorize/
.. _`MariaDB`: https://mariadb.org/projects/mariadb-vector/
.. _`Pinecone`: https://www.pinecone.io/
.. _`Postgres`: https://www.postgresql.org/about/news/pgvector-070-released-2852/
Expand Down
145 changes: 145 additions & 0 deletions src/store/src/Bridge/Cloudflare/Store.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Store\Bridge\Cloudflare;

use Symfony\AI\Platform\Vector\NullVector;
use Symfony\AI\Platform\Vector\Vector;
use Symfony\AI\Store\Document\Metadata;
use Symfony\AI\Store\Document\VectorDocument;
use Symfony\AI\Store\Exception\InvalidArgumentException;
use Symfony\AI\Store\ManagedStoreInterface;
use Symfony\AI\Store\StoreInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final readonly class Store implements ManagedStoreInterface, StoreInterface
{
public function __construct(
private HttpClientInterface $httpClient,
private string $accountId,
#[\SensitiveParameter] private string $apiKey,
private string $index,
private int $dimensions = 1536,
private string $metric = 'cosine',
private string $endpointUrl = 'https://api.cloudflare.com/client/v4/accounts',
) {
}

public function setup(array $options = []): void
{
if ([] !== $options) {
throw new InvalidArgumentException('No supported options.');
}

$this->request('POST', 'vectorize/v2/indexes', [
'config' => [
'dimensions' => $this->dimensions,
'metric' => $this->metric,
],
'name' => $this->index,
]);
}

public function drop(): void
{
$this->request('DELETE', \sprintf('vectorize/v2/indexes/%s', $this->index));
}

public function add(VectorDocument ...$documents): void
{
$payload = array_map(
$this->convertToIndexableArray(...),
$documents,
);

$this->request('POST', \sprintf('vectorize/v2/indexes/%s/upsert', $this->index), function () use ($payload) {
foreach ($payload as $entry) {
yield json_encode($entry).\PHP_EOL;
}
});
}

public function query(Vector $vector, array $options = []): array
{
$results = $this->request('POST', \sprintf('vectorize/v2/indexes/%s/query', $this->index), [
'vector' => $vector->getData(),
'returnValues' => true,
'returnMetadata' => 'all',
]);

return array_map($this->convertToVectorDocument(...), $results['result']['matches']);
}

/**
* @param array<string, mixed> $payload
*
* @return array<string, mixed>
*/
private function request(string $method, string $endpoint, \Closure|array $payload = []): array
{
$url = \sprintf('%s/%s/%s', $this->endpointUrl, $this->accountId, $endpoint);

$options = [
'auth_bearer' => $this->apiKey,
];

if ($payload instanceof \Closure) {
$options['headers'] = [
'Content-Type' => 'application/x-ndjson',
];

$options['body'] = $payload();
}

if (\is_array($payload)) {
$options['json'] = $payload;
}

$response = $this->httpClient->request($method, $url, $options);

return $response->toArray();
}

/**
* @return array<string, mixed>
*/
private function convertToIndexableArray(VectorDocument $document): array
{
return [
'id' => $document->id->toRfc4122(),
'values' => $document->vector->getData(),
'metadata' => $document->metadata->getArrayCopy(),
];
}

/**
* @param array<string, mixed> $data
*/
private function convertToVectorDocument(array $data): VectorDocument
{
$id = $data['id'] ?? throw new InvalidArgumentException('Missing "id" field in the document data.');

$vector = !\array_key_exists('values', $data) || null === $data['values']
? new NullVector()
: new Vector($data['values']);

return new VectorDocument(
id: Uuid::fromString($id),
vector: $vector,
metadata: new Metadata($data['metadata']),
score: $data['score'] ?? null
);
}
}
Loading