Skip to content

Commit 6e9b56d

Browse files
committed
WIP add perplexity platform
1 parent 9803007 commit 6e9b56d

File tree

7 files changed

+350
-0
lines changed

7 files changed

+350
-0
lines changed

examples/perplexity/chat.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\Perplexity\Perplexity;
14+
use Symfony\AI\Platform\Bridge\Perplexity\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$platform = PlatformFactory::create(env('PERPLEXITY_API_KEY'), http_client());
21+
$model = new Perplexity();
22+
$agent = new Agent($platform, $model);
23+
24+
$messages = new MessageBag(Message::ofUser('What is the best French cheese?'));
25+
$response = $agent->call($messages);
26+
27+
echo $response->getContent().\PHP_EOL;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\Perplexity\Perplexity;
14+
use Symfony\AI\Platform\Bridge\Perplexity\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$platform = PlatformFactory::create(env('PERPLEXITY_API_KEY'), http_client());
21+
$model = new Perplexity();
22+
$agent = new Agent($platform, $model, logger: logger());
23+
24+
$messages = new MessageBag(Message::ofUser('What is 2 + 2?'));
25+
$response = $agent->call($messages, [
26+
'disable_search' => true,
27+
]);
28+
29+
echo $response->getContent().\PHP_EOL;

examples/perplexity/search.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\Perplexity\Perplexity;
14+
use Symfony\AI\Platform\Bridge\Perplexity\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$platform = PlatformFactory::create(env('PERPLEXITY_API_KEY'), http_client());
21+
$model = new Perplexity();
22+
$agent = new Agent($platform, $model, logger: logger());
23+
24+
$messages = new MessageBag(Message::ofUser('What is the best French cheese of the first quarter-century of 21st century?'));
25+
$response = $agent->call($messages, [
26+
'search_domain_filter' => [
27+
'https://en.wikipedia.org/wiki/Cheese',
28+
],
29+
'search_mode' => 'academic',
30+
'search_after_date_filter' => '2000-01-01',
31+
'search_before_date_filter' => '2025-01-01',
32+
]);
33+
34+
echo $response->getContent().\PHP_EOL;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\Bridge\Perplexity;
13+
14+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
15+
use Symfony\AI\Platform\Model;
16+
use Symfony\AI\Platform\ModelClientInterface;
17+
use Symfony\AI\Platform\Result\RawHttpResult;
18+
use Symfony\AI\Platform\Result\RawResultInterface;
19+
use Symfony\Component\HttpClient\EventSourceHttpClient;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @author Mathieu Santostefano <msantostefano@proton.me>
24+
*/
25+
final readonly class
26+
ModelClient implements ModelClientInterface
27+
{
28+
private EventSourceHttpClient $httpClient;
29+
30+
public function __construct(
31+
HttpClientInterface $httpClient,
32+
#[\SensitiveParameter]
33+
private string $apiKey,
34+
) {
35+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
36+
'' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.');
37+
str_starts_with($apiKey, 'pplx-') || throw new InvalidArgumentException('The API key must start with "pplx-".');
38+
}
39+
40+
public function supports(Model $model): bool
41+
{
42+
return $model instanceof Perplexity;
43+
}
44+
45+
public function request(Model $model, array|string $payload, array $options = []): RawResultInterface
46+
{
47+
return new RawHttpResult($this->httpClient->request('POST', 'https://api.perplexity.ai/chat/completions', [
48+
'auth_bearer' => $this->apiKey,
49+
'json' => array_merge($options, $payload),
50+
]));
51+
}
52+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Bridge\Perplexity;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Model;
16+
17+
/**
18+
* @author Mathieu Santostefano <msantostefano@proton.me>
19+
*/
20+
final class Perplexity extends Model
21+
{
22+
public const SONAR = 'sonar';
23+
public const SONAR_PRO = 'sonar-pro';
24+
public const SONAR_REASONING = 'sonar-reasoning';
25+
public const SONAR_REASONING_PRO = 'sonar-reasoning-pro';
26+
public const SONAR_DEEP_RESEARCH = 'sonar-deep-research';
27+
28+
/**
29+
* @param array<string, mixed> $options
30+
*/
31+
public function __construct(
32+
string $name = self::SONAR,
33+
array $options = [],
34+
) {
35+
$capabilities = [
36+
Capability::INPUT_MESSAGES,
37+
Capability::INPUT_PDF,
38+
Capability::OUTPUT_TEXT,
39+
Capability::OUTPUT_STREAMING,
40+
Capability::OUTPUT_STRUCTURED,
41+
];
42+
43+
if (self::SONAR_DEEP_RESEARCH !== $name) {
44+
$capabilities[] = Capability::INPUT_IMAGE;
45+
}
46+
47+
parent::__construct($name, $capabilities, $options);
48+
}
49+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Bridge\Perplexity;
13+
14+
use Symfony\AI\Platform\Contract;
15+
use Symfony\AI\Platform\Platform;
16+
use Symfony\Component\HttpClient\EventSourceHttpClient;
17+
use Symfony\Contracts\HttpClient\HttpClientInterface;
18+
19+
/**
20+
* @author Mathieu Santostefano <msantostefano@proton.me>
21+
*/
22+
final class PlatformFactory
23+
{
24+
public static function create(
25+
#[\SensitiveParameter] string $apiKey,
26+
?HttpClientInterface $httpClient = null,
27+
?Contract $contract = null,
28+
): Platform {
29+
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
30+
31+
return new Platform(
32+
[new ModelClient($httpClient, $apiKey)],
33+
[new ResultConverter()],
34+
$contract,
35+
);
36+
}
37+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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\Bridge\Perplexity;
13+
14+
use Symfony\AI\Platform\Exception\ContentFilterException;
15+
use Symfony\AI\Platform\Exception\RuntimeException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\Result\ChoiceResult;
18+
use Symfony\AI\Platform\Result\RawHttpResult;
19+
use Symfony\AI\Platform\Result\RawResultInterface;
20+
use Symfony\AI\Platform\Result\ResultInterface;
21+
use Symfony\AI\Platform\Result\StreamResult;
22+
use Symfony\AI\Platform\Result\TextResult;
23+
use Symfony\AI\Platform\Result\ToolCall;
24+
use Symfony\AI\Platform\Result\ToolCallResult;
25+
use Symfony\AI\Platform\ResultConverterInterface as PlatformResponseConverter;
26+
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
27+
use Symfony\Component\HttpClient\EventSourceHttpClient;
28+
use Symfony\Component\HttpClient\Exception\JsonException;
29+
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
30+
31+
/**
32+
* @author Mathieu Santostefano <msantostefano@proton.me>
33+
*/
34+
final class ResultConverter implements PlatformResponseConverter
35+
{
36+
public function supports(Model $model): bool
37+
{
38+
return $model instanceof Perplexity;
39+
}
40+
41+
public function convert(RawResultInterface|RawHttpResult $result, array $options = []): ResultInterface
42+
{
43+
if ($options['stream'] ?? false) {
44+
return new StreamResult($this->convertStream($result->getObject()));
45+
}
46+
47+
$data = $result->getData();
48+
dump($data);
49+
50+
if (!isset($data['choices'])) {
51+
throw new RuntimeException('Response does not contain choices.');
52+
}
53+
54+
$choices = array_map($this->convertChoice(...), $data['choices']);
55+
56+
$result = 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices);
57+
58+
if (isset($data['citations'])) {
59+
$citations = $this->convertCitations($data['citations']);
60+
}
61+
62+
if (isset($data['search_results'])) {
63+
$searchResults = $this->convertSearchResults($data['search_results']);
64+
}
65+
66+
return $result;
67+
}
68+
69+
private function convertStream(HttpResponse $result): \Generator
70+
{
71+
foreach ((new EventSourceHttpClient())->stream($result) as $chunk) {
72+
if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) {
73+
continue;
74+
}
75+
76+
try {
77+
$data = $chunk->getArrayData();
78+
} catch (JsonException) {
79+
// try catch only needed for Symfony 6.4
80+
continue;
81+
}
82+
83+
if (!isset($data['choices'][0]['delta']['content'])) {
84+
continue;
85+
}
86+
87+
yield $data['choices'][0]['delta']['content'];
88+
}
89+
}
90+
91+
/**
92+
* @param array{
93+
* index: int,
94+
* message: array{
95+
* role: 'assistant',
96+
* content: ?string
97+
* },
98+
* delta: array{
99+
* role: 'assistant',
100+
* content: string,
101+
* finish_reason: 'stop'|'length',
102+
* } $choice
103+
*/
104+
private function convertChoice(array $choice): TextResult
105+
{
106+
if (\in_array($choice['finish_reason'], ['stop', 'length'], true)) {
107+
return new TextResult($choice['message']['content']);
108+
}
109+
110+
throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finish_reason']));
111+
}
112+
113+
private function convertCitations(array $citations): array
114+
{
115+
116+
}
117+
118+
private function convertSearchResults(array $searchResults): array
119+
{
120+
121+
}
122+
}

0 commit comments

Comments
 (0)