HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //home/panomity.de/vr.panomity.com/api/ollama-generate-starters.php
<?php
/**
 * Ollama Conversation Starters Generator API für CMS4VR
 * 
 * Generiert Konversationsstarter basierend auf Kontext mit gpt-oss:20b
 */

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Method not allowed']);
    exit;
}

// Datenbank-Verbindung
$dbPath = __DIR__ . '/../admin/data/vrm.db';
if (!file_exists($dbPath)) {
    http_response_code(500);
    echo json_encode(['error' => 'Database not found']);
    exit;
}

$db = new SQLite3($dbPath);
$db->busyTimeout(5000);

// Sicherstellen, dass benötigte Spalten existieren
function ensure_column($db, $table, $column, $type) {
    $exists = false;
    $res = $db->query("PRAGMA table_info('" . $table . "')");
    if ($res) {
        while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
            if (isset($row['name']) && $row['name'] === $column) { $exists = true; break; }
        }
    }
    if (!$exists) {
        $db->exec("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type);
    }
}

$db->exec('CREATE TABLE IF NOT EXISTS ollama_conversation_starters (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pano_id INTEGER,
    category_id INTEGER,
    language TEXT NOT NULL,
    starter_text TEXT NOT NULL,
    position INTEGER DEFAULT 0,
    active INTEGER DEFAULT 1
)');

// Spalten für Kontext sicherstellen
ensure_column($db, 'panos', 'ollama_context', 'TEXT');
ensure_column($db, 'category_phrases', 'ollama_context', 'TEXT');

require_once __DIR__ . '/ollama_context_helper.php';

// Request-Body lesen
$input = file_get_contents('php://input');
$data = json_decode($input, true);

// Validierung
$panoId = isset($data['pano_id']) ? (int)$data['pano_id'] : null;
$categoryId = isset($data['category_id']) ? (int)$data['category_id'] : null;
$language = isset($data['language']) ? $data['language'] : 'de';

if (!$panoId && !$categoryId) {
    http_response_code(400);
    echo json_encode(['error' => 'pano_id or category_id required']);
    exit;
}

// Kontext abrufen
$context = '';
if ($panoId) {
    // Pano-Kontext (sprachabhängig)
    $context = read_pano_context($db, $panoId, $language);

    // Category-Kontext hinzufügen
    $stmt = $db->prepare('
        SELECT cp.ollama_context 
        FROM pano_category_connections pcc
        JOIN category_phrases cp ON cp.categories_id = pcc.category_id
        JOIN languages l ON l.id = cp.languages_id
        WHERE pcc.pano_id = :pano_id AND l.short = :language
        LIMIT 1
    ');
    $stmt->bindValue(':pano_id', $panoId, SQLITE3_INTEGER);
    $stmt->bindValue(':language', $language, SQLITE3_TEXT);
    $result = $stmt->execute();
    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {
        $catContext = $row['ollama_context'] ?: '';
        if ($catContext) {
            $context = $context ? $context . "\n\n" . $catContext : $catContext;
        }
    }
} else {
    // Category-Kontext
    $stmt = $db->prepare('
        SELECT ollama_context 
        FROM category_phrases cp
        JOIN languages l ON l.id = cp.languages_id
        WHERE cp.categories_id = :category_id AND l.short = :language
        LIMIT 1
    ');
    $stmt->bindValue(':category_id', $categoryId, SQLITE3_INTEGER);
    $stmt->bindValue(':language', $language, SQLITE3_TEXT);
    $result = $stmt->execute();
    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {
        $context = $row['ollama_context'] ?: '';
    }
}

if (empty($context)) {
    http_response_code(400);
    echo json_encode(['error' => 'No context found']);
    exit;
}

// Prompt für Konversationsstarter-Generierung (Besucher-Perspektive)
$prompt =
    "Schreibe in der Sprache: " . $language . ".\n" .
    "Erzeuge 2–4 kurze Konversationsstarter (Fragen oder Aufforderungen),\n" .
    "die ein BESUCHER stellen würde – also als direkte Nutzeräußerungen.\n" .
    "Perspektive: Nutzer (Besucher). Vermeide Formulierungen aus Sicht des Assistenten\n" .
    "(kein 'wir', 'unser', keine Begrüßungen wie 'Willkommen').\n" .
    "Erlaubt sind Fragen oder knappe Aufforderungen (z. B. 'Zeig mir ...', 'Kann ich ...?').\n" .
    "Länge: jeweils max. 10–12 Wörter.\n\n" .
    "Kontext:\n" . $context . "\n\n" .
    "Gib NUR die Starter aus: jeweils exakt eine Zeile pro Starter,\n" .
    "ohne Nummerierung, ohne Aufzählungszeichen, ohne zusätzliche Erläuterungen.";

// Generierung läuft als vr_chat-Job über die GPUQ-Queue.
$chat = gpuq_chat([['role' => 'user', 'content' => $prompt]], ['priority' => 'normal', 'timeout' => 120, 'max_wait' => 150]);

if (empty($chat['success'])) {
    http_response_code(500);
    echo json_encode(['error' => 'GPUQ chat error', 'details' => $chat['error'] ?? 'unknown']);
    $db->close();
    exit;
}

$generatedText = $chat['content'];

// Starter aus Response extrahieren (Zeilenweise)
$lines = explode("\n", $generatedText);
$starters = [];
$position = 0;

foreach ($lines as $line) {
    $trimmed = trim($line);
    // Entferne Nummerierungen, Bullets, etc.
    $trimmed = preg_replace('/^[\d\.\-\*\•]\s*/', '', $trimmed);
    $trimmed = trim($trimmed);
    
    if (!empty($trimmed) && strlen($trimmed) > 5) {
        $starters[] = [
            'text' => $trimmed,
            'position' => $position++
        ];
    }
}

// Begrenze auf 4 Starter
$starters = array_slice($starters, 0, 4);

// Starter in Datenbank speichern
$savedStarters = [];
foreach ($starters as $starter) {
    $stmt = $db->prepare('
        INSERT INTO ollama_conversation_starters 
        (pano_id, category_id, language, starter_text, position, active)
        VALUES (:pano_id, :category_id, :language, :starter_text, :position, 1)
    ');
    $stmt->bindValue(':pano_id', $panoId, SQLITE3_INTEGER);
    $stmt->bindValue(':category_id', $categoryId, SQLITE3_INTEGER);
    $stmt->bindValue(':language', $language, SQLITE3_TEXT);
    $stmt->bindValue(':starter_text', $starter['text'], SQLITE3_TEXT);
    $stmt->bindValue(':position', $starter['position'], SQLITE3_INTEGER);
    $stmt->execute();
    
    $savedStarters[] = [
        'id' => $db->lastInsertRowID(),
        'text' => $starter['text'],
        'position' => $starter['position']
    ];
}

$db->close();

echo json_encode([
    'success' => true,
    'starters' => $savedStarters
]);