Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.35% |
41 / 43 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
AiInstructor | |
95.35% |
41 / 43 |
|
83.33% |
5 / 6 |
9 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAiClient | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
instruct | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
4.01 | |||
createForCustomConfig | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
createForOllama | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
createForOpenAi | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | namespace Apie\AiInstructor; |
3 | |
4 | use Apie\Core\Context\ApieContext; |
5 | use Apie\Core\ValueObjects\NonEmptyString; |
6 | use Apie\SchemaGenerator\ComponentsBuilderFactory; |
7 | use Apie\SchemaGenerator\SchemaGenerator; |
8 | use Apie\Serializer\Serializer; |
9 | use Apie\TypeConverter\ReflectionTypeFactory; |
10 | use ReflectionNamedType; |
11 | use ReflectionUnionType; |
12 | use SensitiveParameter; |
13 | use Symfony\Component\HttpClient\HttpClient; |
14 | |
15 | final 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 | } |