Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
30 / 36
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
OpenAiClient
83.33% covered (warning)
83.33%
30 / 36
50.00% covered (danger)
50.00%
1 / 2
4.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 ask
82.35% covered (warning)
82.35%
28 / 34
0.00% covered (danger)
0.00%
0 / 1
3.05
1<?php
2namespace Apie\AiInstructor;
3
4use cebe\openapi\spec\Schema;
5use SensitiveParameter;
6use Symfony\Component\HttpClient\Exception\ClientException;
7use Symfony\Component\HttpClient\HttpClient;
8use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
9use Symfony\Contracts\HttpClient\HttpClientInterface;
10
11class OpenAiClient extends AiClient
12{
13    private HttpClientInterface $client;
14    private string $apiKey;
15
16    public function __construct(?HttpClientInterface $client = null, #[SensitiveParameter] string $apiKey = 'ignored')
17    {
18        $this->client = $client ?? HttpClient::create([]);
19        $this->apiKey = $apiKey;
20    }
21
22    public function ask(string $systemMessage, string $prompt, Schema $schema, ?string $model = null): string
23    {
24        try {
25            $response = $this->client->request('POST', 'https://api.openai.com/v1/chat/completions', [
26                'headers' => [
27                    'Authorization' => 'Bearer ' . $this->apiKey,
28                    'Content-Type' => 'application/json',
29                ],
30                'json' => [
31                    'model' => $model ?? 'gpt-4o-mini',
32                    'messages' => [
33                        ['role' => 'system', 'content' => $systemMessage],
34                        ['role' => 'user', 'content' => $prompt],
35                    ],
36                    'functions' => [[
37                        'name' => 'structured_response',
38                        'description' => 'Structured response as defined by schema, stored in a "result" property',
39                        'parameters' => [
40                            'type' => 'object',
41                            'properties' => [
42                                'result' => $schema->getSerializableData()
43                            ],
44                            'required' => ['result']
45                        ],
46                    ]],
47                    'function_call' => ['name' => 'structured_response'],
48                ],
49            ]);
50
51            $data = $response->toArray();
52            $functionCall = (array) json_decode($data['choices'][0]['message']['function_call']['arguments'] ?? null, true);
53            return $functionCall ? json_encode($functionCall['result'] ?? null, JSON_PRETTY_PRINT) : 'No structured response';
54        } catch (TransportExceptionInterface|ClientException $e) {
55            throw new \RuntimeException(
56                'Request failed: ' . $e->getMessage() . ' "' . ($response ?? null)?->getContent(false) . '"',
57                0,
58                $e
59            );
60        }
61    }
62}