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
|
||||
|
||||
# Security & Performance
|
||||
Options -Indexes
|
||||
Options -MultiViews
|
||||
ServerSignature Off
|
||||
|
||||
# Enable Rewrite Engine
|
||||
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} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
|
||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
||||
RewriteRule ^(.+?)/?$ $1.php [L]
|
||||
# 2) Handle clean URLs for questionnaire
|
||||
# Rewrite /ki-fit-check to index.php (main questionnaire)
|
||||
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
|
||||
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
|
||||
// LocalAIApi — proxy client for the Responses API.
|
||||
// Usage (async: auto-polls status until ready):
|
||||
// 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]);
|
||||
// ai/LocalAIApi.php — proxy client for the Responses API.
|
||||
// For Appwizzy platform compatibility
|
||||
|
||||
class LocalAIApi
|
||||
{
|
||||
/** @var array<string,mixed>|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.
|
||||
*
|
||||
* @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).
|
||||
* Usage:
|
||||
* 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{
|
||||
* success:bool,
|
||||
* status?:int,
|
||||
* data?:mixed,
|
||||
* error?:string,
|
||||
* response?:mixed,
|
||||
* message?:string
|
||||
* success: bool,
|
||||
* status?: int,
|
||||
* data?: mixed,
|
||||
* error?: string,
|
||||
* response?: mixed,
|
||||
* message?: string
|
||||
* }
|
||||
*/
|
||||
public static function createResponse(array $params, array $options = []): array
|
||||
@ -47,6 +50,7 @@ class LocalAIApi
|
||||
$cfg = self::config();
|
||||
$payload = $params;
|
||||
|
||||
// Validate input
|
||||
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
@ -55,10 +59,15 @@ class LocalAIApi
|
||||
];
|
||||
}
|
||||
|
||||
// Set default model if not provided
|
||||
if (!isset($payload['model']) || $payload['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);
|
||||
if (empty($initial['success'])) {
|
||||
return $initial;
|
||||
@ -68,14 +77,7 @@ class LocalAIApi
|
||||
$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; // seconds
|
||||
$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 self::awaitResponse($aiRequestId, $options);
|
||||
}
|
||||
|
||||
return $initial;
|
||||
@ -96,9 +98,9 @@ class LocalAIApi
|
||||
/**
|
||||
* Perform a raw request to the AI proxy.
|
||||
*
|
||||
* @param string $path Endpoint (may be an absolute URL).
|
||||
* @param array<string,mixed> $payload JSON payload.
|
||||
* @param array<string,mixed> $options Additional request options.
|
||||
* @param string|null $path Endpoint (may be an absolute URL)
|
||||
* @param array<string,mixed> $payload JSON payload
|
||||
* @param array<string,mixed> $options Additional request options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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']);
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
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;
|
||||
$timeout = $options['timeout'] ?? $cfg['timeout'] ?? 30;
|
||||
$verifyTls = $options['verify_tls'] ?? $cfg['verify_tls'] ?? true;
|
||||
$maxRetries = $options['max_retries'] ?? $cfg['max_retries'] ?? 3;
|
||||
$retryDelay = $options['retry_delay'] ?? $cfg['retry_delay'] ?? 2;
|
||||
|
||||
$projectHeader = $cfg['project_header'];
|
||||
|
||||
@ -143,6 +139,7 @@ class LocalAIApi
|
||||
'Accept: application/json',
|
||||
];
|
||||
$headers[] = $projectHeader . ': ' . $projectUuid;
|
||||
|
||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||
foreach ($options['headers'] as $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)) {
|
||||
$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) {
|
||||
return [
|
||||
'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();
|
||||
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
||||
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
|
||||
$timeout = $options['poll_timeout'] ?? 300; // seconds
|
||||
$interval = $options['poll_interval'] ?? 5; // seconds
|
||||
if ($interval <= 0) {
|
||||
$interval = 5;
|
||||
}
|
||||
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
|
||||
$perCallTimeout = $options['timeout'] ?? null;
|
||||
|
||||
$deadline = time() + max($timeout, $interval);
|
||||
$headers = $options['headers'] ?? [];
|
||||
|
||||
$attempt = 0;
|
||||
|
||||
while (true) {
|
||||
$attempt++;
|
||||
$statusResp = self::fetchStatus($aiRequestId, [
|
||||
'headers' => $headers,
|
||||
'timeout' => $perCallTimeout,
|
||||
]);
|
||||
|
||||
if (!empty($statusResp['success'])) {
|
||||
$data = $statusResp['data'] ?? [];
|
||||
if (is_array($data)) {
|
||||
$statusValue = $data['status'] ?? null;
|
||||
if ($statusValue === 'success') {
|
||||
if ($statusValue === 'success' || $statusValue === 'completed') {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
'data' => $data['response'] ?? $data,
|
||||
'attempts' => $attempt,
|
||||
];
|
||||
}
|
||||
if ($statusValue === 'failed') {
|
||||
if ($statusValue === 'failed' || $statusValue === 'error') {
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
||||
'data' => $data,
|
||||
'attempts' => $attempt,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -221,10 +261,14 @@ class LocalAIApi
|
||||
return [
|
||||
'success' => false,
|
||||
'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);
|
||||
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
||||
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
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;
|
||||
$timeout = $options['timeout'] ?? $cfg['timeout'] ?? 30;
|
||||
$verifyTls = $options['verify_tls'] ?? $cfg['verify_tls'] ?? true;
|
||||
|
||||
$projectHeader = $cfg['project_header'];
|
||||
$headers = [
|
||||
@ -280,7 +316,7 @@ class LocalAIApi
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function extractText(array $response): string
|
||||
@ -290,6 +326,7 @@ class LocalAIApi
|
||||
return '';
|
||||
}
|
||||
|
||||
// Try to extract from OpenAI Responses API format
|
||||
if (!empty($payload['output']) && is_array($payload['output'])) {
|
||||
$combined = '';
|
||||
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'])) {
|
||||
return (string) $payload['choices'][0]['message']['content'];
|
||||
}
|
||||
|
||||
// Try to extract from generic response
|
||||
if (!empty($payload['text'])) {
|
||||
return (string) $payload['text'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -327,12 +370,14 @@ class LocalAIApi
|
||||
return null;
|
||||
}
|
||||
|
||||
// First try to decode directly
|
||||
$decoded = json_decode($text, true);
|
||||
if (is_array($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) {
|
||||
$decoded = json_decode($stripped, true);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -347,6 +400,7 @@ class LocalAIApi
|
||||
* Load configuration from ai/config.php.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private static function config(): array
|
||||
{
|
||||
@ -359,7 +413,20 @@ class LocalAIApi
|
||||
if (!is_array($cfg)) {
|
||||
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;
|
||||
@ -420,7 +487,7 @@ class LocalAIApi
|
||||
return [
|
||||
'success' => false,
|
||||
'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.',
|
||||
];
|
||||
}
|
||||
|
||||
@ -432,6 +499,16 @@ class LocalAIApi
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
||||
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);
|
||||
if ($upper === 'POST') {
|
||||
@ -442,19 +519,24 @@ class LocalAIApi
|
||||
}
|
||||
|
||||
$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) {
|
||||
$error = curl_error($ch) ?: 'Unknown cURL error';
|
||||
curl_close($ch);
|
||||
return [
|
||||
'success' => false,
|
||||
'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;
|
||||
if ($responseBody !== '' && $responseBody !== null) {
|
||||
$decoded = json_decode($responseBody, true);
|
||||
@ -491,3 +573,17 @@ class LocalAIApi
|
||||
if (!class_exists('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);
|
||||
}
|
||||
}
|
||||
101
ai/config.php
101
ai/config.php
@ -1,45 +1,61 @@
|
||||
<?php
|
||||
// OpenAI proxy configuration (workspace scope).
|
||||
// Reads values from environment variables or executor/.env.
|
||||
// ai/config.php - OpenAI proxy configuration (workspace scope)
|
||||
// Reads values from environment variables or executor/.env
|
||||
// For Appwizzy platform compatibility
|
||||
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
|
||||
if (
|
||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||
($projectId === false || $projectId === null || $projectId === '')
|
||||
) {
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value, "\"' ");
|
||||
if (getenv($key) === false || getenv($key) === '') {
|
||||
putenv("{$key}={$value}");
|
||||
/**
|
||||
* 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 (
|
||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||
($projectId === false || $projectId === null || $projectId === '')
|
||||
) {
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value, "\"' ");
|
||||
if (getenv($key) === false || getenv($key) === '') {
|
||||
putenv("{$key}={$value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
}
|
||||
}
|
||||
|
||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
||||
$projectId = ($projectId === false) ? null : $projectId;
|
||||
// Load environment variables if needed
|
||||
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;
|
||||
|
||||
// Return configuration array
|
||||
return [
|
||||
'base_url' => $baseUrl,
|
||||
'responses_path' => $responsesPath,
|
||||
@ -49,4 +65,23 @@ return [
|
||||
'default_model' => 'gpt-5-mini',
|
||||
'timeout' => 30,
|
||||
'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