Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
21 / 42
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AiInstructor
50.00% covered (danger)
50.00%
21 / 42
20.00% covered (danger)
20.00%
1 / 5
16.00
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
 instruct
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
4.01
 createForCustomConfig
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 createForOllama
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 createForOpenAi
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
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 instruct(
25        ReflectionNamedType|ReflectionUnionType|string $type,
26        NonEmptyString|string $model,
27        string $systemMessage,
28        string $prompt
29    ) {
30        if (is_string($type)) {
31            $type = ReflectionTypeFactory::createReflectionType($type);
32        }
33        if (is_string($model)) {
34            $model = NonEmptyString::fromNative($model);
35        }
36        $schema = $this->schemaGenerator->createSchema((string) $type);
37        $response = $this->aiClient->ask(
38            $systemMessage,
39            $prompt,
40            $schema,
41            $model
42        );
43        try {
44            return $this->serializer->denormalizeNewObject(
45                json_decode($response, true),
46                (string) $type,
47                new ApieContext()
48            );
49        } catch (\Exception $exception) {
50            throw new \LogicException(
51                "I could not map the AI response '" . $response . "' to '" . ((string) $type) . "', error: '" . $exception->getMessage() . '"',
52                0,
53                $exception
54            );
55        }
56    }
57
58    public static function createForCustomConfig(#[SensitiveParameter] string $apiKey, string $baseUrl): self
59    {
60        return new self(
61            new SchemaGenerator(ComponentsBuilderFactory::createComponentsBuilderFactory()),
62            Serializer::create(),
63            AiClient::create(
64                HttpClient::create([
65                    'max_redirects' => 7,
66                ]),
67                $baseUrl,
68                $apiKey
69            )
70        );
71    }
72
73    public static function createForOllama(string $baseUrl = 'http://localhost:11434/'): self
74    {
75        return self::createForCustomConfig(
76            'IGNORED',
77            $baseUrl,
78        );
79    }
80
81    public static function createForOpenAi(#[SensitiveParameter] string $apiSecret): self
82    {
83        return self::createForCustomConfig(
84            $apiSecret,
85            'https://api.openai.com/v1',
86        );
87    }
88}