File: /home/panomity.de/public_html/unhinged-ai/chat-backend.php
<?php
declare(strict_types=1);
function load_system_prompt(): array
{
$path = __DIR__ . DIRECTORY_SEPARATOR . 'system.txt';
$default = "You are a precise, concise assistant. Answer directly, avoid fluff, and show steps only when asked.";
if (is_file($path) && is_readable($path)) {
$raw = file_get_contents($path);
if ($raw !== false) {
if (substr($raw, 0, 3) === "\xEF\xBB\xBF") {
$raw = substr($raw, 3);
}
$content = trim($raw);
return [$content === '' ? $default : $content, true];
}
}
return [$default, false];
}
function current_history(): array
{
if (!isset($_SESSION['history']) || !is_array($_SESSION['history'])) {
$_SESSION['history'] = [];
}
return $_SESSION['history'];
}
function save_history(array $history): void
{
$_SESSION['history'] = $history;
}
function reset_session_chat(): void
{
$_SESSION['initialized'] = false;
$_SESSION['system_content'] = null;
$_SESSION['history'] = [];
}
function unhinged_read_json_body(): array
{
$raw = file_get_contents('php://input');
if ($raw === false || $raw === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
function unhinged_json_response(array $payload, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function unhinged_http_json(string $method, string $url, ?array $payload, int $timeoutSeconds): array
{
$ch = curl_init($url);
if ($ch === false) {
return [
'ok' => false,
'status' => 0,
'error' => 'Failed to initialize cURL',
'json' => null,
'raw' => '',
];
}
$headers = ['Accept: application/json', 'Expect:'];
$body = null;
if ($payload !== null) {
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($body === false) {
curl_close($ch);
return [
'ok' => false,
'status' => 0,
'error' => 'JSON encoding failed',
'json' => null,
'raw' => '',
];
}
$headers[] = 'Content-Type: application/json';
}
$upperMethod = strtoupper($method);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $upperMethod,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => max(5, $timeoutSeconds),
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$raw = curl_exec($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($raw === false) {
$err = curl_error($ch);
curl_close($ch);
return [
'ok' => false,
'status' => 0,
'error' => $err !== '' ? $err : 'HTTP request failed',
'json' => null,
'raw' => '',
];
}
curl_close($ch);
$decoded = json_decode((string)$raw, true);
if (!is_array($decoded)) {
$decoded = null;
}
return [
'ok' => $status >= 200 && $status < 300,
'status' => $status,
'error' => '',
'json' => $decoded,
'raw' => (string)$raw,
];
}
function unhinged_is_ollama_reachable(string $ollamaBase, int $timeoutSeconds = 2): bool
{
$res = unhinged_http_json('GET', rtrim($ollamaBase, '/') . '/api/tags', null, max(1, $timeoutSeconds));
return $res['ok'] === true;
}
function unhinged_wait_for_ollama(string $ollamaBase, int $maxWaitSeconds = 30): bool
{
$deadline = microtime(true) + max(2, $maxWaitSeconds);
while (microtime(true) < $deadline) {
if (unhinged_is_ollama_reachable($ollamaBase, 2)) {
return true;
}
usleep(500000);
}
return false;
}
function unhinged_start_ollama_if_needed(string $ollamaBase): array
{
if (unhinged_is_ollama_reachable($ollamaBase, 2)) {
return ['ok' => true, 'started' => false];
}
@exec('sudo -n systemctl start ollama >/dev/null 2>&1');
if (unhinged_wait_for_ollama($ollamaBase, 20)) {
return ['ok' => true, 'started' => true];
}
@exec('nohup /usr/local/bin/ollama serve >/tmp/ollama-unhinged.log 2>&1 &');
if (unhinged_wait_for_ollama($ollamaBase, 25)) {
return ['ok' => true, 'started' => true];
}
return ['ok' => false, 'started' => false];
}
function unhinged_call_ollama_chat(
string $ollamaBase,
string $model,
array $messages,
array $options,
int $timeoutSeconds
): array {
$start = unhinged_start_ollama_if_needed($ollamaBase);
if (empty($start['ok'])) {
return [
'ok' => false,
'message' => 'Ollama ist offline und konnte nicht gestartet werden.',
'assistant_content' => '',
'ollama_started' => false,
];
}
$payload = [
'model' => $model,
'stream' => false,
'messages' => array_values($messages),
'options' => (array)$options,
];
$res = unhinged_http_json(
'POST',
rtrim($ollamaBase, '/') . '/api/chat',
$payload,
max(20, min(1800, $timeoutSeconds))
);
if ($res['ok'] !== true) {
$message = $res['error'];
if ($message === '') {
$message = 'HTTP ' . (int)$res['status'];
}
if (is_array($res['json']) && isset($res['json']['error'])) {
$message = (string)$res['json']['error'];
}
return [
'ok' => false,
'message' => $message,
'assistant_content' => '',
'ollama_started' => (bool)$start['started'],
];
}
$json = is_array($res['json']) ? $res['json'] : [];
$assistant = trim((string)($json['message']['content'] ?? ''));
if ($assistant === '') {
return [
'ok' => false,
'message' => 'Leere Antwort vom Modell.',
'assistant_content' => '',
'ollama_started' => (bool)$start['started'],
];
}
return [
'ok' => true,
'message' => '',
'assistant_content' => $assistant,
'ollama_started' => (bool)$start['started'],
];
}
function unhinged_handle_execute_request(array $config): void
{
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if (strtoupper($method) !== 'POST') {
unhinged_json_response(['success' => false, 'message' => 'Nur POST wird unterstützt.'], 405);
}
$body = unhinged_read_json_body();
$model = trim((string)($body['model'] ?? ''));
$messagesIn = $body['messages'] ?? null;
$options = (array)($body['options'] ?? []);
$timeoutSeconds = (int)($body['timeout_seconds'] ?? 300);
if ($model === '') {
unhinged_json_response(['success' => false, 'message' => 'model fehlt'], 400);
}
if (!is_array($messagesIn) || $messagesIn === []) {
unhinged_json_response(['success' => false, 'message' => 'messages fehlt'], 400);
}
$messages = [];
foreach ($messagesIn as $item) {
if (!is_array($item)) {
continue;
}
$role = trim((string)($item['role'] ?? ''));
$content = (string)($item['content'] ?? '');
if ($role === '' || $content === '') {
continue;
}
$messages[] = ['role' => $role, 'content' => $content];
}
if ($messages === []) {
unhinged_json_response(['success' => false, 'message' => 'messages ist ungültig'], 400);
}
$ollamaBase = (string)($config['ollama_base'] ?? 'http://127.0.0.1:11434');
$run = unhinged_call_ollama_chat($ollamaBase, $model, $messages, $options, $timeoutSeconds);
if (empty($run['ok'])) {
unhinged_json_response(
[
'success' => false,
'message' => (string)($run['message'] ?? 'Ausführung fehlgeschlagen'),
'ollama_started' => (bool)($run['ollama_started'] ?? false),
],
502
);
}
unhinged_json_response(
[
'success' => true,
'assistant_content' => (string)$run['assistant_content'],
'model' => $model,
'ollama_started' => (bool)($run['ollama_started'] ?? false),
]
);
}
function unhinged_submit_gpuq_job(array $config, string $mode, string $model, array $messages): array
{
$gpuqBase = rtrim((string)($config['gpuq_api_base'] ?? 'http://127.0.0.1:8020'), '/');
$executeUrl = (string)($config['execute_url'] ?? '');
$options = (array)($config['options'] ?? []);
$chatTimeout = $mode === 'deep' ? 1500 : 900;
$queueTimeout = $mode === 'deep' ? 1800 : 1200;
if ($executeUrl === '') {
return ['ok' => false, 'message' => 'execute_url ist nicht gesetzt.'];
}
$submitPayload = [
'app_name' => 'unhinged',
'job_type' => 'chat_generate',
'priority' => $mode === 'deep' ? 'normal' : 'high',
'execute_url' => $executeUrl,
'execute_method' => 'POST',
'timeout_seconds' => $queueTimeout,
'payload' => [
'model' => $model,
'messages' => $messages,
'options' => $options,
'timeout_seconds' => $chatTimeout,
],
];
$res = unhinged_http_json('POST', $gpuqBase . '/api/v1/jobs', $submitPayload, 20);
if ($res['ok'] !== true) {
$message = $res['error'];
if ($message === '' && is_array($res['json']) && isset($res['json']['detail'])) {
$message = is_string($res['json']['detail']) ? $res['json']['detail'] : json_encode($res['json']['detail']);
}
if ($message === '') {
$message = 'GPUQ HTTP ' . (int)$res['status'];
}
return ['ok' => false, 'message' => $message];
}
$json = is_array($res['json']) ? $res['json'] : [];
$jobId = trim((string)($json['job_id'] ?? ''));
if ($jobId === '') {
return ['ok' => false, 'message' => 'GPUQ lieferte keine job_id zurück.'];
}
return ['ok' => true, 'job_id' => $jobId];
}
function unhinged_wait_for_gpuq_result(array $config, string $jobId, int $maxWaitSeconds): array
{
$gpuqBase = rtrim((string)($config['gpuq_api_base'] ?? 'http://127.0.0.1:8020'), '/');
$deadline = time() + max(15, $maxWaitSeconds);
while (time() <= $deadline) {
$res = unhinged_http_json(
'GET',
$gpuqBase . '/api/v1/jobs/' . rawurlencode($jobId),
null,
20
);
if ($res['ok'] !== true || !is_array($res['json'])) {
$msg = $res['error'] !== '' ? $res['error'] : ('GPUQ Status HTTP ' . (int)$res['status']);
return ['ok' => false, 'message' => $msg];
}
$statusPayload = $res['json'];
$status = (string)($statusPayload['status'] ?? '');
if ($status === 'completed') {
$result = $statusPayload['result'] ?? [];
if (!is_array($result)) {
$result = ['raw' => (string)$result];
}
return ['ok' => true, 'result' => $result];
}
if ($status === 'failed' || $status === 'canceled' || $status === 'killed') {
$message = (string)($statusPayload['message'] ?? '');
$result = $statusPayload['result'] ?? null;
if ($message === '' && is_array($result) && isset($result['message'])) {
$message = (string)$result['message'];
}
if ($message === '') {
$message = 'GPUQ-Job fehlgeschlagen (' . $status . ').';
}
return ['ok' => false, 'message' => $message];
}
usleep(900000);
}
return ['ok' => false, 'message' => 'GPUQ-Timeout beim Warten auf das Ergebnis.'];
}
function unhinged_stream_chat(string $userPrompt, string $mode, array $config): void
{
$fastModel = (string)($config['fast_model'] ?? 'huihui_ai/gemma3-abliterated:12b');
$deepModel = (string)($config['thoughtful_model'] ?? 'huihui_ai/qwen3.5-abliterated:35b');
$model = $mode === 'deep' ? $deepModel : $fastModel;
[$systemPrompt] = load_system_prompt();
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache, no-transform');
header('X-Accel-Buffering: no');
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', '0');
while (ob_get_level() > 0) {
@ob_end_flush();
}
ob_implicit_flush(true);
if (empty($_SESSION['initialized'])) {
$_SESSION['system_content'] = $userPrompt . "\n\n" . $systemPrompt;
$_SESSION['initialized'] = true;
if (!isset($_SESSION['history']) || !is_array($_SESSION['history'])) {
$_SESSION['history'] = [];
}
}
$systemContent = (string)($_SESSION['system_content'] ?? $systemPrompt);
$history = current_history();
$messages = [];
$messages[] = ['role' => 'system', 'content' => $systemContent];
foreach ($history as $msg) {
if (!is_array($msg)) {
continue;
}
if (!isset($msg['role'], $msg['content'])) {
continue;
}
$messages[] = [
'role' => (string)$msg['role'],
'content' => (string)$msg['content'],
];
}
$messages[] = ['role' => 'user', 'content' => $userPrompt];
$submit = unhinged_submit_gpuq_job($config, $mode, $model, $messages);
if (empty($submit['ok'])) {
http_response_code(502);
echo '[GPUQ submit error] ' . (string)($submit['message'] ?? 'Unbekannter Fehler') . "\n";
flush();
return;
}
$maxWait = $mode === 'deep' ? 1800 : 1200;
$resultPayload = unhinged_wait_for_gpuq_result($config, (string)$submit['job_id'], $maxWait);
if (empty($resultPayload['ok'])) {
http_response_code(502);
echo '[GPUQ job error] ' . (string)($resultPayload['message'] ?? 'Unbekannter Fehler') . "\n";
flush();
return;
}
$result = (array)($resultPayload['result'] ?? []);
$success = !empty($result['success']);
if (!$success) {
http_response_code(502);
$message = (string)($result['message'] ?? 'Backend-Fehler');
echo '[Backend error] ' . ($message !== '' ? $message : 'Unbekannter Fehler') . "\n";
flush();
return;
}
$assistantBuffer = (string)($result['assistant_content'] ?? '');
if ($assistantBuffer === '') {
http_response_code(502);
echo "[Backend error] Leere Antwort vom Modell.\n";
flush();
return;
}
// Persist turn to session history.
$history[] = ['role' => 'user', 'content' => $userPrompt];
$history[] = ['role' => 'assistant', 'content' => $assistantBuffer];
save_history($history);
echo $assistantBuffer;
echo "\n";
flush();
}