From 9072e72ca14fe6cdf6761ed72deda826da924bd4 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Fri, 23 Jan 2026 18:50:19 +0100 Subject: [PATCH] Fix Metadata handling on Perplexity streams --- examples/perplexity/academic-search.php | 4 +- examples/perplexity/bootstrap.php | 24 ++--- examples/perplexity/image-input-url.php | 4 +- examples/perplexity/pdf-input-url.php | 4 +- examples/perplexity/stream.php | 4 +- examples/perplexity/web-search.php | 4 +- .../src/Bridge/Perplexity/ResultConverter.php | 24 ++--- .../src/Bridge/Perplexity/StreamListener.php | 40 +++++++++ .../Perplexity/Tests/StreamListenerTest.php | 88 +++++++++++++++++++ 9 files changed, 154 insertions(+), 42 deletions(-) create mode 100644 src/platform/src/Bridge/Perplexity/StreamListener.php create mode 100644 src/platform/src/Bridge/Perplexity/Tests/StreamListenerTest.php diff --git a/examples/perplexity/academic-search.php b/examples/perplexity/academic-search.php index 421fb895..f8ddb903 100644 --- a/examples/perplexity/academic-search.php +++ b/examples/perplexity/academic-search.php @@ -27,5 +27,5 @@ $result = $platform->invoke('sonar', $messages, [ echo $result->asText().\PHP_EOL; echo \PHP_EOL; -print_search_results($result->getMetadata()); -print_citations($result->getMetadata()); +print_search_results($result->getMetadata()->get('search_results')); +print_citations($result->getMetadata()->get('citations')); diff --git a/examples/perplexity/bootstrap.php b/examples/perplexity/bootstrap.php index b1b548a6..d76b6ccf 100644 --- a/examples/perplexity/bootstrap.php +++ b/examples/perplexity/bootstrap.php @@ -9,18 +9,13 @@ * file that was distributed with this source code. */ -use Symfony\AI\Platform\Metadata\Metadata; - require_once dirname(__DIR__).'/bootstrap.php'; -function print_search_results(Metadata $metadata): void +/** + * @param array> $searchResults + */ +function print_search_results(array $searchResults): void { - $searchResults = $metadata->get('search_results'); - - if (null === $searchResults) { - return; - } - echo 'Search results:'.\PHP_EOL; if (0 === count($searchResults)) { @@ -40,14 +35,11 @@ function print_search_results(Metadata $metadata): void } } -function print_citations(Metadata $metadata): void +/** + * @param array $citations + */ +function print_citations(array $citations): void { - $citations = $metadata->get('citations'); - - if (null === $citations) { - return; - } - echo 'Citations:'.\PHP_EOL; if (0 === count($citations)) { diff --git a/examples/perplexity/image-input-url.php b/examples/perplexity/image-input-url.php index 69b00b73..21d7649d 100644 --- a/examples/perplexity/image-input-url.php +++ b/examples/perplexity/image-input-url.php @@ -29,5 +29,5 @@ $result = $platform->invoke('sonar', $messages); echo $result->asText().\PHP_EOL; -print_search_results($result->getMetadata()); -print_citations($result->getMetadata()); +print_search_results($result->getMetadata()->get('search_results')); +print_citations($result->getMetadata()->get('citations')); diff --git a/examples/perplexity/pdf-input-url.php b/examples/perplexity/pdf-input-url.php index 6869d7d2..044fe073 100644 --- a/examples/perplexity/pdf-input-url.php +++ b/examples/perplexity/pdf-input-url.php @@ -28,5 +28,5 @@ $result = $platform->invoke('sonar', $messages); echo $result->asText().\PHP_EOL; -print_search_results($result->getMetadata()); -print_citations($result->getMetadata()); +print_search_results($result->getMetadata()->get('search_results')); +print_citations($result->getMetadata()->get('citations')); diff --git a/examples/perplexity/stream.php b/examples/perplexity/stream.php index 0fbd3709..523acee7 100644 --- a/examples/perplexity/stream.php +++ b/examples/perplexity/stream.php @@ -30,5 +30,5 @@ foreach ($result->asStream() as $word) { } echo \PHP_EOL; -print_search_results($result->getMetadata()); -print_citations($result->getMetadata()); +print_search_results($result->getResult()->getMetadata()->get('search_results')); +print_citations($result->getResult()->getMetadata()->get('citations')); diff --git a/examples/perplexity/web-search.php b/examples/perplexity/web-search.php index 47e52177..5f80b700 100644 --- a/examples/perplexity/web-search.php +++ b/examples/perplexity/web-search.php @@ -30,5 +30,5 @@ $result = $platform->invoke('sonar', $messages, [ echo $result->asText().\PHP_EOL; echo \PHP_EOL; -print_search_results($result->getMetadata()); -print_citations($result->getMetadata()); +print_search_results($result->getMetadata()->get('search_results')); +print_citations($result->getMetadata()->get('citations')); diff --git a/src/platform/src/Bridge/Perplexity/ResultConverter.php b/src/platform/src/Bridge/Perplexity/ResultConverter.php index 4c6142a4..5b49857f 100644 --- a/src/platform/src/Bridge/Perplexity/ResultConverter.php +++ b/src/platform/src/Bridge/Perplexity/ResultConverter.php @@ -12,7 +12,6 @@ namespace Symfony\AI\Platform\Bridge\Perplexity; use Symfony\AI\Platform\Exception\RuntimeException; -use Symfony\AI\Platform\Metadata\Metadata; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -34,7 +33,7 @@ final class ResultConverter implements ResultConverterInterface public function convert(RawResultInterface $result, array $options = []): ResultInterface { if ($options['stream'] ?? false) { - return new StreamResult($this->convertStream($result)); + return new StreamResult($this->convertStream($result), [new StreamListener()]); } $data = $result->getData(); @@ -67,26 +66,19 @@ final class ResultConverter implements ResultConverterInterface private function convertStream(RawResultInterface $result): \Generator { - $searchResults = $citations = []; - /** @var Metadata $metadata */ - $metadata = yield; - foreach ($result->getDataStream() as $data) { if (isset($data['choices'][0]['delta']['content'])) { yield $data['choices'][0]['delta']['content']; } - - if (isset($data['search_results'])) { - $searchResults = $data['search_results']; - } - - if (isset($data['citations'])) { - $citations = $data['citations']; - } } - $metadata->add('search_results', $searchResults); - $metadata->add('citations', $citations); + if (isset($data['search_results'])) { + yield ['search_results' => $data['search_results']]; + } + + if (isset($data['citations'])) { + yield ['citations' => $data['citations']]; + } } /** diff --git a/src/platform/src/Bridge/Perplexity/StreamListener.php b/src/platform/src/Bridge/Perplexity/StreamListener.php new file mode 100644 index 00000000..79856fd0 --- /dev/null +++ b/src/platform/src/Bridge/Perplexity/StreamListener.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Perplexity; + +use Symfony\AI\Platform\Result\Stream\AbstractStreamListener; +use Symfony\AI\Platform\Result\Stream\ChunkEvent; + +/** + * @author Christopher Hertel + */ +final class StreamListener extends AbstractStreamListener +{ + public function onChunk(ChunkEvent $event): void + { + $chunk = $event->getChunk(); + + if (!\is_array($chunk)) { + return; + } + + if (isset($chunk['search_results'])) { + $event->getMetadata()->add('search_results', $chunk['search_results']); + $event->skipChunk(); + } + + if (isset($chunk['citations'])) { + $event->getMetadata()->add('citations', $chunk['citations']); + $event->skipChunk(); + } + } +} diff --git a/src/platform/src/Bridge/Perplexity/Tests/StreamListenerTest.php b/src/platform/src/Bridge/Perplexity/Tests/StreamListenerTest.php new file mode 100644 index 00000000..09c64a58 --- /dev/null +++ b/src/platform/src/Bridge/Perplexity/Tests/StreamListenerTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Perplexity\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Perplexity\StreamListener; +use Symfony\AI\Platform\Result\StreamResult; + +final class StreamListenerTest extends TestCase +{ + public function testSearchResultsAreAddedToMetadataAndChunkIsSkipped() + { + $searchResults = [['url' => 'https://example.com', 'title' => 'Example']]; + $streamResult = new StreamResult((static function () use ($searchResults): \Generator { + yield ['search_results' => $searchResults]; + yield 'Hello World'; + })()); + + $streamResult->addListener(new StreamListener()); + + $chunks = iterator_to_array($streamResult->getContent()); + + $this->assertSame(['Hello World'], $chunks); + $this->assertTrue($streamResult->getMetadata()->has('search_results')); + $this->assertSame($searchResults, $streamResult->getMetadata()->get('search_results')); + } + + public function testCitationsAreAddedToMetadataAndChunkIsSkipped() + { + $citations = ['https://example.com/1', 'https://example.com/2']; + $streamResult = new StreamResult((static function () use ($citations): \Generator { + yield ['citations' => $citations]; + yield 'Hello World'; + })()); + + $streamResult->addListener(new StreamListener()); + + $chunks = iterator_to_array($streamResult->getContent()); + + $this->assertSame(['Hello World'], $chunks); + $this->assertTrue($streamResult->getMetadata()->has('citations')); + $this->assertSame($citations, $streamResult->getMetadata()->get('citations')); + } + + public function testNonArrayChunksAreNotProcessed() + { + $streamResult = new StreamResult((static function (): \Generator { + yield 'Hello '; + yield 'World'; + })()); + + $streamResult->addListener(new StreamListener()); + + $chunks = iterator_to_array($streamResult->getContent()); + + $this->assertSame(['Hello ', 'World'], $chunks); + $this->assertFalse($streamResult->getMetadata()->has('search_results')); + $this->assertFalse($streamResult->getMetadata()->has('citations')); + } + + public function testBothSearchResultsAndCitationsAreProcessed() + { + $searchResults = [['url' => 'https://example.com']]; + $citations = ['https://example.com/1']; + $streamResult = new StreamResult((static function () use ($searchResults, $citations): \Generator { + yield ['search_results' => $searchResults]; + yield 'Content'; + yield ['citations' => $citations]; + })()); + + $streamResult->addListener(new StreamListener()); + + $chunks = iterator_to_array($streamResult->getContent()); + + $this->assertSame(['Content'], $chunks); + $this->assertSame($searchResults, $streamResult->getMetadata()->get('search_results')); + $this->assertSame($citations, $streamResult->getMetadata()->get('citations')); + } +}