false, 'error' => 'Not authenticated']); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'error' => 'Method not allowed']); exit; } if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']); exit; } // ── Input validation ──────────────────────────────────────────────────────── $recordId = (int)trim($_POST['rid'] ?? ''); $randId = trim($_POST['rand'] ?? ''); $section = trim($_POST['section'] ?? ''); $validSections = ['overview', 'ai_interpretation', 'foliar', 'microbial']; if (!$recordId || $randId === '' || !in_array($section, $validSections, true)) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'Invalid parameters']); exit; } // ── Load data ─────────────────────────────────────────────────────────────── try { $pdo = getDBConnection(); $userId = getCurrentUserId(); $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?'); $stmt->execute([$recordId, $randId]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { http_response_code(404); echo json_encode(['success' => false, 'error' => 'Record not found']); exit; } $spec = []; if (!empty($row['soil_type'])) { $stmtSpec = $pdo->prepare('SELECT * FROM soil_specifications WHERE soil_type = ? LIMIT 1'); $stmtSpec->execute([$row['soil_type']]); $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: []; } } catch (PDOException $e) { error_log('DB error in ollamaGenerate.php: ' . $e->getMessage()); http_response_code(500); echo json_encode(['success' => false, 'error' => 'Database error']); exit; } // ── Build soil data summary for the prompt ────────────────────────────────── /** Helper: format a numeric value safely, '' → 'N/A' */ function fv(mixed $v, int $dp = 2): string { if ($v === null || $v === '') return 'N/A'; return is_numeric($v) ? number_format((float)$v, $dp) : (string)$v; } /** Compare a value to a min/max range, return status string */ function rangeStatus(mixed $value, mixed $min, mixed $max): string { if (!is_numeric($value)) return ''; $v = (float)$value; $lo = is_numeric($min) ? (float)$min : null; $hi = is_numeric($max) ? (float)$max : null; if ($lo !== null && $v < $lo) return 'DEFICIENT'; if ($hi !== null && $v > $hi) return 'EXCESS'; return 'IDEAL'; } $r = $row; $s = $spec; $soilSummary = << "You are an experienced Australian agronomist writing a professional soil analysis report.\n\n" . $soilSummary . "\n\nWrite a concise executive overview (3–4 paragraphs) suitable for a farmer. " . "Summarise the overall soil health, the most important deficiencies or imbalances, " . "and their likely impact on crop performance. Use plain language — avoid jargon. " . "Do not include specific product recommendations in this section.", 'ai_interpretation' => "You are an experienced Australian agronomist.\n\n" . $soilSummary . "\n\nProvide a detailed technical interpretation of these soil test results. " . "For each element that is DEFICIENT or in EXCESS, explain: " . "(1) the agronomic significance, " . "(2) the likely cause in Australian soils, " . "(3) the interactions with other nutrients shown in this test. " . "Also comment on the base saturation balance, pH implications, and organic matter. " . "Write in a professional tone suitable for an agronomist's report.", 'foliar' => "You are an experienced Australian agronomist.\n\n" . $soilSummary . "\n\nDesign a foliar spray program to correct the nutrient deficiencies shown in these soil test results. " . "List recommended applications by growth stage, including product types, rates (L/ha or kg/ha), " . "and timing. Focus on elements that are DEFICIENT. " . "Note any antagonisms to avoid (e.g. Ca and Mg competing). " . "Write as a practical program a farmer can follow.", 'microbial' => "You are an experienced Australian agronomist with expertise in soil biology.\n\n" . $soilSummary . "\n\nDesign a microbial and biological soil program suited to these test results. " . "Consider: the organic matter level, pH, and nutrient imbalances shown. " . "Recommend specific microbial inoculants, compost teas, or biological amendments " . "that would improve soil biology and nutrient cycling for the crop type listed. " . "Include timing and application rates where possible.", ]; // ── Call Ollama ───────────────────────────────────────────────────────────── define('OLLAMA_URL', 'http://localhost:11434/api/generate'); define('OLLAMA_MODEL', 'llama3.2'); // change to match your installed model define('OLLAMA_TIMEOUT', 120); // seconds — LLM can be slow $prompt = $prompts[$section]; $payload = json_encode([ 'model' => OLLAMA_MODEL, 'prompt' => $prompt, 'stream' => false, ]); $ch = curl_init(OLLAMA_URL); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => OLLAMA_TIMEOUT, CURLOPT_CONNECTTIMEOUT => 5, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); curl_close($ch); if ($curlErr || $response === false) { http_response_code(502); echo json_encode([ 'success' => false, 'error' => 'Could not connect to Ollama: ' . ($curlErr ?: 'no response'), ]); exit; } if ($httpCode !== 200) { http_response_code(502); echo json_encode([ 'success' => false, 'error' => 'Ollama returned HTTP ' . $httpCode, ]); exit; } $ollamaData = json_decode($response, true); $text = trim($ollamaData['response'] ?? ''); if ($text === '') { http_response_code(502); echo json_encode(['success' => false, 'error' => 'Ollama returned an empty response']); exit; } echo json_encode(['success' => true, 'text' => $text]); exit;