Skip to content
Open
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
9 changes: 7 additions & 2 deletions src/chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ CHANGELOG
0.1
---

* Introduce the component
* Add support for external message stores:
* Introduce the component
* Add support for external message stores:
- Symfony Cache
- Cloudflare
- Doctrine
Expand All @@ -14,3 +14,8 @@ CHANGELOG
- Pogocache
- Redis
- SurrealDb
* Add streaming support to `ChatInterface::submit()`
- Add `StreamableStoreInterface` which indicates `StoreInterface` implementation can be configured with streaming
- Add `AccumulatingStreamResult` wrapper class which adds accumulation logic & callback chaining to `StreamResult` implementations (can wrap both `Agent` and `Platform` variants) to return the full message once `Generator` is exhausted
- Streamed responses now also create `AssistantMessage` & are added to `Store` in `Chat::submit()`
- Bugfixed loss of metadata in `Chat::submit()`
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/Doctrine/DoctrineDbalMessageStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
Expand All @@ -34,7 +35,7 @@
/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class DoctrineDbalMessageStore implements ManagedStoreInterface, MessageStoreInterface
final class DoctrineDbalMessageStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
public function __construct(
private readonly string $tableName,
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/Local/CacheStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
use Symfony\AI\Agent\Exception\RuntimeException;
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;

/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class CacheStore implements ManagedStoreInterface, MessageStoreInterface
final class CacheStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
public function __construct(
private readonly CacheItemPoolInterface $cache,
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/Local/InMemoryStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@

use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;

/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class InMemoryStore implements ManagedStoreInterface, MessageStoreInterface
final class InMemoryStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
/**
* @var MessageBag[]
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/Meilisearch/MessageStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Clock\ClockInterface;
Expand All @@ -31,7 +32,7 @@
/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient,
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/MongoDb/MessageStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
Expand All @@ -27,7 +28,7 @@
/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
public function __construct(
private readonly Client $client,
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/Pogocache/MessageStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
Expand All @@ -28,7 +29,7 @@
/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient,
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/Redis/MessageStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
Expand All @@ -24,7 +25,7 @@
/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
public function __construct(
private readonly \Redis $redis,
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/Bridge/SurrealDb/MessageStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Chat\StreamableStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
Expand All @@ -29,7 +30,7 @@
/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface, StreamableStoreInterface
{
private string $authenticationToken = '';

Expand Down
18 changes: 16 additions & 2 deletions src/chat/src/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
namespace Symfony\AI\Chat;

use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Agent\Exception\RuntimeException;
use Symfony\AI\Agent\Toolbox\StreamResult as ToolboxStreamResult;
use Symfony\AI\Chat\Result\AccumulatingStreamResult;
use Symfony\AI\Platform\Message\AssistantMessage;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\UserMessage;
use Symfony\AI\Platform\Result\StreamResult;
use Symfony\AI\Platform\Result\TextResult;

/**
Expand All @@ -35,18 +39,28 @@ public function initiate(MessageBag $messages): void
$this->store->save($messages);
}

public function submit(UserMessage $message): AssistantMessage
public function submit(UserMessage $message): AssistantMessage|AccumulatingStreamResult
{
$messages = $this->store->load();

$messages->add($message);
$result = $this->agent->call($messages);

if ($result instanceof StreamResult || $result instanceof ToolboxStreamResult) {
if (!$this->store instanceof StreamableStoreInterface) {
throw new RuntimeException($this->store::class.' does not support streaming.');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we should throw an exception here, maybe just returning the complete stream at once?

Or maybe we can just store the message "as it" in the store?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly certain throwing is the right call here. Returning the stream "as is" would work, but it's likely to confuse developers and hide the fact that streaming and session storage are incompatible by nature. As a developer, I'd rather get an early exception that forces me to fix a faulty configuration than a silently degraded solution. I think it'll also prevent a lot of GitHub issues down the road.


return new AccumulatingStreamResult($result, fn (AssistantMessage $assistantMessage) => $this->store->save($messages->with($assistantMessage)));
}

\assert($result instanceof TextResult);

$assistantMessage = Message::ofAssistant($result->getContent());
$messages->add($assistantMessage);

$assistantMessage->getMetadata()->set($result->getMetadata()->all());

$messages->add($assistantMessage);
$this->store->save($messages);

return $assistantMessage;
Expand Down
3 changes: 2 additions & 1 deletion src/chat/src/ChatInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\AI\Chat;

use Symfony\AI\Agent\Exception\ExceptionInterface;
use Symfony\AI\Chat\Result\AccumulatingStreamResult;
use Symfony\AI\Platform\Message\AssistantMessage;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\UserMessage;
Expand All @@ -26,5 +27,5 @@ public function initiate(MessageBag $messages): void;
/**
* @throws ExceptionInterface When the chat submission fails due to agent errors
*/
public function submit(UserMessage $message): AssistantMessage;
public function submit(UserMessage $message): AssistantMessage|AccumulatingStreamResult;
}
65 changes: 65 additions & 0 deletions src/chat/src/Result/AccumulatingStreamResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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\Chat\Result;

use Symfony\AI\Agent\Toolbox\StreamResult as ToolboxStreamResult;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Metadata\Metadata;
use Symfony\AI\Platform\Result\StreamResult;
use Symfony\AI\Platform\Result\ToolCallResult;

/**
* @author Marco van Angeren <marco@jouwweb.nl>
*/
final class AccumulatingStreamResult
{
public function __construct(
private readonly StreamResult|ToolboxStreamResult $innerResult,
private ?\Closure $onComplete = null,
) {
}

public function getContent(): \Generator
{
$accumulatedContent = '';
$toolCalls = [];

try {
foreach ($this->innerResult->getContent() as $value) {
if ($value instanceof ToolCallResult) {
array_push($toolCalls, ...$value->getContent());
yield $value;
continue;
}

$accumulatedContent .= $value;
yield $value;
}
} finally {
if (null !== $this->onComplete) {
$assistantMessage = Message::ofAssistant(
'' === $accumulatedContent ? null : $accumulatedContent,
$toolCalls ?: null
);

$assistantMessage->getMetadata()->set($this->innerResult->getMetadata()->all());

($this->onComplete)($assistantMessage);
}
}
}

public function getMetadata(): Metadata
{
return $this->innerResult->getMetadata();
}
}
19 changes: 19 additions & 0 deletions src/chat/src/StreamableStoreInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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\Chat;

/**
* @author Marco van Angeren <marco@jouwweb.nl>
*/
interface StreamableStoreInterface
{
}
Loading
Loading