Skip to content

Commit b33bedc

Browse files
committed
Add tests to cover Perplexity platform bridge
1 parent 086f84e commit b33bedc

File tree

10 files changed

+595
-39
lines changed

10 files changed

+595
-39
lines changed

examples/bootstrap.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,53 @@ function print_vectors(ResultPromise $result): void
8080

8181
echo 'Dimensions: '.$result->asVectors()[0]->getDimensions().\PHP_EOL;
8282
}
83+
84+
function perplexity_print_search_results(Metadata $metadata): void
85+
{
86+
$searchResults = $metadata->get('search_results');
87+
88+
if (null === $searchResults) {
89+
return;
90+
}
91+
92+
echo 'Search results:'.\PHP_EOL;
93+
94+
if (0 === count($searchResults)) {
95+
echo 'No search results.'.\PHP_EOL;
96+
97+
return;
98+
}
99+
100+
foreach ($searchResults as $i => $searchResult) {
101+
echo 'Result #'.($i + 1).':'.\PHP_EOL;
102+
echo $searchResult['title'].\PHP_EOL;
103+
echo $searchResult['url'].\PHP_EOL;
104+
echo $searchResult['date'].\PHP_EOL;
105+
echo $searchResult['last_updated'] ? $searchResult['last_updated'].\PHP_EOL : '';
106+
echo $searchResult['snippet'] ? $searchResult['snippet'].\PHP_EOL : '';
107+
echo \PHP_EOL;
108+
}
109+
}
110+
111+
function perplexity_print_citations(Metadata $metadata): void
112+
{
113+
$citations = $metadata->get('citations');
114+
115+
if (null === $citations) {
116+
return;
117+
}
118+
119+
echo 'Citations:'.\PHP_EOL;
120+
121+
if (0 === count($citations)) {
122+
echo 'No citations.'.\PHP_EOL;
123+
124+
return;
125+
}
126+
127+
foreach ($citations as $i => $citation) {
128+
echo 'Citation #'.($i + 1).':'.\PHP_EOL;
129+
echo $citation.\PHP_EOL;
130+
echo \PHP_EOL;
131+
}
132+
}

examples/perplexity/academic-search.php

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,35 +33,5 @@
3333
echo $response->getContent().\PHP_EOL;
3434
echo \PHP_EOL;
3535

36-
$metadata = $response->getMetadata();
37-
if ($metadata->has('search_results')) {
38-
echo 'Search results:'.\PHP_EOL;
39-
if (0 === count($metadata->get('search_results'))) {
40-
echo 'No search results.'.\PHP_EOL;
41-
42-
return;
43-
}
44-
foreach ($metadata->get('search_results') as $i => $searchResult) {
45-
echo 'Result #'.($i + 1).':'.\PHP_EOL;
46-
echo $searchResult['title'].\PHP_EOL;
47-
echo $searchResult['url'].\PHP_EOL;
48-
echo $searchResult['date'].\PHP_EOL;
49-
echo $searchResult['last_updated'] ? $searchResult['last_updated'].\PHP_EOL : '';
50-
echo $searchResult['snippet'] ? $searchResult['snippet'].\PHP_EOL : '';
51-
echo \PHP_EOL;
52-
}
53-
}
54-
55-
if ($metadata->has('citations')) {
56-
echo 'Citations:'.\PHP_EOL;
57-
if (0 === count($metadata->get('citations'))) {
58-
echo 'No citations.'.\PHP_EOL;
59-
60-
return;
61-
}
62-
foreach ($metadata->get('citations') as $i => $citation) {
63-
echo 'Citation #'.($i + 1).':'.\PHP_EOL;
64-
echo $citation.\PHP_EOL;
65-
echo \PHP_EOL;
66-
}
67-
}
36+
perplexity_print_search_results($response->getMetadata());
37+
perplexity_print_citations($response->getMetadata());

examples/perplexity/token-metadata.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use Symfony\AI\Platform\Bridge\Perplexity\TokenOutputProcessor;
1717
use Symfony\AI\Platform\Message\Message;
1818
use Symfony\AI\Platform\Message\MessageBag;
19-
use Symfony\AI\Platform\Metadata\TokenUsage;
2019

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

@@ -33,9 +32,4 @@
3332
'max_tokens' => 500, // specific options just for this call
3433
]);
3534

36-
$metadata = $result->getMetadata();
37-
$tokenUsage = $metadata->get('token_usage');
38-
39-
assert($tokenUsage instanceof TokenUsage);
40-
4135
print_token_usage($result->getMetadata());

examples/perplexity/web-search.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,3 @@
3434
]);
3535

3636
echo $response->getContent().\PHP_EOL;
37-
echo \PHP_EOL;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\Bridge\Perplexity\Contract;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\Attributes\Medium;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\AI\Platform\Bridge\Perplexity\Contract\FileUrlNormalizer;
19+
use Symfony\AI\Platform\Bridge\Perplexity\Perplexity;
20+
use Symfony\AI\Platform\Contract;
21+
use Symfony\AI\Platform\Contract\Normalizer\Message\MessageBagNormalizer;
22+
use Symfony\AI\Platform\Message\Content\DocumentUrl;
23+
24+
#[Medium]
25+
#[CoversClass(FileUrlNormalizer::class)]
26+
#[CoversClass(MessageBagNormalizer::class)]
27+
final class FileUrlNormalizerTest extends TestCase
28+
{
29+
public function testSupportsNormalization()
30+
{
31+
$normalizer = new FileUrlNormalizer();
32+
33+
$this->assertTrue($normalizer->supportsNormalization(new DocumentUrl(\dirname(__DIR__, 6).'/fixtures/not-a-document.pdf'), context: [
34+
Contract::CONTEXT_MODEL => new Perplexity(),
35+
]));
36+
$this->assertFalse($normalizer->supportsNormalization('not a document'));
37+
}
38+
39+
public function testGetSupportedTypes()
40+
{
41+
$normalizer = new FileUrlNormalizer();
42+
43+
$expected = [
44+
DocumentUrl::class => true,
45+
];
46+
47+
$this->assertSame($expected, $normalizer->getSupportedTypes(null));
48+
}
49+
50+
#[DataProvider('normalizeDataProvider')]
51+
public function testNormalize(DocumentUrl $document, array $expected)
52+
{
53+
$normalizer = new FileUrlNormalizer();
54+
55+
$normalized = $normalizer->normalize($document);
56+
57+
$this->assertEquals($expected, $normalized);
58+
}
59+
60+
public static function normalizeDataProvider(): iterable
61+
{
62+
yield 'document from file url' => [
63+
new DocumentUrl(\dirname(__DIR__, 6).'/fixtures/document.pdf'),
64+
[
65+
'type' => 'file_url',
66+
'file_url' => [
67+
'url' => \dirname(__DIR__, 6).'/fixtures/document.pdf',
68+
],
69+
],
70+
];
71+
}
72+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Bridge\Perplexity;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\Attributes\TestWith;
17+
use PHPUnit\Framework\Attributes\UsesClass;
18+
use PHPUnit\Framework\TestCase;
19+
use Symfony\AI\Platform\Bridge\Perplexity\ModelClient;
20+
use Symfony\AI\Platform\Bridge\Perplexity\Perplexity;
21+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
22+
use Symfony\Component\HttpClient\EventSourceHttpClient;
23+
use Symfony\Component\HttpClient\MockHttpClient;
24+
use Symfony\Component\HttpClient\Response\MockResponse;
25+
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
26+
27+
/**
28+
* @author Mathieu Santostefano <msantostefano@proton.me>
29+
*/
30+
#[CoversClass(ModelClient::class)]
31+
#[UsesClass(Perplexity::class)]
32+
#[Small]
33+
final class ModelClientTest extends TestCase
34+
{
35+
public function testItThrowsExceptionWhenApiKeyIsEmpty()
36+
{
37+
$this->expectException(InvalidArgumentException::class);
38+
$this->expectExceptionMessage('The API key must not be empty.');
39+
40+
new ModelClient(new MockHttpClient(), '');
41+
}
42+
43+
#[TestWith(['api-key-without-prefix'])]
44+
#[TestWith(['plx-api-key'])]
45+
#[TestWith(['PPLX-api-key'])]
46+
#[TestWith(['pplxapikey'])]
47+
#[TestWith(['pplx api-key'])]
48+
#[TestWith(['pplx'])]
49+
public function testItThrowsExceptionWhenApiKeyDoesNotStartWithPplx(string $invalidApiKey)
50+
{
51+
$this->expectException(InvalidArgumentException::class);
52+
$this->expectExceptionMessage('The API key must start with "pplx-".');
53+
54+
new ModelClient(new MockHttpClient(), $invalidApiKey);
55+
}
56+
57+
public function testItAcceptsValidApiKey()
58+
{
59+
$modelClient = new ModelClient(new MockHttpClient(), 'pplx-valid-api-key');
60+
61+
$this->assertInstanceOf(ModelClient::class, $modelClient);
62+
}
63+
64+
public function testItWrapsHttpClientInEventSourceHttpClient()
65+
{
66+
$httpClient = new MockHttpClient();
67+
$modelClient = new ModelClient($httpClient, 'pplx-valid-api-key');
68+
69+
$this->assertInstanceOf(ModelClient::class, $modelClient);
70+
}
71+
72+
public function testItAcceptsEventSourceHttpClientDirectly()
73+
{
74+
$httpClient = new EventSourceHttpClient(new MockHttpClient());
75+
$modelClient = new ModelClient($httpClient, 'pplx-valid-api-key');
76+
77+
$this->assertInstanceOf(ModelClient::class, $modelClient);
78+
}
79+
80+
public function testItIsSupportingTheCorrectModel()
81+
{
82+
$modelClient = new ModelClient(new MockHttpClient(), 'pplx-api-key');
83+
84+
$this->assertTrue($modelClient->supports(new Perplexity()));
85+
}
86+
87+
public function testItIsExecutingTheCorrectRequest()
88+
{
89+
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
90+
self::assertSame('POST', $method);
91+
self::assertSame('https://api.perplexity.ai/chat/completions', $url);
92+
self::assertSame('Authorization: Bearer pplx-api-key', $options['normalized_headers']['authorization'][0]);
93+
self::assertSame('{"model":"sonar","messages":[{"role":"user","content":"test message"}]}', $options['body']);
94+
95+
return new MockResponse();
96+
};
97+
$httpClient = new MockHttpClient([$resultCallback]);
98+
$modelClient = new ModelClient($httpClient, 'pplx-api-key');
99+
$modelClient->request(new Perplexity(), ['model' => 'sonar', 'messages' => [['role' => 'user', 'content' => 'test message']]]);
100+
}
101+
102+
public function testItIsExecutingTheCorrectRequestWithArrayPayload()
103+
{
104+
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
105+
self::assertSame('POST', $method);
106+
self::assertSame('https://api.perplexity.ai/chat/completions', $url);
107+
self::assertSame('Authorization: Bearer pplx-api-key', $options['normalized_headers']['authorization'][0]);
108+
self::assertSame('{"model":"sonar","messages":[{"role":"user","content":"Hello"}]}', $options['body']);
109+
110+
return new MockResponse();
111+
};
112+
$httpClient = new MockHttpClient([$resultCallback]);
113+
$modelClient = new ModelClient($httpClient, 'pplx-api-key');
114+
$modelClient->request(new Perplexity(), ['model' => 'sonar', 'messages' => [['role' => 'user', 'content' => 'Hello']]]);
115+
}
116+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\Bridge\Perplexity;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\Platform\Bridge\Perplexity\Perplexity;
18+
19+
/**
20+
* @author Mathieu Santostefano <msantostefano@proton.me>
21+
*/
22+
#[CoversClass(Perplexity::class)]
23+
#[Small]
24+
final class PerplexityTest extends TestCase
25+
{
26+
public function testItCreatesPerplexityWithDefaultSettings()
27+
{
28+
$perplexity = new Perplexity();
29+
30+
$this->assertSame(Perplexity::SONAR, $perplexity->getName());
31+
$this->assertSame([], $perplexity->getOptions());
32+
}
33+
34+
public function testItCreatesPerplexityWithCustomSettings()
35+
{
36+
$perplexity = new Perplexity(Perplexity::SONAR_PRO, ['temperature' => 0.5, 'max_tokens' => 1000]);
37+
38+
$this->assertSame(Perplexity::SONAR_PRO, $perplexity->getName());
39+
$this->assertSame(['temperature' => 0.5, 'max_tokens' => 1000], $perplexity->getOptions());
40+
}
41+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\Bridge\Perplexity;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\Platform\Bridge\Perplexity\PlatformFactory;
18+
use Symfony\AI\Platform\Platform;
19+
use Symfony\Component\HttpClient\EventSourceHttpClient;
20+
use Symfony\Component\HttpClient\MockHttpClient;
21+
22+
/**
23+
* @author Mathieu Santostefano <msantostefano@proton.me>
24+
*/
25+
#[CoversClass(PlatformFactory::class)]
26+
#[Small]
27+
final class PlatformFactoryTest extends TestCase
28+
{
29+
public function testItCreatesPlatformWithDefaultSettings()
30+
{
31+
$platform = PlatformFactory::create('pplx-test-api-key');
32+
33+
$this->assertInstanceOf(Platform::class, $platform);
34+
}
35+
36+
public function testItCreatesPlatformWithCustomHttpClient()
37+
{
38+
$httpClient = new MockHttpClient();
39+
$platform = PlatformFactory::create('pplx-test-api-key', $httpClient);
40+
41+
$this->assertInstanceOf(Platform::class, $platform);
42+
}
43+
44+
public function testItCreatesPlatformWithEventSourceHttpClient()
45+
{
46+
$httpClient = new EventSourceHttpClient(new MockHttpClient());
47+
$platform = PlatformFactory::create('pplx-test-api-key', $httpClient);
48+
49+
$this->assertInstanceOf(Platform::class, $platform);
50+
}
51+
}

0 commit comments

Comments
 (0)