Skip to content

Commit b24a508

Browse files
committed
feature #416 [Platform] Introduce CachedPlatform (Guikingone)
This PR was merged into the main branch. Discussion ---------- [Platform] Introduce `CachedPlatform` | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | Somehow related to #337 | License | MIT Hi 👋🏻 This PR aim to introduce a caching layer for `Ollama` platform (like OpenAI, Anthropic and more already does). Commits ------- c66e733 feat(platform): Ollama prompt cache
2 parents 4499146 + c66e733 commit b24a508

File tree

10 files changed

+336
-0
lines changed

10 files changed

+336
-0
lines changed

docs/bundles/ai-bundle.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,28 @@ Advanced Example with Multiple Agents
136136
vectorizer: 'ai.vectorizer.mistral_embeddings'
137137
store: 'ai.store.memory.research'
138138
139+
Cached Platform
140+
---------------
141+
142+
Thanks to Symfony's Cache component, platforms can be decorated and use any cache adapter,
143+
this platform allows to reduce network calls / resource consumption:
144+
145+
.. code-block:: yaml
146+
147+
# config/packages/ai.yaml
148+
ai:
149+
platform:
150+
openai:
151+
api_key: '%env(OPENAI_API_KEY)%'
152+
cache:
153+
platform: 'ai.platform.openai'
154+
service: 'cache.app'
155+
156+
agent:
157+
openai:
158+
platform: 'ai.platform.cache.openai'
159+
model: 'gpt-4o-mini'
160+
139161
Store Dependency Injection
140162
--------------------------
141163

docs/components/platform.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,29 @@ which can be useful to speed up the processing::
406406
echo $result->asText().PHP_EOL;
407407
}
408408

409+
Cached Platform Calls
410+
---------------------
411+
412+
Thanks to Symfony's Cache component, platform calls can be cached to reduce calls and resources consumption::
413+
414+
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
415+
use Symfony\AI\Platform\CachedPlatform;
416+
use Symfony\AI\Platform\Message\Message;
417+
use Symfony\AI\Platform\Message\MessageBag;
418+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
419+
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
420+
421+
$platform = PlatformFactory::create($apiKey, eventDispatcher: $dispatcher);
422+
$cachedPlatform = new CachedPlatform($platform, new TagAwareAdapter(new ArrayAdapter());
423+
424+
$firstResult = $cachedPlatform->invoke('gpt-4o-mini', new MessageBag(Message::ofUser('What is the capital of France?')));
425+
426+
echo $firstResult->getContent().\PHP_EOL;
427+
428+
$secondResult = $cachedPlatform->invoke('gpt-4o-mini', new MessageBag(Message::ofUser('What is the capital of France?')));
429+
430+
echo $secondResult->getContent().\PHP_EOL;
431+
409432
Testing Tools
410433
-------------
411434

examples/misc/agent-with-cache.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Platform\Bridge\Ollama\PlatformFactory;
14+
use Symfony\AI\Platform\CachedPlatform;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
18+
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
19+
20+
require_once dirname(__DIR__).'/bootstrap.php';
21+
22+
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client());
23+
$cachedPlatform = new CachedPlatform($platform, new TagAwareAdapter(new ArrayAdapter()));
24+
25+
$agent = new Agent($cachedPlatform, 'qwen3:0.6b-q4_K_M');
26+
$messages = new MessageBag(
27+
Message::forSystem('You are a helpful assistant.'),
28+
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
29+
);
30+
$result = $agent->call($messages, [
31+
'prompt_cache_key' => 'chat',
32+
]);
33+
34+
assert($result->getMetadata()->has('cached'));
35+
36+
echo $result->getContent().\PHP_EOL;
37+
38+
// Thanks to the cache adapter and the "prompt_cache_key" key, this call will not trigger any network call
39+
40+
$secondResult = $agent->call($messages, [
41+
'prompt_cache_key' => 'chat',
42+
]);
43+
44+
assert($secondResult->getMetadata()->has('cached'));
45+
46+
echo $secondResult->getContent().\PHP_EOL;

src/ai-bundle/config/options.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@
6565
->end()
6666
->end()
6767
->end()
68+
->arrayNode('cache')
69+
->useAttributeAsKey('name')
70+
->arrayPrototype()
71+
->children()
72+
->stringNode('platform')->isRequired()->end()
73+
->stringNode('service')
74+
->isRequired()
75+
->info('The cache service id as defined under the "cache" configuration key')
76+
->end()
77+
->stringNode('cache_key')
78+
->info('Key used to store platform results, if not set, the current platform name will be used, the "prompt_cache_key" can be set during platform call to override this value')
79+
->end()
80+
->end()
81+
->end()
82+
->end()
6883
->arrayNode('cartesia')
6984
->children()
7085
->stringNode('api_key')->isRequired()->end()

src/ai-bundle/src/AiBundle.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
use Symfony\AI\Platform\Bridge\Scaleway\PlatformFactory as ScalewayPlatformFactory;
6868
use Symfony\AI\Platform\Bridge\VertexAi\PlatformFactory as VertexAiPlatformFactory;
6969
use Symfony\AI\Platform\Bridge\Voyage\PlatformFactory as VoyagePlatformFactory;
70+
use Symfony\AI\Platform\CachedPlatform;
7071
use Symfony\AI\Platform\Capability;
7172
use Symfony\AI\Platform\Exception\RuntimeException;
7273
use Symfony\AI\Platform\Message\Content\File;
@@ -374,6 +375,24 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
374375
return;
375376
}
376377

378+
if ('cache' === $type) {
379+
foreach ($platform as $name => $cachedPlatformConfig) {
380+
$definition = (new Definition(CachedPlatform::class))
381+
->setLazy(true)
382+
->setArguments([
383+
new Reference($cachedPlatformConfig['platform']),
384+
new Reference($cachedPlatformConfig['service'], ContainerInterface::NULL_ON_INVALID_REFERENCE),
385+
$cachedPlatformConfig['cache_key'] ?? $name,
386+
])
387+
->addTag('proxy', ['interface' => PlatformInterface::class])
388+
->addTag('ai.platform', ['name' => 'cache'.$name]);
389+
390+
$container->setDefinition('ai.platform.cache.'.$name, $definition);
391+
}
392+
393+
return;
394+
}
395+
377396
if ('cartesia' === $type) {
378397
$definition = (new Definition(Platform::class))
379398
->setFactory(CartesiaPlatformFactory::class.'::create')

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2890,6 +2890,77 @@ public function testVectorizerModelBooleanOptionsArePreserved()
28902890
$this->assertSame('text-embedding-3-small?normalize=false&cache=true&nested%5Bbool%5D=false', $vectorizerDefinition->getArgument(1));
28912891
}
28922892

2893+
public function testCachedPlatformCanBeUsed()
2894+
{
2895+
$container = $this->buildContainer([
2896+
'ai' => [
2897+
'platform' => [
2898+
'ollama' => [
2899+
'host_url' => 'http://127.0.0.1:11434',
2900+
],
2901+
'cache' => [
2902+
'ollama' => [
2903+
'platform' => 'ai.platform.ollama',
2904+
'service' => 'cache.app',
2905+
'cache_key' => 'ollama',
2906+
],
2907+
],
2908+
],
2909+
],
2910+
]);
2911+
2912+
$this->assertTrue($container->hasDefinition('ai.platform.cache.ollama'));
2913+
2914+
$definition = $container->getDefinition('ai.platform.cache.ollama');
2915+
$this->assertTrue($definition->isLazy());
2916+
$this->assertCount(3, $definition->getArguments());
2917+
2918+
$this->assertInstanceOf(Reference::class, $definition->getArgument(0));
2919+
$platformArgument = $definition->getArgument(0);
2920+
$this->assertSame('ai.platform.ollama', (string) $platformArgument);
2921+
2922+
$this->assertInstanceOf(Reference::class, $definition->getArgument(1));
2923+
$cacheArgument = $definition->getArgument(1);
2924+
$this->assertSame('cache.app', (string) $cacheArgument);
2925+
2926+
$this->assertSame('ollama', $definition->getArgument(2));
2927+
}
2928+
2929+
public function testCachedPlatformCanBeUsedWithoutCustomCacheKey()
2930+
{
2931+
$container = $this->buildContainer([
2932+
'ai' => [
2933+
'platform' => [
2934+
'ollama' => [
2935+
'host_url' => 'http://127.0.0.1:11434',
2936+
],
2937+
'cache' => [
2938+
'ollama' => [
2939+
'platform' => 'ai.platform.ollama',
2940+
'service' => 'cache.app',
2941+
],
2942+
],
2943+
],
2944+
],
2945+
]);
2946+
2947+
$this->assertTrue($container->hasDefinition('ai.platform.cache.ollama'));
2948+
2949+
$definition = $container->getDefinition('ai.platform.cache.ollama');
2950+
$this->assertTrue($definition->isLazy());
2951+
$this->assertCount(3, $definition->getArguments());
2952+
2953+
$this->assertInstanceOf(Reference::class, $definition->getArgument(0));
2954+
$platformArgument = $definition->getArgument(0);
2955+
$this->assertSame('ai.platform.ollama', (string) $platformArgument);
2956+
2957+
$this->assertInstanceOf(Reference::class, $definition->getArgument(1));
2958+
$cacheArgument = $definition->getArgument(1);
2959+
$this->assertSame('cache.app', (string) $cacheArgument);
2960+
2961+
$this->assertSame('ollama', $definition->getArgument(2));
2962+
}
2963+
28932964
public function testCacheMessageStoreCanBeConfiguredWithCustomKey()
28942965
{
28952966
$container = $this->buildContainer([
@@ -3559,6 +3630,13 @@ private function getFullConfig(): array
35593630
'api_version' => '2024-02-15-preview',
35603631
],
35613632
],
3633+
'cache' => [
3634+
'azure' => [
3635+
'platform' => 'ai.platform.azure.my_azure_instance',
3636+
'service' => 'cache.app',
3637+
'cache_key' => 'foo',
3638+
],
3639+
],
35623640
'cartesia' => [
35633641
'api_key' => 'cartesia_key_full',
35643642
'version' => '2025-04-16',

src/platform/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"phpstan/phpstan-symfony": "^2.0.6",
6767
"phpunit/phpunit": "^11.5",
6868
"symfony/ai-agent": "@dev",
69+
"symfony/cache": "^7.3|^8.0",
6970
"symfony/console": "^7.3|^8.0",
7071
"symfony/dotenv": "^7.3|^8.0",
7172
"symfony/finder": "^7.3|^8.0",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\Platform;
13+
14+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
15+
use Symfony\AI\Platform\Result\DeferredResult;
16+
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
17+
use Symfony\Contracts\Cache\CacheInterface;
18+
use Symfony\Contracts\Cache\ItemInterface;
19+
20+
/**
21+
* @author Guillaume Loulier <[email protected]>
22+
*/
23+
final class CachedPlatform implements PlatformInterface
24+
{
25+
public function __construct(
26+
private readonly PlatformInterface $platform,
27+
private readonly (CacheInterface&TagAwareAdapterInterface)|null $cache = null,
28+
private readonly ?string $cacheKey = null,
29+
) {
30+
}
31+
32+
public function invoke(string $model, array|string|object $input, array $options = []): DeferredResult
33+
{
34+
$invokeCall = fn (string $model, array|string|object $input, array $options = []): DeferredResult => $this->platform->invoke($model, $input, $options);
35+
36+
if ($this->cache instanceof CacheInterface && (\array_key_exists('prompt_cache_key', $options) && '' !== $options['prompt_cache_key'])) {
37+
$cacheKey = \sprintf('%s_%s_%s', $this->cacheKey ?? $options['prompt_cache_key'], md5($model), \is_string($input) ? md5($input) : md5(json_encode($input)));
38+
39+
unset($options['prompt_cache_key']);
40+
41+
return $this->cache->get($cacheKey, static function (ItemInterface $item) use ($invokeCall, $model, $input, $options, $cacheKey): DeferredResult {
42+
$item->tag($model);
43+
44+
$result = $invokeCall($model, $input, $options);
45+
46+
$result = new DeferredResult(
47+
$result->getResultConverter(),
48+
$result->getRawResult(),
49+
$options,
50+
);
51+
52+
$result->getMetadata()->set([
53+
'cached' => true,
54+
'cache_key' => $cacheKey,
55+
'cached_at' => (new \DateTimeImmutable())->getTimestamp(),
56+
]);
57+
58+
return $result;
59+
});
60+
}
61+
62+
return $invokeCall($model, $input, $options);
63+
}
64+
65+
public function getModelCatalog(): ModelCatalogInterface
66+
{
67+
return $this->platform->getModelCatalog();
68+
}
69+
}

src/platform/src/Result/DeferredResult.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\AI\Platform\Exception\ExceptionInterface;
1515
use Symfony\AI\Platform\Exception\UnexpectedResultTypeException;
16+
use Symfony\AI\Platform\Metadata\MetadataAwareTrait;
1617
use Symfony\AI\Platform\ResultConverterInterface;
1718
use Symfony\AI\Platform\Vector\Vector;
1819

@@ -21,6 +22,8 @@
2122
*/
2223
final class DeferredResult
2324
{
25+
use MetadataAwareTrait;
26+
2427
private bool $isConverted = false;
2528
private ResultInterface $convertedResult;
2629

@@ -50,6 +53,8 @@ public function getResult(): ResultInterface
5053
$this->isConverted = true;
5154
}
5255

56+
$this->convertedResult->getMetadata()->set($this->getMetadata()->all());
57+
5358
return $this->convertedResult;
5459
}
5560

0 commit comments

Comments
 (0)