calendi/backend/src/Infrastructure/Chat/OpenAIChatProvider.php
2025-04-26 04:16:02 +02:00

99 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Infrastructure\Chat;
use App\Domain\Chat\ChatProviderInterface;
use App\Domain\Chat\ChatResult;
use App\Domain\Chat\Choice;
use App\Domain\Chat\Message;
use App\Domain\Chat\MessageCollection;
use App\Domain\Chat\ToolCollection;
use App\Domain\Chat\ToolInterface;
use Exception;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
final readonly class OpenAIChatProvider implements ChatProviderInterface
{
public function __construct(
private HttpClientInterface $httpClient,
#[Autowire('%env(OPENAI_API_KEY)%')]
private string $openAiApiKey,
) {
}
/**
* @throws Exception When the OpenAI API returns an error
*/
public function chat(MessageCollection $messages, ToolCollection $tools, bool $forceToolCalls = false, bool $reasoning = true): ChatResult
{
$payload = [
'model' => $reasoning ? 'o4-mini' : 'gpt-4o-mini',
'messages' => array_map(function (Message $message) {
return $message->toArray();
}, $messages->toArray()),
];
if ($tools->count() > 0) {
$payload['tool_choice'] = $forceToolCalls ? 'required' : 'auto';
$payload['tools'] = [];
/** @var ToolInterface $tool */
foreach ($tools->toArray() as $tool) {
$payload['tools'][] = [
'type' => 'function',
'function' => [
'name' => $tool->getName(),
'description' => $tool->getDescription(),
'parameters' => [
'type' => 'object',
'properties' => $tool->getArguments(),
'required' => $tool->getRequiredArguments(),
],
],
];
}
}
try {
/** @var ResponseInterface $response */
$response = $this->httpClient->request('POST', "https://api.openai.com/v1/chat/completions", [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->openAiApiKey,
],
'json' => $payload,
'timeout' => 60 * 10,
'max_duration' => 60 * 10,
]);
$responseArray = $response->toArray(false);
if (isset($responseArray['choices']) && is_array($responseArray['choices']) && count($responseArray['choices']) > 0) {
$choices = [];
foreach ($responseArray['choices'] as $choice) {
$choices[] = Choice::fromArray($choice);
}
return new ChatResult(
choices: $choices,
messageHistory: $messages->toArray(),
);
}
throw new Exception('Error: ' . $response->getContent(false));
} catch (ClientExceptionInterface | DecodingExceptionInterface | RedirectionExceptionInterface |
ServerExceptionInterface | TransportExceptionInterface $e) {
throw new Exception('API Error: ' . $e->getMessage(), 0, $e);
}
}
}