Files
api/services/nginx/app/classes/openai.php
T
Jeppe B 6d888a455d Automate XL-Vask invoice-period resolution (#340)
Deploy the revision-aware XL-Vask import and guarded autopilot infrastructure. Automatic actions remain fail-closed pending production readiness, calibration, dry-run, and canary gates.
2026-08-03 15:33:55 +02:00

310 lines
13 KiB
PHP

<?php
namespace classes;
require_once WD . '/modules/openAI/openAI_c.php';
use Exception;
use interfaces\openai_i;
use openAI\openAI_c;
class openai implements openai_i
{
/**
* The configuration of the module
* @var openAI_c
*/
public openAI_c $config;
/**
* API URL
* @var string
*/
private string $api_url = 'https://api.openai.com/v1/responses';
protected string $model = 'gpt-4.1-mini';
protected string $temperature = '0.7';
protected string $max_tokens = '1000';
public string $prompt_LPR = 'You are an AI assistant specialized in reading license plates. Your task is to extract the license plate number from the provided image. The license plate number can be in various formats, including letters and numbers. Please provide the license plate number in a clear and concise format.';
public function __construct()
{
$this->config = new openAI_c();
}
/**
* @inheritDoc
*/
public function requireModuleEnabled(): void
{
if (!$this->config->enabled->isTrue()) {
throw new Exception('OpenAI module is not enabled.');
}
}
/**
* Send a structured JSON text task to the OpenAI Responses API.
*
* @throws Exception
*/
public function jsonTask(
string $schemaName,
string $prompt,
array $payload,
array $schema,
float $temperature = 0.1,
?string $model = null
): array
{
$this->requireModuleEnabled();
$data = [
'model' => $model ?? $this->model,
// The caller owns the durable audit record. Do not retain application state at OpenAI.
'store' => false,
'input' => [
[
'role' => 'user',
'content' => [
[
'type' => 'input_text',
'text' => $prompt . "\n\nData:\n" . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
],
],
],
],
'text' => [
'format' => [
'type' => 'json_schema',
'name' => $schemaName,
'schema' => $schema,
'strict' => true,
],
],
'temperature' => $temperature,
];
$response = $this->sendRequest($data);
$output = $response['output'][0]['content'][0]['text'] ?? null;
if (!is_string($output) || $output === '') {
throw new Exception('Invalid response format from OpenAI API. (Missing text field)');
}
$decoded = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
throw new Exception('Error parsing JSON response: ' . json_last_error_msg());
}
return $decoded;
}
protected function getLPRSchema(): array
{
return [
/**
* "format": {
* "type": "json_schema",
* "name": "math_reasoning",
* "schema": {
* "type": "object",
* "properties": {
* "steps": {
* "type": "array",
* "items": {
* "type": "object",
* "properties": {
* "explanation": { "type": "string" },
* "output": { "type": "string" }
* },
* "required": ["explanation", "output"],
* "additionalProperties": false
* }
* },
* "final_answer": { "type": "string" }
* },
* "required": ["steps", "final_answer"],
* "additionalProperties": false
* },
* "strict": true
* }
*/
'format' => [
'type' => 'json_schema',
'name' => 'lpr',
'schema' => [
'type' => 'object',
'properties' => [
'success' => [
'type' => 'boolean',
'description' => 'Indicates whether the license plate number was successfully extracted.'
],
'license_plate_number' => [
'type' => 'string',
'description' => 'The extracted license plate number from the image.'
],
],
'required' => ['license_plate_number', 'success'],
'additionalProperties' => false
],
'strict' => true
]
];
}
/**
* @inheritDoc
* @return array The extracted license plate number (If there is a valid response)
* @throws Exception If the API response cannot be parsed
* @throws Exception If the image base64 is not valid.
* @throws Exception If the module is not enabled or if there is an error in the API request
*/
public function lpr(string $base64_image): array
{
$this->requireModuleEnabled();
if (empty($base64_image)) {
throw new Exception('Image URL cannot be empty.');
}
$prompt = $this->prompt_LPR;
$data = [
'model' => $this->model,
'input' => [
[
'role' => 'user',
'content' => [
['type' => 'input_text', 'text' => $prompt],
[
'type' => 'input_image',
'image_url' => $this->generateTemporaryDirectDownloadLink($base64_image) // Generate a temporary direct download link for the base64 image
]
]
]
],
'text' => [
...self::getLPRSchema(), // Apply the LPR schema { success, license_plate_number }
],
'temperature' => (float)$this->temperature,
];
// Send the request to the OpenAI API
$response = $this->sendRequest($data);
// Check if the response contains the expected format
return $this->parseLPRResponse($response);
}
/**
* Generates a temporary direct download link for the image.
*
* @param string $image The image object key name
* @return string The temporary direct download link for the image.
*/
private function generateTemporaryDirectDownloadLink(string $image): string
{
$uploads = new upload_store();
// Store the image temporarily and return the direct download link
return $uploads->generateDirectDownloadUrl($image);
}
/**
* Parses the response from the OpenAI API for the LPR task.
*
* @param mixed $response The response object from the OpenAI API.
* @return array The extracted license plate number if successful, otherwise an empty string.
* @throws Exception If the response does not contain the expected format or if parsing fails.
*/
private function parseLPRResponse(mixed $response): array
{
/**
* {"success":true,"data":{"id":"resp_68a2e2d5eb608191a546ef0540797d3b0148dcae28786ad6","object":"response","created_at":1755505366,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-mini-2025-04-14","output":[{"id":"msg_68a2e2d91050819183cc2c1d9be252950148dcae28786ad6","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"success\":true,\"license_plate_number\":\"CU 61 297\"}"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":0.7,"text":{"format":{"type":"json_schema","description":null,"name":"lpr","schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates whether the license plate number was successfully extracted."},"license_plate_number":{"type":"string","description":"The extracted license plate number from the image."}},"required":["license_plate_number","success"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":{"input_tokens":2470,"input_tokens_details":{"cached_tokens":0},"output_tokens":16,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":2486},"user":null,"metadata":[]},"meta":[],"includes":[]}
*/
// Check if the response contains the expected format
if (!isset($response['output'][0]['content'][0]['text'])) {
throw new Exception('Invalid response format from OpenAI API. (Missing text field)');
}
$output = $response['output'][0]['content'][0]['text'];
// Decode the JSON response
$decodedOutput = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Error parsing JSON response: ' . json_last_error_msg());
}
// Check if the response contains the expected fields
if (!isset($decodedOutput['success']) || !isset($decodedOutput['license_plate_number'])) {
throw new Exception('Invalid response format from OpenAI API. (Missing success or license_plate_number field)');
}
// Return the license plate number if successful
if ($decodedOutput['success']) {
// Remove any spaces or unwanted characters from the license plate number
return [
'success' => $decodedOutput['success'],
'license_plate_number' => $this->cleanLicensePlate($decodedOutput['license_plate_number']),
];
// return $decodedOutput['license_plate_number']);
} else {
throw new Exception('License plate extraction failed.');
}
}
/**
* Cleans the license plate number by removing unwanted characters and spaces.
*
* @param string $license_plate The license plate number to clean.
* @return string The cleaned license plate number.
*/
private function cleanLicensePlate(string $license_plate): string
{
// Remove any unwanted characters (e.g., spaces, special characters)
$cleaned = preg_replace('/[^A-Za-z0-9]/', '', $license_plate);
// Return the cleaned license plate number
return strtoupper(trim($cleaned)); // Convert to uppercase and trim any whitespace
}
/**
* Sends a request to the OpenAI API and returns the response.
*
* @param array $data The data to send in the request.
* @return array The response from the API, typically an associative array containing the response data.
* @throws Exception If the API request fails or if the response cannot be parsed.
*/
private function sendRequest(array $data): array
{
$this->requireModuleEnabled();
/**
* curl https://api.openai.com/v1/responses \
* -H "Content-Type: application/json" \
* -H "Authorization: Bearer $OPENAI_API_KEY" \
* -d '{
* "model": "gpt-4.1-mini",
* "input": [
* {
* "role": "user",
* "content": [
* {"type": "input_text", "text": "what is in this image?"},
* {
* "type": "input_image",
* "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
* }
* ]
* }
* ]
* }'
*/
$curl = curl_init($this->api_url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($curl, CURLOPT_TIMEOUT, 45);
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->config->api_key->getVariableValue()
]);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($curl);
if (curl_errno($curl)) {
throw new Exception('cURL error: ' . curl_error($curl));
}
curl_close($curl);
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Error parsing JSON response: ' . json_last_error_msg());
}
//print_r($responseData);
return $responseData;
}
}