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
]);