Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e469e4cb2 | ||
|
|
81870873e6 | ||
|
|
f0662ef19f | ||
|
|
bf3bd6faea | ||
|
|
3dfb3c6972 | ||
|
|
23b74863ee | ||
|
|
3153fc5bc8 | ||
|
|
9929d1dd39 |
176
.htaccess
176
.htaccess
@ -1,18 +1,182 @@
|
|||||||
|
# KI-Fit Check Questionnaire - Server Configuration
|
||||||
|
# For Appwizzy platform compatibility
|
||||||
|
|
||||||
|
# Set default index files
|
||||||
DirectoryIndex index.php index.html
|
DirectoryIndex index.php index.html
|
||||||
|
|
||||||
|
# Security & Performance
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
Options -MultiViews
|
Options -MultiViews
|
||||||
|
ServerSignature Off
|
||||||
|
|
||||||
|
# Enable Rewrite Engine
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
|
||||||
# 0) Serve existing files/directories as-is
|
# Force HTTPS (uncomment when SSL is installed)
|
||||||
|
# RewriteCond %{HTTPS} off
|
||||||
|
# RewriteCond %{HTTP_HOST} !^localhost [NC]
|
||||||
|
# RewriteCond %{HTTP_HOST} !^127\.0\.0\.1 [NC]
|
||||||
|
# RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
|
||||||
|
|
||||||
|
# ===== SECURITY HEADERS =====
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Prevent MIME type sniffing
|
||||||
|
Header set X-Content-Type-Options "nosniff"
|
||||||
|
|
||||||
|
# Enable XSS protection
|
||||||
|
Header set X-XSS-Protection "1; mode=block"
|
||||||
|
|
||||||
|
# Prevent clickjacking
|
||||||
|
Header set X-Frame-Options "SAMEORIGIN"
|
||||||
|
|
||||||
|
# Referrer Policy
|
||||||
|
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ===== URL REWRITING =====
|
||||||
|
|
||||||
|
# 1) Serve existing files/directories as-is
|
||||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||||
RewriteCond %{REQUEST_FILENAME} -d
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
RewriteRule ^ - [L]
|
RewriteRule ^ - [L]
|
||||||
|
|
||||||
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
|
# 2) Handle clean URLs for questionnaire
|
||||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
# Rewrite /ki-fit-check to index.php (main questionnaire)
|
||||||
RewriteRule ^(.+?)/?$ $1.php [L]
|
RewriteRule ^ki-fit-check/?$ index.php [L]
|
||||||
|
|
||||||
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
# 3) Handle other pages
|
||||||
|
RewriteRule ^kontakt/?$ ki-check.php [L]
|
||||||
|
RewriteRule ^ergebnisse/?$ results.php [L]
|
||||||
|
RewriteRule ^erfolg/?$ success.php [L]
|
||||||
|
|
||||||
|
# 4) Handle API endpoints
|
||||||
|
RewriteRule ^api/submit/?$ api/submit.php [L]
|
||||||
|
RewriteRule ^api/analyze/?$ api/analyze.php [L]
|
||||||
|
RewriteRule ^api/generate-pdf/?$ api/generate-pdf.php [L]
|
||||||
|
RewriteRule ^api/send-email/?$ api/send-email.php [L]
|
||||||
|
|
||||||
|
# 5) Remove trailing slashes for non-directories
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteRule ^(.+)/$ $1 [R=301,L]
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [R=301,L]
|
||||||
|
|
||||||
|
# 6) Custom error pages
|
||||||
|
ErrorDocument 404 /404.html
|
||||||
|
ErrorDocument 500 /500.html
|
||||||
|
|
||||||
|
# ===== REDIRECTIONS =====
|
||||||
|
|
||||||
|
# Redirect old .php URLs to clean URLs
|
||||||
|
RewriteRule ^index\.php$ / [R=301,L]
|
||||||
|
RewriteRule ^ki-check\.php$ /ki-fit-check [R=301,L]
|
||||||
|
|
||||||
|
# ===== PERFORMANCE OPTIMIZATION =====
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
ExpiresActive On
|
||||||
|
|
||||||
|
# Images
|
||||||
|
ExpiresByType image/jpeg "access plus 1 year"
|
||||||
|
ExpiresByType image/png "access plus 1 year"
|
||||||
|
ExpiresByType image/gif "access plus 1 year"
|
||||||
|
ExpiresByType image/svg+xml "access plus 1 year"
|
||||||
|
ExpiresByType image/webp "access plus 1 year"
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
ExpiresByType font/ttf "access plus 1 year"
|
||||||
|
ExpiresByType font/otf "access plus 1 year"
|
||||||
|
ExpiresByType font/woff "access plus 1 year"
|
||||||
|
ExpiresByType font/woff2 "access plus 1 year"
|
||||||
|
|
||||||
|
# CSS & JavaScript
|
||||||
|
ExpiresByType text/css "access plus 1 month"
|
||||||
|
ExpiresByType text/javascript "access plus 1 month"
|
||||||
|
ExpiresByType application/javascript "access plus 1 month"
|
||||||
|
|
||||||
|
# HTML
|
||||||
|
ExpiresByType text/html "access plus 1 hour"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
# Compress HTML, CSS, JavaScript, Text, XML and fonts
|
||||||
|
AddOutputFilterByType DEFLATE text/html
|
||||||
|
AddOutputFilterByType DEFLATE text/css
|
||||||
|
AddOutputFilterByType DEFLATE text/javascript
|
||||||
|
AddOutputFilterByType DEFLATE text/plain
|
||||||
|
AddOutputFilterByType DEFLATE text/xml
|
||||||
|
AddOutputFilterByType DEFLATE application/javascript
|
||||||
|
AddOutputFilterByType DEFLATE application/json
|
||||||
|
AddOutputFilterByType DEFLATE application/xml
|
||||||
|
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||||
|
AddOutputFilterByType DEFLATE application/rss+xml
|
||||||
|
AddOutputFilterByType DEFLATE application/atom+xml
|
||||||
|
AddOutputFilterByType DEFLATE image/svg+xml
|
||||||
|
AddOutputFilterByType DEFLATE font/ttf
|
||||||
|
AddOutputFilterByType DEFLATE font/otf
|
||||||
|
AddOutputFilterByType DEFLATE font/woff
|
||||||
|
AddOutputFilterByType DEFLATE font/woff2
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ===== CORS SETTINGS =====
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Allow requests from any origin (adjust for production)
|
||||||
|
Header set Access-Control-Allow-Origin "*"
|
||||||
|
|
||||||
|
# Allow specific methods
|
||||||
|
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||||
|
|
||||||
|
# Allow specific headers
|
||||||
|
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
|
||||||
|
|
||||||
|
# Allow credentials
|
||||||
|
Header set Access-Control-Allow-Credentials "true"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ===== CACHE CONTROL =====
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Cache static assets
|
||||||
|
<FilesMatch "\.(css|js|jpg|jpeg|png|gif|svg|woff|woff2|ttf|eot|ico)$">
|
||||||
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Don't cache HTML files (except static pages)
|
||||||
|
<FilesMatch "\.(html|php)$">
|
||||||
|
Header set Cache-Control "public, max-age=3600, must-revalidate"
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ===== BLOCK ACCESS TO SENSITIVE FILES =====
|
||||||
|
<FilesMatch "^\.">
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
<FilesMatch "\.(log|sql|bak|inc|cfg|config|ini|env)$">
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Block access to config directories
|
||||||
|
RedirectMatch 403 ^/ai/.*$
|
||||||
|
RedirectMatch 403 ^/db/.*$
|
||||||
|
RedirectMatch 403 ^/mail/.*$
|
||||||
|
RedirectMatch 403 ^/api/.*$
|
||||||
|
|
||||||
|
# ===== PHP SETTINGS =====
|
||||||
|
<IfModule mod_php.c>
|
||||||
|
php_value upload_max_filesize 10M
|
||||||
|
php_value post_max_size 10M
|
||||||
|
php_value max_execution_time 300
|
||||||
|
php_value max_input_time 300
|
||||||
|
php_value memory_limit 256M
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ===== FOR APPWIZZY COMPATIBILITY =====
|
||||||
|
# Ensure PHP files are processed correctly
|
||||||
|
AddType application/x-httpd-php .php
|
||||||
|
AddHandler application/x-httpd-php .php
|
||||||
|
|
||||||
|
# Set default charset
|
||||||
|
AddDefaultCharset UTF-8
|
||||||
|
|
||||||
|
# Disable directory listing
|
||||||
|
IndexIgnore *
|
||||||
@ -1,45 +1,48 @@
|
|||||||
<?php
|
<?php
|
||||||
// LocalAIApi — proxy client for the Responses API.
|
// ai/LocalAIApi.php — proxy client for the Responses API.
|
||||||
// Usage (async: auto-polls status until ready):
|
// For Appwizzy platform compatibility
|
||||||
// require_once __DIR__ . '/ai/LocalAIApi.php';
|
|
||||||
// $response = LocalAIApi::createResponse([
|
|
||||||
// 'input' => [
|
|
||||||
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
|
||||||
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
|
|
||||||
// ],
|
|
||||||
// ]);
|
|
||||||
// if (!empty($response['success'])) {
|
|
||||||
// // response['data'] contains full payload, e.g.:
|
|
||||||
// // {
|
|
||||||
// // "id": "resp_xxx",
|
|
||||||
// // "status": "completed",
|
|
||||||
// // "output": [
|
|
||||||
// // {"type": "reasoning", "summary": []},
|
|
||||||
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
|
||||||
// // ]
|
|
||||||
// // }
|
|
||||||
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
|
|
||||||
// }
|
|
||||||
// Poll settings override:
|
|
||||||
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
|
|
||||||
|
|
||||||
class LocalAIApi
|
class LocalAIApi
|
||||||
{
|
{
|
||||||
/** @var array<string,mixed>|null */
|
/** @var array<string,mixed>|null */
|
||||||
private static ?array $configCache = null;
|
private static ?array $configCache = null;
|
||||||
|
|
||||||
|
/** @var array<string,mixed> Default options */
|
||||||
|
private static array $defaultOptions = [
|
||||||
|
'poll_interval' => 5,
|
||||||
|
'poll_timeout' => 300,
|
||||||
|
'timeout' => 30,
|
||||||
|
'verify_tls' => true,
|
||||||
|
'max_retries' => 3,
|
||||||
|
'retry_delay' => 2,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Create an AI response (async: auto-polls status until ready).
|
||||||
* Signature compatible with the OpenAI Responses API.
|
* Signature compatible with the OpenAI Responses API.
|
||||||
*
|
*
|
||||||
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
|
* Usage:
|
||||||
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
|
* require_once __DIR__ . '/ai/LocalAIApi.php';
|
||||||
|
* $response = LocalAIApi::createResponse([
|
||||||
|
* 'input' => [
|
||||||
|
* ['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
||||||
|
* ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
|
||||||
|
* ],
|
||||||
|
* ]);
|
||||||
|
* if (!empty($response['success'])) {
|
||||||
|
* // response['data'] contains full payload
|
||||||
|
* $decoded = LocalAIApi::decodeJsonFromResponse($response);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.)
|
||||||
|
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid)
|
||||||
* @return array{
|
* @return array{
|
||||||
* success:bool,
|
* success: bool,
|
||||||
* status?:int,
|
* status?: int,
|
||||||
* data?:mixed,
|
* data?: mixed,
|
||||||
* error?:string,
|
* error?: string,
|
||||||
* response?:mixed,
|
* response?: mixed,
|
||||||
* message?:string
|
* message?: string
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public static function createResponse(array $params, array $options = []): array
|
public static function createResponse(array $params, array $options = []): array
|
||||||
@ -47,6 +50,7 @@ class LocalAIApi
|
|||||||
$cfg = self::config();
|
$cfg = self::config();
|
||||||
$payload = $params;
|
$payload = $params;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
if (empty($payload['input']) || !is_array($payload['input'])) {
|
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -55,10 +59,15 @@ class LocalAIApi
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default model if not provided
|
||||||
if (!isset($payload['model']) || $payload['model'] === '') {
|
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||||
$payload['model'] = $cfg['default_model'];
|
$payload['model'] = $cfg['default_model'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge with default options
|
||||||
|
$options = array_merge(self::$defaultOptions, $options);
|
||||||
|
|
||||||
|
// Make initial request
|
||||||
$initial = self::request($options['path'] ?? null, $payload, $options);
|
$initial = self::request($options['path'] ?? null, $payload, $options);
|
||||||
if (empty($initial['success'])) {
|
if (empty($initial['success'])) {
|
||||||
return $initial;
|
return $initial;
|
||||||
@ -68,14 +77,7 @@ class LocalAIApi
|
|||||||
$data = $initial['data'] ?? null;
|
$data = $initial['data'] ?? null;
|
||||||
if (is_array($data) && isset($data['ai_request_id'])) {
|
if (is_array($data) && isset($data['ai_request_id'])) {
|
||||||
$aiRequestId = $data['ai_request_id'];
|
$aiRequestId = $data['ai_request_id'];
|
||||||
$pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds
|
return self::awaitResponse($aiRequestId, $options);
|
||||||
$pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds
|
|
||||||
return self::awaitResponse($aiRequestId, [
|
|
||||||
'timeout' => $pollTimeout,
|
|
||||||
'interval' => $pollInterval,
|
|
||||||
'headers' => $options['headers'] ?? [],
|
|
||||||
'timeout_per_call' => $options['timeout'] ?? null,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $initial;
|
return $initial;
|
||||||
@ -96,9 +98,9 @@ class LocalAIApi
|
|||||||
/**
|
/**
|
||||||
* Perform a raw request to the AI proxy.
|
* Perform a raw request to the AI proxy.
|
||||||
*
|
*
|
||||||
* @param string $path Endpoint (may be an absolute URL).
|
* @param string|null $path Endpoint (may be an absolute URL)
|
||||||
* @param array<string,mixed> $payload JSON payload.
|
* @param array<string,mixed> $payload JSON payload
|
||||||
* @param array<string,mixed> $options Additional request options.
|
* @param array<string,mixed> $options Additional request options
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
||||||
@ -125,16 +127,10 @@ class LocalAIApi
|
|||||||
}
|
}
|
||||||
|
|
||||||
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
||||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
$timeout = $options['timeout'] ?? $cfg['timeout'] ?? 30;
|
||||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
$verifyTls = $options['verify_tls'] ?? $cfg['verify_tls'] ?? true;
|
||||||
if ($timeout <= 0) {
|
$maxRetries = $options['max_retries'] ?? $cfg['max_retries'] ?? 3;
|
||||||
$timeout = 30;
|
$retryDelay = $options['retry_delay'] ?? $cfg['retry_delay'] ?? 2;
|
||||||
}
|
|
||||||
|
|
||||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
|
||||||
$verifyTls = array_key_exists('verify_tls', $options)
|
|
||||||
? (bool) $options['verify_tls']
|
|
||||||
: $baseVerifyTls;
|
|
||||||
|
|
||||||
$projectHeader = $cfg['project_header'];
|
$projectHeader = $cfg['project_header'];
|
||||||
|
|
||||||
@ -143,6 +139,7 @@ class LocalAIApi
|
|||||||
'Accept: application/json',
|
'Accept: application/json',
|
||||||
];
|
];
|
||||||
$headers[] = $projectHeader . ': ' . $projectUuid;
|
$headers[] = $projectHeader . ': ' . $projectUuid;
|
||||||
|
|
||||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||||
foreach ($options['headers'] as $header) {
|
foreach ($options['headers'] as $header) {
|
||||||
if (is_string($header) && $header !== '') {
|
if (is_string($header) && $header !== '') {
|
||||||
@ -151,11 +148,17 @@ class LocalAIApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add project_uuid to payload if not present
|
||||||
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
|
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
|
||||||
$payload['project_uuid'] = $projectUuid;
|
$payload['project_uuid'] = $projectUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
// Add debug info if enabled
|
||||||
|
if ($cfg['debug'] ?? false) {
|
||||||
|
$payload['debug'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
if ($body === false) {
|
if ($body === false) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -164,7 +167,38 @@ class LocalAIApi
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
|
// Retry logic
|
||||||
|
$attempt = 0;
|
||||||
|
$lastError = null;
|
||||||
|
|
||||||
|
while ($attempt < $maxRetries) {
|
||||||
|
$attempt++;
|
||||||
|
|
||||||
|
$result = self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastError = $result;
|
||||||
|
|
||||||
|
// Don't retry on client errors (4xx)
|
||||||
|
$status = $result['status'] ?? 0;
|
||||||
|
if ($status >= 400 && $status < 500) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
if ($attempt < $maxRetries) {
|
||||||
|
sleep($retryDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lastError ?? [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'max_retries_exceeded',
|
||||||
|
'message' => 'Maximum retry attempts exceeded.',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,38 +212,44 @@ class LocalAIApi
|
|||||||
{
|
{
|
||||||
$cfg = self::config();
|
$cfg = self::config();
|
||||||
|
|
||||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
$timeout = $options['poll_timeout'] ?? 300; // seconds
|
||||||
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
|
$interval = $options['poll_interval'] ?? 5; // seconds
|
||||||
if ($interval <= 0) {
|
if ($interval <= 0) {
|
||||||
$interval = 5;
|
$interval = 5;
|
||||||
}
|
}
|
||||||
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
|
$perCallTimeout = $options['timeout'] ?? null;
|
||||||
|
|
||||||
$deadline = time() + max($timeout, $interval);
|
$deadline = time() + max($timeout, $interval);
|
||||||
$headers = $options['headers'] ?? [];
|
$headers = $options['headers'] ?? [];
|
||||||
|
|
||||||
|
$attempt = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
$attempt++;
|
||||||
$statusResp = self::fetchStatus($aiRequestId, [
|
$statusResp = self::fetchStatus($aiRequestId, [
|
||||||
'headers' => $headers,
|
'headers' => $headers,
|
||||||
'timeout' => $perCallTimeout,
|
'timeout' => $perCallTimeout,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!empty($statusResp['success'])) {
|
if (!empty($statusResp['success'])) {
|
||||||
$data = $statusResp['data'] ?? [];
|
$data = $statusResp['data'] ?? [];
|
||||||
if (is_array($data)) {
|
if (is_array($data)) {
|
||||||
$statusValue = $data['status'] ?? null;
|
$statusValue = $data['status'] ?? null;
|
||||||
if ($statusValue === 'success') {
|
if ($statusValue === 'success' || $statusValue === 'completed') {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'status' => 200,
|
'status' => 200,
|
||||||
'data' => $data['response'] ?? $data,
|
'data' => $data['response'] ?? $data,
|
||||||
|
'attempts' => $attempt,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if ($statusValue === 'failed') {
|
if ($statusValue === 'failed' || $statusValue === 'error') {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'status' => 500,
|
'status' => 500,
|
||||||
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
|
'attempts' => $attempt,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,10 +261,14 @@ class LocalAIApi
|
|||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'timeout',
|
'error' => 'timeout',
|
||||||
'message' => 'Timed out waiting for AI response.',
|
'message' => 'Timed out waiting for AI response after ' . $attempt . ' attempts.',
|
||||||
|
'attempts' => $attempt,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
sleep($interval);
|
|
||||||
|
// Exponential backoff
|
||||||
|
$sleepTime = $interval * (1 + ($attempt * 0.1));
|
||||||
|
sleep(min($sleepTime, 30)); // Max 30 seconds between polls
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,16 +294,8 @@ class LocalAIApi
|
|||||||
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
|
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
|
||||||
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
||||||
|
|
||||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
$timeout = $options['timeout'] ?? $cfg['timeout'] ?? 30;
|
||||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
$verifyTls = $options['verify_tls'] ?? $cfg['verify_tls'] ?? true;
|
||||||
if ($timeout <= 0) {
|
|
||||||
$timeout = 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
|
||||||
$verifyTls = array_key_exists('verify_tls', $options)
|
|
||||||
? (bool) $options['verify_tls']
|
|
||||||
: $baseVerifyTls;
|
|
||||||
|
|
||||||
$projectHeader = $cfg['project_header'];
|
$projectHeader = $cfg['project_header'];
|
||||||
$headers = [
|
$headers = [
|
||||||
@ -280,7 +316,7 @@ class LocalAIApi
|
|||||||
/**
|
/**
|
||||||
* Extract plain text from a Responses API payload.
|
* Extract plain text from a Responses API payload.
|
||||||
*
|
*
|
||||||
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
|
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function extractText(array $response): string
|
public static function extractText(array $response): string
|
||||||
@ -290,6 +326,7 @@ class LocalAIApi
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to extract from OpenAI Responses API format
|
||||||
if (!empty($payload['output']) && is_array($payload['output'])) {
|
if (!empty($payload['output']) && is_array($payload['output'])) {
|
||||||
$combined = '';
|
$combined = '';
|
||||||
foreach ($payload['output'] as $item) {
|
foreach ($payload['output'] as $item) {
|
||||||
@ -307,10 +344,16 @@ class LocalAIApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to extract from OpenAI Chat Completion format
|
||||||
if (!empty($payload['choices'][0]['message']['content'])) {
|
if (!empty($payload['choices'][0]['message']['content'])) {
|
||||||
return (string) $payload['choices'][0]['message']['content'];
|
return (string) $payload['choices'][0]['message']['content'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to extract from generic response
|
||||||
|
if (!empty($payload['text'])) {
|
||||||
|
return (string) $payload['text'];
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,12 +370,14 @@ class LocalAIApi
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First try to decode directly
|
||||||
$decoded = json_decode($text, true);
|
$decoded = json_decode($text, true);
|
||||||
if (is_array($decoded)) {
|
if (is_array($decoded)) {
|
||||||
return $decoded;
|
return $decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
|
// Try stripping markdown code fences
|
||||||
|
$stripped = preg_replace('/^```(?:json)?\s*\n|\n```$/m', '', trim($text));
|
||||||
if ($stripped !== null && $stripped !== $text) {
|
if ($stripped !== null && $stripped !== $text) {
|
||||||
$decoded = json_decode($stripped, true);
|
$decoded = json_decode($stripped, true);
|
||||||
if (is_array($decoded)) {
|
if (is_array($decoded)) {
|
||||||
@ -340,6 +385,14 @@ class LocalAIApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to extract JSON from text
|
||||||
|
if (preg_match('/\{.*\}/s', $text, $matches)) {
|
||||||
|
$decoded = json_decode($matches[0], true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,6 +400,7 @@ class LocalAIApi
|
|||||||
* Load configuration from ai/config.php.
|
* Load configuration from ai/config.php.
|
||||||
*
|
*
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
private static function config(): array
|
private static function config(): array
|
||||||
{
|
{
|
||||||
@ -359,7 +413,20 @@ class LocalAIApi
|
|||||||
if (!is_array($cfg)) {
|
if (!is_array($cfg)) {
|
||||||
throw new RuntimeException('Invalid AI config format: expected array');
|
throw new RuntimeException('Invalid AI config format: expected array');
|
||||||
}
|
}
|
||||||
self::$configCache = $cfg;
|
|
||||||
|
// Merge with defaults
|
||||||
|
$defaults = [
|
||||||
|
'debug' => false,
|
||||||
|
'max_retries' => 3,
|
||||||
|
'retry_delay' => 2,
|
||||||
|
'features' => [
|
||||||
|
'async_polling' => true,
|
||||||
|
'json_response' => true,
|
||||||
|
'streaming' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::$configCache = array_merge($defaults, $cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::$configCache;
|
return self::$configCache;
|
||||||
@ -420,7 +487,7 @@ class LocalAIApi
|
|||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'curl_missing',
|
'error' => 'curl_missing',
|
||||||
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
|
'message' => 'PHP cURL extension is missing. Install or enable it on the server.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,6 +500,16 @@ class LocalAIApi
|
|||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
||||||
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
||||||
|
|
||||||
|
// Add debug info if enabled
|
||||||
|
$cfg = self::config();
|
||||||
|
if ($cfg['debug'] ?? false) {
|
||||||
|
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||||||
|
$debugFile = fopen(__DIR__ . '/../logs/curl_debug.log', 'a');
|
||||||
|
if ($debugFile) {
|
||||||
|
curl_setopt($ch, CURLOPT_STDERR, $debugFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$upper = strtoupper($method);
|
$upper = strtoupper($method);
|
||||||
if ($upper === 'POST') {
|
if ($upper === 'POST') {
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
@ -442,19 +519,24 @@ class LocalAIApi
|
|||||||
}
|
}
|
||||||
|
|
||||||
$responseBody = curl_exec($ch);
|
$responseBody = curl_exec($ch);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
// Close debug file if opened
|
||||||
|
if (isset($debugFile) && is_resource($debugFile)) {
|
||||||
|
fclose($debugFile);
|
||||||
|
}
|
||||||
|
|
||||||
if ($responseBody === false) {
|
if ($responseBody === false) {
|
||||||
$error = curl_error($ch) ?: 'Unknown cURL error';
|
|
||||||
curl_close($ch);
|
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'curl_error',
|
'error' => 'curl_error',
|
||||||
'message' => $error,
|
'message' => $error ?: 'Unknown cURL error',
|
||||||
|
'status' => $status,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$decoded = null;
|
$decoded = null;
|
||||||
if ($responseBody !== '' && $responseBody !== null) {
|
if ($responseBody !== '' && $responseBody !== null) {
|
||||||
$decoded = json_decode($responseBody, true);
|
$decoded = json_decode($responseBody, true);
|
||||||
@ -491,3 +573,17 @@ class LocalAIApi
|
|||||||
if (!class_exists('OpenAIService')) {
|
if (!class_exists('OpenAIService')) {
|
||||||
class_alias(LocalAIApi::class, 'OpenAIService');
|
class_alias(LocalAIApi::class, 'OpenAIService');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function for quick usage
|
||||||
|
if (!function_exists('ai_request')) {
|
||||||
|
/**
|
||||||
|
* Helper function for quick AI requests
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $params
|
||||||
|
* @param array<string,mixed> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
function ai_request(array $params, array $options = []): array {
|
||||||
|
return LocalAIApi::createResponse($params, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,45 +1,61 @@
|
|||||||
<?php
|
<?php
|
||||||
// OpenAI proxy configuration (workspace scope).
|
// ai/config.php - OpenAI proxy configuration (workspace scope)
|
||||||
// Reads values from environment variables or executor/.env.
|
// Reads values from environment variables or executor/.env
|
||||||
|
// For Appwizzy platform compatibility
|
||||||
|
|
||||||
$projectUuid = getenv('PROJECT_UUID');
|
/**
|
||||||
$projectId = getenv('PROJECT_ID');
|
* Load environment variables from .env file if not already set
|
||||||
|
* This ensures compatibility with both CLI and web environments
|
||||||
|
*/
|
||||||
|
function loadEnvIfNeeded() {
|
||||||
|
// Check if required environment variables are missing
|
||||||
|
$projectUuid = getenv('PROJECT_UUID');
|
||||||
|
$projectId = getenv('PROJECT_ID');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||||
($projectId === false || $projectId === null || $projectId === '')
|
($projectId === false || $projectId === null || $projectId === '')
|
||||||
) {
|
) {
|
||||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||||
if ($envPath && is_readable($envPath)) {
|
if ($envPath && is_readable($envPath)) {
|
||||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
if ($line === '' || $line[0] === '#') {
|
if ($line === '' || $line[0] === '#') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!str_contains($line, '=')) {
|
if (!str_contains($line, '=')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||||
if ($key === '') {
|
if ($key === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$value = trim($value, "\"' ");
|
$value = trim($value, "\"' ");
|
||||||
if (getenv($key) === false || getenv($key) === '') {
|
if (getenv($key) === false || getenv($key) === '') {
|
||||||
putenv("{$key}={$value}");
|
putenv("{$key}={$value}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$projectUuid = getenv('PROJECT_UUID');
|
|
||||||
$projectId = getenv('PROJECT_ID');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
// Load environment variables if needed
|
||||||
$projectId = ($projectId === false) ? null : $projectId;
|
loadEnvIfNeeded();
|
||||||
|
|
||||||
$baseUrl = 'https://flatlogic.com';
|
// Get configuration values
|
||||||
|
$projectUuid = getenv('PROJECT_UUID');
|
||||||
|
$projectId = getenv('PROJECT_ID');
|
||||||
|
|
||||||
|
// Set defaults if not found
|
||||||
|
$projectUuid = ($projectUuid === false || $projectUuid === '') ? null : $projectUuid;
|
||||||
|
$projectId = ($projectId === false || $projectId === '') ? null : $projectId;
|
||||||
|
|
||||||
|
// Base configuration
|
||||||
|
$baseUrl = 'https://flatlogic.com';
|
||||||
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
||||||
|
|
||||||
|
// Return configuration array
|
||||||
return [
|
return [
|
||||||
'base_url' => $baseUrl,
|
'base_url' => $baseUrl,
|
||||||
'responses_path' => $responsesPath,
|
'responses_path' => $responsesPath,
|
||||||
@ -49,4 +65,23 @@ return [
|
|||||||
'default_model' => 'gpt-5-mini',
|
'default_model' => 'gpt-5-mini',
|
||||||
'timeout' => 30,
|
'timeout' => 30,
|
||||||
'verify_tls' => true,
|
'verify_tls' => true,
|
||||||
|
|
||||||
|
// Additional settings for Appwizzy compatibility
|
||||||
|
'debug' => getenv('APP_ENV') === 'development' || getenv('APP_DEBUG') === 'true',
|
||||||
|
'max_retries' => 3,
|
||||||
|
'retry_delay' => 2,
|
||||||
|
|
||||||
|
// Feature flags
|
||||||
|
'features' => [
|
||||||
|
'async_polling' => true,
|
||||||
|
'json_response' => true,
|
||||||
|
'streaming' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Cache settings
|
||||||
|
'cache' => [
|
||||||
|
'enabled' => getenv('AI_CACHE_ENABLED') === 'true',
|
||||||
|
'ttl' => 3600, // 1 hour
|
||||||
|
'path' => __DIR__ . '/../cache/ai-responses',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
4154
ki-check-full.html
Normal file
4154
ki-check-full.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user