Skip to content

Commit 1e69969

Browse files
committed
bug #868 [Agent] Fix sources metadata unavailable during streaming (OskarStark)
This PR was merged into the main branch. Discussion ---------- [Agent] Fix sources metadata unavailable during streaming | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Docs? | no | Issues | Fix #833 | License | MIT Replaces * #838 Commits ------- aeb6e22 [Agent] Fix sources metadata unavailable during streaming
2 parents c840dad + aeb6e22 commit 1e69969

File tree

2 files changed

+65
-1
lines changed

2 files changed

+65
-1
lines changed

src/agent/src/Toolbox/StreamResult.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,22 @@ public function getContent(): \Generator
3131
$streamedResult = '';
3232
foreach ($this->generator as $value) {
3333
if ($value instanceof ToolCallResult) {
34-
yield from ($this->handleToolCallsCallback)($value, Message::ofAssistant($streamedResult))->getContent();
34+
$innerResult = ($this->handleToolCallsCallback)($value, Message::ofAssistant($streamedResult));
35+
36+
// Propagate metadata from inner result to this result
37+
foreach ($innerResult->getMetadata()->all() as $key => $metadataValue) {
38+
$this->getMetadata()->add($key, $metadataValue);
39+
}
40+
41+
$content = $innerResult->getContent();
42+
// Strings are iterable in PHP but yield from would iterate character-by-character.
43+
// We need to yield the complete string as a single value to preserve streaming behavior.
44+
// null should also be yielded as-is.
45+
if (\is_string($content) || null === $content || !is_iterable($content)) {
46+
yield $content;
47+
} else {
48+
yield from $content;
49+
}
3550

3651
break;
3752
}

src/agent/tests/Toolbox/AgentProcessorTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\AI\Platform\PlatformInterface;
2727
use Symfony\AI\Platform\Result\DeferredResult;
2828
use Symfony\AI\Platform\Result\InMemoryRawResult;
29+
use Symfony\AI\Platform\Result\StreamResult as GenericStreamResult;
2930
use Symfony\AI\Platform\Result\TextResult;
3031
use Symfony\AI\Platform\Result\ToolCall;
3132
use Symfony\AI\Platform\Result\ToolCallResult;
@@ -233,4 +234,52 @@ public function testSourcesGetCollectedAcrossConsecutiveToolCalls()
233234
$this->assertCount(2, $metadata->get('sources'));
234235
$this->assertSame([$source1, $source2], $metadata->get('sources'));
235236
}
237+
238+
public function testSourcesEndUpInResultMetadataWithStreaming()
239+
{
240+
$toolCall = new ToolCall('call_1234', 'tool_sources', ['arg1' => 'value1']);
241+
$source1 = new Source('Relevant Article 1', 'http://example.com/article1', 'Content of article about the topic');
242+
$source2 = new Source('Relevant Article 2', 'http://example.com/article2', 'More content of article about the topic');
243+
$toolbox = $this->createMock(ToolboxInterface::class);
244+
$toolbox
245+
->expects($this->once())
246+
->method('execute')
247+
->willReturn(new ToolResult($toolCall, 'Response based on the two articles.', [$source1, $source2]));
248+
249+
$messageBag = new MessageBag();
250+
251+
// Create a generator that yields chunks and then a ToolCallResult
252+
$generator = (function () use ($toolCall) {
253+
yield 'chunk1';
254+
yield 'chunk2';
255+
yield new ToolCallResult($toolCall);
256+
})();
257+
258+
$result = new GenericStreamResult($generator);
259+
260+
$agent = $this->createMock(AgentInterface::class);
261+
$agent
262+
->expects($this->once())
263+
->method('call')
264+
->willReturn(new TextResult('Final response based on the two articles.'));
265+
266+
$processor = new AgentProcessor($toolbox, includeSources: true);
267+
$processor->setAgent($agent);
268+
269+
$output = new Output('gpt-4', $result, $messageBag);
270+
271+
$processor->processOutput($output);
272+
273+
// Consume the stream
274+
$content = '';
275+
foreach ($output->getResult()->getContent() as $chunk) {
276+
$content .= $chunk;
277+
}
278+
279+
// After consuming the stream, metadata should be available
280+
$metadata = $output->getResult()->getMetadata();
281+
$this->assertTrue($metadata->has('sources'));
282+
$this->assertCount(2, $metadata->get('sources'));
283+
$this->assertSame([$source1, $source2], $metadata->get('sources'));
284+
}
236285
}

0 commit comments

Comments
 (0)