Files
portfoliowai/api/chat.php
T

206 lines
7.8 KiB
PHP

<?php
header('Content-Type: application/json');
// Start session for simple rate limiting (skip if session already started or headers sent)
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
@session_start();
}
// Simple rate limiting: max 30 requests per minute
if (!isset($_SESSION['chat_requests'])) {
$_SESSION['chat_requests'] = [];
}
$now = time();
// Keep only requests from the last 60 seconds
$_SESSION['chat_requests'] = array_filter($_SESSION['chat_requests'], function($timestamp) use ($now) {
return ($now - $timestamp) < 60;
});
if (count($_SESSION['chat_requests']) >= 30) {
http_response_code(429);
echo json_encode(['error' => 'Too many requests. Please wait a minute before sending another message.']);
exit;
}
$_SESSION['chat_requests'][] = $now;
// Load DB connection which also loads environment variables
require_once __DIR__ . '/../config/db.php';
// Load Groq API key (free tier: 14,400 req/day)
$apiKey = $_ENV['GROQ_API_KEY'] ?? getenv('GROQ_API_KEY') ?? '';
if (empty($apiKey)) {
http_response_code(500);
echo json_encode(['error' => 'API Key configuration is missing. Please set GROQ_API_KEY in your .env file.']);
exit;
}
// Get the raw POST data
$inputData = json_decode(file_get_contents('php://input'), true);
if (!$inputData || !isset($inputData['message'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid request data.']);
exit;
}
$userMessage = trim($inputData['message']);
if (empty($userMessage)) {
http_response_code(400);
echo json_encode(['error' => 'Message cannot be empty.']);
exit;
}
// Fetch context data from database to construct system instructions
// 1. Settings / About
$settingsQuery = $mysqli->query("SELECT * FROM settings WHERE id = 1");
$settings = $settingsQuery ? $settingsQuery->fetch_assoc() : null;
// 2. Projects
$projectsQuery = $mysqli->query("SELECT title, des, techstack, url FROM projects");
$projects = [];
if ($projectsQuery) {
while ($row = $projectsQuery->fetch_assoc()) {
$projects[] = $row;
}
}
// 3. Certifications
$certsQuery = $mysqli->query("SELECT title, issuer, date_issued FROM certifications");
$certs = [];
if ($certsQuery) {
while ($row = $certsQuery->fetch_assoc()) {
$certs[] = $row;
}
}
// 4. Articles
$articlesQuery = $mysqli->query("SELECT title, excerpt, date_published, tag, url FROM articles");
$articles = [];
if ($articlesQuery) {
while ($row = $articlesQuery->fetch_assoc()) {
$articles[] = $row;
}
}
// Build the System Instruction
$systemPrompt = "You are a friendly, professional, and intelligent AI assistant built into the personal portfolio website of Cris Charles Garcia.\n";
$systemPrompt .= "Your role is to represent Cris, assist recruiters, potential clients, and visitors, and answer questions about his background, experience, skills, and projects.\n\n";
$systemPrompt .= "Cris's Profile & Biography:\n";
if ($settings) {
$systemPrompt .= "- Bio Paragraph 1: " . $settings['about_text_1'] . "\n";
$systemPrompt .= "- Bio Paragraph 2: " . $settings['about_text_2'] . "\n";
$systemPrompt .= "- Years coding: " . $settings['years_coding'] . "\n";
$systemPrompt .= "- Projects completed: " . $settings['projects_count'] . "\n";
$systemPrompt .= "- Certifications: " . $settings['certifications_count'] . "\n";
$systemPrompt .= "- Github: " . $settings['github_url'] . "\n";
$systemPrompt .= "- LinkedIn: " . $settings['linkedin_url'] . "\n";
$systemPrompt .= "- Instagram: " . $settings['instagram_url'] . "\n";
$systemPrompt .= "- Facebook: " . $settings['facebook_url'] . "\n";
$systemPrompt .= "- TikTok: " . $settings['tiktok_url'] . "\n";
} else {
$systemPrompt .= "- Cris is a senior IT student majoring in Software Development based in Laguna, Philippines.\n";
}
$systemPrompt .= "\nCris's Projects:\n";
foreach ($projects as $p) {
$systemPrompt .= "- **" . $p['title'] . "**: " . $p['des'] . " (Tech: " . $p['techstack'] . "). Link: " . $p['url'] . "\n";
}
$systemPrompt .= "\nCris's Certifications:\n";
foreach ($certs as $c) {
$systemPrompt .= "- " . $c['title'] . " by " . $c['issuer'] . " (Issued: " . $c['date_issued'] . ")\n";
}
$systemPrompt .= "\nCris's Latest Articles/Blog Posts:\n";
foreach ($articles as $a) {
$systemPrompt .= "- \"" . $a['title'] . "\": " . $a['excerpt'] . " [Tag: " . $a['tag'] . ", Date: " . $a['date_published'] . ", Link: " . $a['url'] . "]\n";
}
$systemPrompt .= "\nGuidelines for your responses:\n";
$systemPrompt .= "1. Respond in first-person (e.g., \"I am a senior IT student...\" or \"In my project UniMarket...\") or as a dedicated assistant of Cris. Represent Cris accurately, warmly, and professionally.\n";
$systemPrompt .= "2. Keep responses brief, engaging, and directly related to the question. Recruiters appreciate concise answers.\n";
$systemPrompt .= "3. If asked about contact options or hiring, point them to his contact form at the bottom of the page or his professional links (Github, LinkedIn).\n";
$systemPrompt .= "4. Do not make up facts or external projects not listed in the context. If you don't know the answer, politely invite the user to contact Cris directly.\n";
$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n";
// Build Groq messages array (OpenAI-compatible format)
// System prompt goes in its own 'system' role message
$messages = [
['role' => 'system', 'content' => $systemPrompt]
];
// Append chat history
if (isset($inputData['history']) && is_array($inputData['history']) && !empty($inputData['history'])) {
foreach ($inputData['history'] as $msg) {
if (isset($msg['role']) && isset($msg['text'])) {
$role = ($msg['role'] === 'user') ? 'user' : 'assistant';
$messages[] = ['role' => $role, 'content' => $msg['text']];
}
}
}
// Add the current user message
$messages[] = ['role' => 'user', 'content' => $userMessage];
// Groq request payload (OpenAI-compatible)
$requestPayload = [
'model' => 'llama-3.3-70b-versatile', // 14,400 RPD free
'messages' => $messages,
'temperature' => 0.7,
'max_tokens' => 800,
'stream' => false
];
// Guard: cURL must be available
if (!function_exists('curl_init')) {
http_response_code(503);
echo json_encode(['error' => 'cURL is not available on this server. Please use the client-side fallback.']);
exit;
}
// Call Groq API via cURL (OpenAI-compatible endpoint)
// Model: llama-3.3-70b-versatile — free tier, 14,400 req/day
$url = 'https://api.groq.com/openai/v1/chat/completions';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$errorMsg = curl_error($ch);
curl_close($ch);
http_response_code(500);
echo json_encode(['error' => 'cURL error: ' . $errorMsg]);
exit;
}
curl_close($ch);
if ($httpCode !== 200) {
http_response_code($httpCode);
$errorDetails = json_decode($response, true);
$errMsg = isset($errorDetails['error']['message']) ? $errorDetails['error']['message'] : 'Failed to retrieve response from Groq.';
echo json_encode(['error' => $errMsg]);
exit;
}
$responseData = json_decode($response, true);
$reply = '';
if (isset($responseData['choices'][0]['message']['content'])) {
$reply = $responseData['choices'][0]['message']['content'];
} else {
http_response_code(502);
echo json_encode(['error' => 'Invalid response structure from Groq API.', 'raw' => $responseData]);
exit;
}
echo json_encode(['reply' => $reply]);