Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractAnthropicGenerator
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
6 / 6
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPromptData
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 createPayload
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 formatContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 ensureExpectedKeys
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 sendApiRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getExpectedKeys
n/a
0 / 0
n/a
0 / 0
0
 getLoggerName
n/a
0 / 0
n/a
0 / 0
0
1<?php
2declare(strict_types=1);
3
4namespace App\Service\Api\Anthropic;
5
6use App\Model\Table\AipromptsTable;
7use App\Service\Api\AiProviderInterface;
8use Cake\Log\LogTrait;
9use InvalidArgumentException;
10
11/**
12 * AbstractAnthropicGenerator
13 *
14 * Base class for all Anthropic API generators providing common functionality
15 * for prompt retrieval, payload creation, and response validation.
16 */
17abstract class AbstractAnthropicGenerator
18{
19    use LogTrait;
20
21    /**
22     * The AI provider service used for sending requests and parsing responses.
23     *
24     * @var \App\Service\Api\AiProviderInterface
25     */
26    protected AiProviderInterface $apiService;
27
28    /**
29     * The AI prompts table for retrieving prompt data necessary for generation.
30     *
31     * @var \App\Model\Table\AipromptsTable
32     */
33    protected AipromptsTable $aipromptsTable;
34
35    /**
36     * AbstractAnthropicGenerator constructor.
37     *
38     * Initializes the API service and AI prompts table.
39     *
40     * @param \App\Service\Api\AiProviderInterface $apiService The AI provider service.
41     * @param \App\Model\Table\AipromptsTable $aipromptsTable The AI prompts table.
42     */
43    public function __construct(AiProviderInterface $apiService, AipromptsTable $aipromptsTable)
44    {
45        $this->apiService = $apiService;
46        $this->aipromptsTable = $aipromptsTable;
47    }
48
49    /**
50     * Retrieves prompt data for a specific task from the AI prompts table.
51     *
52     * The model returned depends on the configured provider:
53     * - For 'openrouter': uses the openrouter_model field
54     * - For 'anthropic' (or any other): uses the model field
55     *
56     * @param string $task The task type for which to retrieve prompt data.
57     * @return array The retrieved prompt data.
58     * @throws \InvalidArgumentException If the task is unknown or not found.
59     */
60    protected function getPromptData(string $task): array
61    {
62        $prompt = $this->aipromptsTable->find()
63            ->where(['task_type' => $task])
64            ->first();
65
66        if (!$prompt) {
67            throw new InvalidArgumentException("Unknown task: {$task}");
68        }
69
70        // Select the appropriate model based on the provider
71        $providerName = $this->apiService->getProviderName();
72        $model = $providerName === 'openrouter' && !empty($prompt->openrouter_model)
73            ? $prompt->openrouter_model
74            : $prompt->model;
75
76        return [
77            'system_prompt' => $prompt->system_prompt,
78            'model' => $model,
79            'max_tokens' => $prompt->max_tokens,
80            'temperature' => $prompt->temperature,
81        ];
82    }
83
84    /**
85     * Creates a basic payload for the API request using the provided prompt data and content.
86     *
87     * This method handles the standard payload structure. Child classes can override
88     * this method for specialized payload requirements (e.g., image data).
89     *
90     * @param array $promptData The prompt data retrieved from the AI prompts table.
91     * @param mixed $content The content to be included in the payload.
92     * @return array The created payload for the API request.
93     */
94    protected function createPayload(array $promptData, mixed $content): array
95    {
96        return [
97            'model' => $promptData['model'],
98            'max_tokens' => $promptData['max_tokens'],
99            'temperature' => $promptData['temperature'],
100            'system' => $promptData['system_prompt'],
101            'messages' => [
102                [
103                    'role' => 'user',
104                    'content' => $this->formatContent($content),
105                ],
106            ],
107        ];
108    }
109
110    /**
111     * Formats content for inclusion in the API payload.
112     *
113     * Most generators use JSON encoding for arrays, plain strings for text.
114     * Child classes can override for specialized formatting.
115     *
116     * @param mixed $content The content to format.
117     * @return mixed The formatted content.
118     */
119    protected function formatContent(mixed $content): mixed
120    {
121        return is_array($content) ? json_encode($content) : $content;
122    }
123
124    /**
125     * Ensures that the result contains all expected keys, initializing them if necessary.
126     *
127     * @param array $result The result array to check and modify.
128     * @param array|null $expectedKeys Optional array of expected keys. If null, uses getExpectedKeys().
129     * @return array The result array with all expected keys initialized.
130     */
131    protected function ensureExpectedKeys(array $result, ?array $expectedKeys = null): array
132    {
133        $keys = $expectedKeys ?? $this->getExpectedKeys();
134
135        foreach ($keys as $key) {
136            if (!isset($result[$key])) {
137                $result[$key] = '';
138                $this->log(
139                    sprintf('%s did not find expected key: %s', $this->getLoggerName(), $key),
140                    'error',
141                    ['group_name' => 'anthropic'],
142                );
143            }
144        }
145
146        return $result;
147    }
148
149    /**
150     * Sends a request to the API with optional timeout override.
151     *
152     * @param array $payload The payload to send.
153     * @param int|null $timeout Optional timeout override.
154     * @return array The parsed response.
155     */
156    protected function sendApiRequest(array $payload, ?int $timeout = null): array
157    {
158        $response = $timeout !== null
159            ? $this->apiService->sendRequest($payload, $timeout)
160            : $this->apiService->sendRequest($payload);
161
162        return $this->apiService->parseResponse($response);
163    }
164
165    /**
166     * Gets the expected keys for the API response.
167     * Each generator must define what keys it expects from the API.
168     *
169     * @return array Array of expected response keys.
170     */
171    abstract protected function getExpectedKeys(): array;
172
173    /**
174     * Gets the logger name for this generator.
175     * Used in error logging to identify which generator logged the message.
176     *
177     * @return string The logger name.
178     */
179    abstract protected function getLoggerName(): string;
180}