Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.35% covered (success)
95.35%
41 / 43
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AiInstructor
95.35% covered (success)
95.35%
41 / 43
83.33% covered (warning)
83.33%
5 / 6
9
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAiClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 instruct
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
4.01
 createForCustomConfig
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 createForOllama
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createForOpenAi
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\AiInstructor;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\ValueObjects\NonEmptyString;
6use Apie\SchemaGenerator\ComponentsBuilderFactory;
7use Apie\SchemaGenerator\SchemaGenerator;
8use Apie\Serializer\Serializer;
9use Apie\TypeConverter\ReflectionTypeFactory;
10use ReflectionNamedType;
11use ReflectionUnionType;
12use SensitiveParameter;
13use Symfony\Component\HttpClient\HttpClient;
14
15final class AiInstructor
16{
17    public function __construct(
18        private readonly SchemaGenerator $schemaGenerator,
19        private readonly Serializer $serializer,
20        private readonly AiClient $aiClient
21    ) {
22    }
23
24    public function getAiClient(): AiClient
25    {
26        return $this->aiClient;
27    }
28
29    public function instruct(
30        ReflectionNamedType|ReflectionUnionType|string $type,
31        NonEmptyString|string $model,
32        string $systemMessage,
33        string $prompt
34    ) {
35        if (is_string($type)) {
36            $type = ReflectionTypeFactory::createReflectionType($type);
37        }
38        if (is_string($model)) {
39            $model = NonEmptyString::fromNative($model);
40        }
41        $schema = $this->schemaGenerator->createSchema((string) $type);
42        $response = $this->aiClient->ask(
43            $systemMessage,
44            $prompt,
45            $schema,
46            $model
47        );
48        try {
49            return $this->serializer->denormalizeNewObject(
50                json_decode($response, true),
51                (string) $type,
52                new ApieContext()
53            );
54        } catch (\Exception $exception) {
55            throw new \LogicException(
56                "I could not map the AI response '" . $response . "' to '" . ((string) $type) . "', error: '" . $exception->getMessage() . '"',
57                0,
58                $exception
59            );
60        }
61    }
62
63    public static function createForCustomConfig(#[SensitiveParameter] string $apiKey, string $baseUrl): self
64    {
65        return new self(
66            new SchemaGenerator(ComponentsBuilderFactory::createComponentsBuilderFactory()),
67            Serializer::create(),
68            AiClient::create(
69                HttpClient::create([
70                    'max_redirects' => 7,
71                ]),
72                $baseUrl,
73                $apiKey
74            )
75        );
76    }
77
78    public static function createForOllama(string $baseUrl = 'http://localhost:11434/'): self
79    {
80        return self::createForCustomConfig(
81            'IGNORED',
82            $baseUrl,
83        );
84    }
85
86    public static function createForOpenAi(#[SensitiveParameter] string $apiSecret): self
87    {
88        return self::createForCustomConfig(
89            $apiSecret,
90            'https://api.openai.com/v1',
91        );
92    }
93}