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 | } |