|null */ private static ?array $configCache = null; /** * Create a response from the configured AI service. * * @param array $params Request body (model, input, etc.). * @param array $options Extra options. * @return array */ public static function createResponse(array $params, array $options = []): array { $cfg = self::config(); $payload = $params; if ($cfg['is_custom']) { // Direct OpenAI API call if (empty($payload['input']) || !is_array($payload['input'])) { return [ 'success' => false, 'error' => 'input_missing', 'message' => 'Parameter "input" is required for custom AI call.', ]; } // OpenAI uses 'messages', not 'input' $openAiPayload = [ 'model' => $payload['model'] ?? $cfg['default_model'], 'messages' => $payload['input'], ]; return self::request(null, $openAiPayload, $options); } else { // Flatlogic Proxy call if (empty($payload['input']) || !is_array($payload['input'])) { return [ 'success' => false, 'error' => 'input_missing', 'message' => 'Parameter "input" is required and must be an array.', ]; } if (!isset($payload['model']) || $payload['model'] === '') { $payload['model'] = $cfg['default_model']; } $initial = self::request($options['path'] ?? null, $payload, $options); if (empty($initial['success'])) { return $initial; } // Async flow for Flatlogic proxy $data = $initial['data'] ?? null; if (is_array($data) && isset($data['ai_request_id'])) { $aiRequestId = $data['ai_request_id']; $pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; $pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; return self::awaitResponse($aiRequestId, [ 'timeout' => $pollTimeout, 'interval' => $pollInterval, 'headers' => $options['headers'] ?? [], 'timeout_per_call' => $options['timeout'] ?? null, ]); } return $initial; } } /** * Perform a raw request. * * @param string|null $path Endpoint path. * @param array $payload JSON payload. * @param array $options Additional options. * @return array */ public static function request(?string $path = null, array $payload = [], array $options = []): array { $cfg = self::config(); if (!$cfg['is_custom']) { $projectUuid = $cfg['project_uuid']; if (empty($projectUuid)) { return [ 'success' => false, 'error' => 'project_uuid_missing', 'message' => 'PROJECT_UUID is not defined; aborting AI request.', ]; } } $defaultPath = $cfg['responses_path'] ?? null; $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); if (empty($resolvedPath)) { return [ 'success' => false, 'error' => 'path_missing', 'message' => 'Cannot resolve AI endpoint path.', ]; } $url = self::buildUrl($resolvedPath, $cfg['base_url']); $timeout = (int)($options['timeout'] ?? $cfg['timeout'] ?? 30); $verifyTls = (bool)($options['verify_tls'] ?? $cfg['verify_tls'] ?? true); $headers = $cfg['headers'] ?? []; if (!empty($options['headers']) && is_array($options['headers'])) { $headers = array_merge($headers, $options['headers']); } if (!$cfg['is_custom']) { if (!empty($cfg['project_uuid']) && !array_key_exists('project_uuid', $payload)) { $payload['project_uuid'] = $cfg['project_uuid']; } } $body = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($body === false) { return ['success' => false, 'error' => 'json_encode_failed']; } return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls); } /** * Extract plain text from an AI response. * * @param array $response * @return string */ public static function extractText(array $response): string { $payload = $response['data'] ?? $response; if (!is_array($payload)) { return ''; } // OpenAI API format if (!empty($payload['choices'][0]['message']['content'])) { return (string) $payload['choices'][0]['message']['content']; } // Flatlogic proxy format if (!empty($payload['output']) && is_array($payload['output'])) { $combined = ''; foreach ($payload['output'] as $item) { if (!isset($item['content']) || !is_array($item['content'])) continue; foreach ($item['content'] as $block) { if (($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { $combined .= $block['text']; } } } if ($combined !== '') return $combined; } return ''; } // --- Helper methods below are mostly unchanged --- /** * Load configuration. * @return array */ private static function config(): array { if (self::$configCache === null) { $configPath = __DIR__ . '/config.php'; if (!file_exists($configPath)) throw new RuntimeException('AI config not found'); self::$configCache = require $configPath; } return self::$configCache; } /** * Shared cURL sender. * @return array */ private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array { if (!function_exists('curl_init')) { return ['success' => false, 'error' => 'curl_missing', 'message' => 'PHP cURL extension is missing.']; } $ch = curl_init($url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); if (strtoupper($method) === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? ''); } $responseBody = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($responseBody === false) { $error = curl_error($ch); curl_close($ch); return ['success' => false, 'error' => 'curl_error', 'message' => $error]; } curl_close($ch); $decoded = json_decode($responseBody, true); if ($status >= 200 && $status < 300) { return ['success' => true, 'status' => $status, 'data' => $decoded ?? $responseBody]; } $errorMessage = 'Request failed'; if (is_array($decoded) && isset($decoded['error']['message'])) { $errorMessage = $decoded['error']['message']; } elseif(is_string($responseBody)) { $errorMessage = $responseBody; } return [ 'success' => false, 'status' => $status, 'error' => $errorMessage, 'response' => $decoded ?? $responseBody ]; } /** * Build an absolute URL. */ private static function buildUrl(string $path, string $baseUrl): string { if (str_starts_with($path, 'http')) return $path; return rtrim($baseUrl, '/') . '/' . ltrim($path, '/'); } // --- Methods for Flatlogic proxy async flow --- public static function awaitResponse($aiRequestId, array $options = []): array { $deadline = time() + (int)($options['timeout'] ?? 300); $interval = (int)($options['interval'] ?? 5); while (time() < $deadline) { $statusResp = self::fetchStatus($aiRequestId, $options); if (!($statusResp['success'] ?? false)) return $statusResp; $data = $statusResp['data'] ?? []; $statusValue = $data['status'] ?? null; if ($statusValue === 'success') { return ['success' => true, 'status' => 200, 'data' => $data['response'] ?? $data]; } if ($statusValue === 'failed') { return ['success' => false, 'error' => 'AI request failed', 'data' => $data]; } sleep($interval); } return ['success' => false, 'error' => 'timeout']; } public static function fetchStatus($aiRequestId, array $options = []): array { $cfg = self::config(); if (empty($cfg['project_uuid'])) { return ['success' => false, 'error' => 'project_uuid_missing']; } $statusPath = self::resolveStatusPath($aiRequestId, $cfg); $url = self::buildUrl($statusPath, $cfg['base_url']); $headers = $options['headers'] ?? []; $headers[] = 'Accept: application/json'; $headers[] = $cfg['project_header'] . ': ' . $cfg['project_uuid']; return self::sendCurl($url, 'GET', null, $headers, (int)($options['timeout'] ?? 30), true); } private static function resolveStatusPath($aiRequestId, array $cfg): string { $basePath = rtrim($cfg['responses_path'] ?? '', '/'); return $basePath . '/' . rawurlencode((string)$aiRequestId) . '/status'; } /** * Snake_case alias for createResponse. * @deprecated */ public static function create_response(array $params, array $options = []): array { return self::createResponse($params, $options); } /** * Decode JSON from response text. */ public static function decodeJsonFromResponse(array $response): ?array { $text = self::extractText($response); if ($text === '') return null; $decoded = json_decode($text, true); if(is_array($decoded)) return $decoded; // Strip markdown fences $stripped = preg_replace('/^```json\n?|\n?```$/', '', $text); if ($stripped !== $text) { $decoded = json_decode($stripped, true); if(is_array($decoded)) return $decoded; } return null; } } // Legacy alias if (!class_exists('OpenAIService')) { class_alias(LocalAIApi::class, 'OpenAIService'); }