Skip to content

Commit b73d83c

Browse files
committed
Improve error reporting in Platform layer
1 parent 2f05b00 commit b73d83c

File tree

10 files changed

+118
-26
lines changed

10 files changed

+118
-26
lines changed

examples/bootstrap.php

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111

1212
use Psr\Log\LoggerAwareInterface;
1313
use Psr\Log\LoggerInterface;
14+
use Symfony\AI\Agent\Exception\ExceptionInterface as AgentException;
15+
use Symfony\AI\Platform\Exception\ExceptionInterface as PlatformException;
16+
use Symfony\AI\Platform\Exception\ResultException;
1417
use Symfony\AI\Platform\Metadata\Metadata;
1518
use Symfony\AI\Platform\Metadata\TokenUsage;
1619
use Symfony\AI\Platform\Result\ResultPromise;
20+
use Symfony\AI\Store\Exception\ExceptionInterface as StoreException;
21+
use Symfony\Component\Console\Helper\Table;
1722
use Symfony\Component\Console\Logger\ConsoleLogger;
1823
use Symfony\Component\Console\Output\ConsoleOutput;
1924
use Symfony\Component\Dotenv\Dotenv;
@@ -26,7 +31,7 @@
2631
function env(string $var): string
2732
{
2833
if (!isset($_SERVER[$var]) || '' === $_SERVER[$var]) {
29-
printf('Please set the "%s" environment variable to run this example.', $var);
34+
output()->writeln(sprintf('<error>Please set the "%s" environment variable to run this example.</error>', $var));
3035
exit(1);
3136
}
3237

@@ -45,6 +50,11 @@ function http_client(): HttpClientInterface
4550
}
4651

4752
function logger(): LoggerInterface
53+
{
54+
return new ConsoleLogger(output());
55+
}
56+
57+
function output(): ConsoleOutput
4858
{
4959
$verbosity = match ($_SERVER['argv'][1] ?? null) {
5060
'-v', '--verbose' => ConsoleOutput::VERBOSITY_VERBOSE,
@@ -53,7 +63,7 @@ function logger(): LoggerInterface
5363
default => ConsoleOutput::VERBOSITY_NORMAL,
5464
};
5565

56-
return new ConsoleLogger(new ConsoleOutput($verbosity));
66+
return new ConsoleOutput($verbosity);
5767
}
5868

5969
function print_token_usage(Metadata $metadata): void
@@ -62,23 +72,29 @@ function print_token_usage(Metadata $metadata): void
6272

6373
assert($tokenUsage instanceof TokenUsage);
6474

65-
echo 'Prompt tokens: '.$tokenUsage->promptTokens.\PHP_EOL;
66-
echo 'Completion tokens: '.$tokenUsage->completionTokens.\PHP_EOL;
67-
echo 'Thinking tokens: '.$tokenUsage->thinkingTokens.\PHP_EOL;
68-
echo 'Tool tokens: '.$tokenUsage->toolTokens.\PHP_EOL;
69-
echo 'Cached tokens: '.$tokenUsage->cachedTokens.\PHP_EOL;
70-
echo 'Remaining tokens minute: '.$tokenUsage->remainingTokensMinute.\PHP_EOL;
71-
echo 'Remaining tokens month: '.$tokenUsage->remainingTokensMonth.\PHP_EOL;
72-
echo 'Remaining tokens: '.$tokenUsage->remainingTokens.\PHP_EOL;
73-
echo 'Utilized tokens: '.$tokenUsage->totalTokens.\PHP_EOL;
75+
$na = '<comment>n/a</comment>';
76+
$table = new Table(output());
77+
$table->setHeaderTitle('Token Usage');
78+
$table->setRows([
79+
['Prompt tokens', $tokenUsage->promptTokens ?? $na],
80+
['Completion tokens', $tokenUsage->completionTokens ?? $na],
81+
['Thinking tokens', $tokenUsage->thinkingTokens ?? $na],
82+
['Tool tokens', $tokenUsage->toolTokens ?? $na],
83+
['Cached tokens', $tokenUsage->cachedTokens ?? $na],
84+
['Remaining tokens minute', $tokenUsage->remainingTokensMinute ?? $na],
85+
['Remaining tokens month', $tokenUsage->remainingTokensMonth ?? $na],
86+
['Remaining tokens', $tokenUsage->remainingTokens ?? $na],
87+
['Utilized tokens', $tokenUsage->totalTokens ?? $na],
88+
]);
89+
$table->render();
7490
}
7591

7692
function print_vectors(ResultPromise $result): void
7793
{
7894
assert([] !== $result->asVectors());
7995
assert(array_key_exists(0, $result->asVectors()));
8096

81-
echo 'Dimensions: '.$result->asVectors()[0]->getDimensions().\PHP_EOL;
97+
output()->writeln(sprintf('Dimensions: %d', $result->asVectors()[0]->getDimensions()));
8298
}
8399

84100
function perplexity_print_search_results(Metadata $metadata): void
@@ -138,3 +154,21 @@ function print_stream(ResultPromise $result): void
138154
}
139155
echo \PHP_EOL;
140156
}
157+
158+
set_exception_handler(function ($exception) {
159+
if ($exception instanceof AgentException || $exception instanceof PlatformException || $exception instanceof StoreException) {
160+
output()->writeln(sprintf('<error>%s</error>', $exception->getMessage()));
161+
162+
if ($exception instanceof ResultException && output()->isVerbose()) {
163+
dump($exception->getDetails());
164+
}
165+
166+
if (output()->isVeryVerbose()) {
167+
output()->writeln($exception->getTraceAsString());
168+
}
169+
170+
exit(1);
171+
}
172+
173+
throw $exception;
174+
});

src/platform/src/Bridge/Anthropic/ResultConverter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\AI\Platform\Bridge\Anthropic;
1313

1414
use Symfony\AI\Platform\Exception\RateLimitExceededException;
15+
use Symfony\AI\Platform\Exception\ResultException;
1516
use Symfony\AI\Platform\Exception\RuntimeException;
1617
use Symfony\AI\Platform\Model;
1718
use Symfony\AI\Platform\Result\RawHttpResult;
@@ -52,6 +53,10 @@ public function convert(RawHttpResult|RawResultInterface $result, array $options
5253

5354
$data = $result->getData();
5455

56+
if (isset($data['error'])) {
57+
throw new ResultException($data['error']['message'], $data['error']);
58+
}
59+
5560
if (!isset($data['content']) || [] === $data['content']) {
5661
throw new RuntimeException('Response does not contain any content.');
5762
}

src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
1515
use Symfony\AI\Platform\Exception\AuthenticationException;
16-
use Symfony\AI\Platform\Exception\ContentFilterException;
1716
use Symfony\AI\Platform\Exception\RateLimitExceededException;
17+
use Symfony\AI\Platform\Exception\ResultException;
1818
use Symfony\AI\Platform\Exception\RuntimeException;
1919
use Symfony\AI\Platform\Model;
2020
use Symfony\AI\Platform\Result\ChoiceResult;
@@ -46,8 +46,8 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options
4646
$response = $result->getObject();
4747

4848
if (401 === $response->getStatusCode()) {
49-
$errorMessage = json_decode($response->getContent(false), true)['error']['message'];
50-
throw new AuthenticationException($errorMessage);
49+
$data = $response->toArray(false);
50+
throw new AuthenticationException($data['error']['message'], $data['error']);
5151
}
5252

5353
if (429 === $response->getStatusCode()) {
@@ -64,17 +64,17 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options
6464
}
6565

6666
if ($options['stream'] ?? false) {
67-
return new StreamResult($this->convertStream($response));
67+
return new StreamResult($this->convertStream($result->getObject()));
6868
}
6969

7070
$data = $result->getData();
7171

72-
if (isset($data['error']['code']) && 'content_filter' === $data['error']['code']) {
73-
throw new ContentFilterException($data['error']['message']);
72+
if (isset($data['error'])) {
73+
throw new ResultException($data['error']['message'], $data['error']);
7474
}
7575

7676
if (!isset($data['choices'])) {
77-
throw new RuntimeException('Response does not contain choices.');
77+
throw new ResultException('Result does not contain choices.');
7878
}
7979

8080
$choices = array_map($this->convertChoice(...), $data['choices']);

src/platform/src/Bridge/Replicate/Client.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\Replicate;
1313

14+
use Symfony\AI\Platform\Exception\ResultException;
1415
use Symfony\Component\Clock\ClockInterface;
1516
use Symfony\Contracts\HttpClient\HttpClientInterface;
1617
use Symfony\Contracts\HttpClient\ResponseInterface;
@@ -40,7 +41,11 @@ public function request(string $model, string $endpoint, array $body): ResponseI
4041
'auth_bearer' => $this->apiKey,
4142
'json' => ['input' => $body],
4243
]);
43-
$data = $response->toArray();
44+
$data = $response->toArray(false);
45+
46+
if (isset($data['detail'])) {
47+
throw new ResultException($data['detail'], $data);
48+
}
4449

4550
while (!\in_array($data['status'], ['succeeded', 'failed', 'canceled'], true)) {
4651
$this->clock->sleep(1); // we need to wait until the prediction is ready

src/platform/src/Bridge/Scaleway/Llm/ResultConverter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace Symfony\AI\Platform\Bridge\Scaleway\Llm;
1313

1414
use Symfony\AI\Platform\Bridge\Scaleway\Scaleway;
15-
use Symfony\AI\Platform\Exception\ContentFilterException;
15+
use Symfony\AI\Platform\Exception\ResultException;
1616
use Symfony\AI\Platform\Exception\RuntimeException;
1717
use Symfony\AI\Platform\Model;
1818
use Symfony\AI\Platform\Result\ChoiceResult;
@@ -46,8 +46,8 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options
4646
}
4747
$data = $result->getData();
4848

49-
if (isset($data['error']['code']) && 'content_filter' === $data['error']['code']) {
50-
throw new ContentFilterException($data['error']['message']);
49+
if (isset($data['error'])) {
50+
throw new ResultException($data['error']['message'], $data['error']);
5151
}
5252

5353
if (!isset($data['choices'])) {

src/platform/src/Bridge/Voyage/ResultConverter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\Voyage;
1313

14+
use Symfony\AI\Platform\Exception\ResultException;
1415
use Symfony\AI\Platform\Exception\RuntimeException;
1516
use Symfony\AI\Platform\Model;
1617
use Symfony\AI\Platform\Result\RawResultInterface;
@@ -33,6 +34,10 @@ public function convert(RawResultInterface $result, array $options = []): Result
3334
{
3435
$result = $result->getData();
3536

37+
if (isset($result['detail'])) {
38+
throw new ResultException($result['detail']);
39+
}
40+
3641
if (!isset($result['data'])) {
3742
throw new RuntimeException('Response does not contain embedding data.');
3843
}

src/platform/src/Exception/AuthenticationException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
* @author Vitalii Kyktov <vitalii.kyktov@gmail.com>
1818
* @author Dmytro Liashko <dmlyashko@gmail.com>
1919
*/
20-
class AuthenticationException extends RuntimeException
20+
class AuthenticationException extends ResultException
2121
{
2222
}

src/platform/src/Exception/RateLimitExceededException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/**
1515
* @author Floran Pagliai <floran.pagliai@gmail.com>
1616
*/
17-
final class RateLimitExceededException extends RuntimeException
17+
final class RateLimitExceededException extends ResultException
1818
{
1919
public function __construct(
2020
private readonly ?float $retryAfter = null,
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+
namespace Symfony\AI\Platform\Exception;
13+
14+
class ResultException extends \Exception implements ExceptionInterface
15+
{
16+
/**
17+
* @param array<string, string> $details
18+
*/
19+
public function __construct(
20+
string $message,
21+
private array $details = [],
22+
?\Throwable $previous = null,
23+
) {
24+
parent::__construct($message, previous: $previous);
25+
}
26+
27+
/**
28+
* @return array<string, string>
29+
*/
30+
public function getDetails(): array
31+
{
32+
return $this->details;
33+
}
34+
}

src/platform/src/Result/RawHttpResult.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\AI\Platform\Result;
1313

14+
use Symfony\AI\Platform\Exception\ResultException;
15+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
16+
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
1417
use Symfony\Contracts\HttpClient\ResponseInterface;
1518

1619
/**
@@ -25,7 +28,13 @@ public function __construct(
2528

2629
public function getData(): array
2730
{
28-
return $this->response->toArray(false);
31+
try {
32+
return $this->response->toArray();
33+
} catch (ClientExceptionInterface $e) {
34+
throw new ResultException(message: \sprintf('API responded with an error: "%s"', $e->getMessage()), details: $this->response->toArray(false), previous: $e);
35+
} catch (ExceptionInterface $e) {
36+
throw new ResultException(\sprintf('Error while calling the API: "%s"', $e->getMessage()), previous: $e);
37+
}
2938
}
3039

3140
public function getObject(): ResponseInterface

0 commit comments

Comments
 (0)