Files
archived-ai-anthropic-platform/ResultConverter.php
2026-02-19 00:08:48 +01:00

187 lines
6.5 KiB
PHP

<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\AI\Platform\Bridge\Anthropic;
use Symfony\AI\Platform\Exception\RateLimitExceededException;
use Symfony\AI\Platform\Exception\RuntimeException;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\Result\RawHttpResult;
use Symfony\AI\Platform\Result\RawResultInterface;
use Symfony\AI\Platform\Result\ResultInterface;
use Symfony\AI\Platform\Result\StreamResult;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\AI\Platform\Result\ThinkingContent;
use Symfony\AI\Platform\Result\ToolCall;
use Symfony\AI\Platform\Result\ToolCallResult;
use Symfony\AI\Platform\ResultConverterInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
class ResultConverter implements ResultConverterInterface
{
public function supports(Model $model): bool
{
return $model instanceof Claude;
}
public function convert(RawHttpResult|RawResultInterface $result, array $options = []): ResultInterface
{
$response = $result->getObject();
if (429 === $response->getStatusCode()) {
$retryAfter = $response->getHeaders(false)['retry-after'][0] ?? null;
$retryAfterValue = $retryAfter ? (int) $retryAfter : null;
throw new RateLimitExceededException($retryAfterValue);
}
if ($options['stream'] ?? false) {
return new StreamResult($this->convertStream($result));
}
$data = $result->getData();
if (isset($data['type']) && 'error' === $data['type']) {
$type = $data['error']['type'] ?? 'Unknown';
$message = $data['error']['message'] ?? 'An unknown error occurred.';
throw new RuntimeException(\sprintf('API Error [%s]: "%s"', $type, $message));
}
if (!isset($data['content']) || [] === $data['content']) {
throw new RuntimeException('Response does not contain any content.');
}
$toolCalls = [];
$text = null;
foreach ($data['content'] as $content) {
if ('tool_use' === $content['type']) {
$toolCalls[] = new ToolCall($content['id'], $content['name'], $content['input']);
} elseif ('text' === $content['type']) {
$text = $content['text'];
}
}
if (null === $text && [] === $toolCalls) {
throw new RuntimeException('Response content does not contain any text nor tool calls.');
}
if ([] !== $toolCalls) {
return new ToolCallResult(...$toolCalls);
}
return new TextResult($text);
}
public function getTokenUsageExtractor(): TokenUsageExtractor
{
return new TokenUsageExtractor();
}
private function convertStream(RawResultInterface $result): \Generator
{
$toolCalls = [];
$currentToolCall = null;
$currentToolCallJson = '';
$currentThinking = null;
$currentThinkingSignature = null;
foreach ($result->getDataStream() as $data) {
$type = $data['type'] ?? '';
// Handle text content deltas
if ('content_block_delta' === $type && isset($data['delta']['text'])) {
yield $data['delta']['text'];
continue;
}
// Handle thinking content block start
if ('content_block_start' === $type
&& isset($data['content_block']['type'])
&& 'thinking' === $data['content_block']['type']
) {
$currentThinking = '';
$currentThinkingSignature = null;
continue;
}
// Handle thinking content deltas
if ('content_block_delta' === $type
&& isset($data['delta']['type'])
&& 'thinking_delta' === $data['delta']['type']
) {
$currentThinking .= $data['delta']['thinking'] ?? '';
continue;
}
// Handle thinking signature deltas
if ('content_block_delta' === $type
&& isset($data['delta']['type'])
&& 'signature_delta' === $data['delta']['type']
) {
$currentThinkingSignature = ($currentThinkingSignature ?? '').$data['delta']['signature'];
continue;
}
// Handle tool_use content block start
if ('content_block_start' === $type
&& isset($data['content_block']['type'])
&& 'tool_use' === $data['content_block']['type']
) {
$currentToolCall = [
'id' => $data['content_block']['id'],
'name' => $data['content_block']['name'],
];
$currentToolCallJson = '';
continue;
}
// Handle tool_use input JSON deltas
if ('content_block_delta' === $type
&& isset($data['delta']['type'])
&& 'input_json_delta' === $data['delta']['type']
) {
$currentToolCallJson .= $data['delta']['partial_json'] ?? '';
continue;
}
// Handle content block stop - finalize current thinking or tool call
if ('content_block_stop' === $type) {
if (null !== $currentThinking) {
yield new ThinkingContent($currentThinking, $currentThinkingSignature);
$currentThinking = null;
$currentThinkingSignature = null;
continue;
}
if (null !== $currentToolCall) {
$input = '' !== $currentToolCallJson
? json_decode($currentToolCallJson, true, flags: \JSON_THROW_ON_ERROR)
: [];
$toolCalls[] = new ToolCall(
$currentToolCall['id'],
$currentToolCall['name'],
$input
);
$currentToolCall = null;
$currentToolCallJson = '';
continue;
}
}
// Handle message stop - yield tool calls if any were collected
if ('message_stop' === $type && [] !== $toolCalls) {
yield new ToolCallResult(...$toolCalls);
}
}
}
}