403 lines
18 KiB
Plaintext
403 lines
18 KiB
Plaintext
<?php
|
|
/**
|
|
* HAMID API — Chat + Vision + Files + Capabilities + Brain
|
|
* Handles: text chat, image analysis, PDF/text extraction, file attachments
|
|
*/
|
|
header('Content-Type: application/json');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type');
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
|
|
|
|
require_once(__DIR__ . '/../hamid-providers-config.php');
|
|
|
|
// ── Parse input: JSON body OR FormData (with files) ──
|
|
$input = [];
|
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
|
|
|
if (strpos($contentType, 'multipart/form-data') !== false) {
|
|
// FormData: files + json payload
|
|
if (!empty($_POST['json'])) {
|
|
$input = json_decode($_POST['json'], true) ?: [];
|
|
}
|
|
// Fallback to individual POST fields
|
|
if (empty($input['message']) && !empty($_POST['message'])) {
|
|
$input['message'] = $_POST['message'];
|
|
}
|
|
if (empty($input['action']) && !empty($_POST['action'])) {
|
|
$input['action'] = $_POST['action'];
|
|
}
|
|
if (empty($input['provider']) && !empty($_POST['provider'])) {
|
|
$input['provider'] = $_POST['provider'];
|
|
}
|
|
} else {
|
|
$raw = file_get_contents('php://input');
|
|
$input = json_decode($raw, true) ?: $_REQUEST;
|
|
}
|
|
|
|
$action = $input['action'] ?? $_GET['action'] ?? 'chat';
|
|
$message = $input['message'] ?? $input['q'] ?? '';
|
|
$provider = $input['provider'] ?? null;
|
|
$capability = $input['capability'] ?? 'normal';
|
|
$useKB = $input['use_kb'] ?? false;
|
|
$session = $input['session'] ?? 'default';
|
|
$convId = $input['conversation_id'] ?? null;
|
|
|
|
switch ($action) {
|
|
case 'chat':
|
|
// ── Collect attached files ──
|
|
$fileContexts = [];
|
|
$imageData = null;
|
|
$hasImage = false;
|
|
$hasFiles = false;
|
|
|
|
if (!empty($_FILES)) {
|
|
$hasFiles = true;
|
|
foreach ($_FILES as $key => $file) {
|
|
if ($file['error'] !== UPLOAD_ERR_OK) continue;
|
|
$mime = $file['type'] ?? mime_content_type($file['tmp_name']);
|
|
$name = $file['name'] ?? 'file';
|
|
$size = $file['size'];
|
|
|
|
if (strpos($mime, 'image/') === 0) {
|
|
// Image → base64 for vision
|
|
$hasImage = true;
|
|
$imageData = [
|
|
'base64' => base64_encode(file_get_contents($file['tmp_name'])),
|
|
'mime' => $mime,
|
|
'name' => $name
|
|
];
|
|
$fileContexts[] = "[Image jointe: {$name} ({$mime}, " . round($size/1024) . "KB)]";
|
|
}
|
|
elseif ($mime === 'application/pdf') {
|
|
// PDF → extract text
|
|
$text = shell_exec("pdftotext " . escapeshellarg($file['tmp_name']) . " - 2>/dev/null");
|
|
if ($text) {
|
|
$text = mb_substr(trim($text), 0, 8000);
|
|
$fileContexts[] = "=== CONTENU PDF: {$name} ===\n{$text}\n=== FIN PDF ===";
|
|
} else {
|
|
$fileContexts[] = "[PDF joint: {$name} - extraction texte échouée]";
|
|
}
|
|
}
|
|
elseif (in_array($mime, ['text/plain','text/csv','text/html','application/json','text/markdown'])) {
|
|
// Text files → read directly
|
|
$text = mb_substr(file_get_contents($file['tmp_name']), 0, 8000);
|
|
$ext = pathinfo($name, PATHINFO_EXTENSION);
|
|
$fileContexts[] = "=== CONTENU {$ext}: {$name} ===\n{$text}\n=== FIN ===";
|
|
}
|
|
elseif (strpos($mime, 'video/') === 0) {
|
|
// Video → extract info + first frame
|
|
$info = shell_exec("ffprobe -v quiet -print_format json -show_format " . escapeshellarg($file['tmp_name']) . " 2>/dev/null");
|
|
$dur = 'unknown';
|
|
if ($info) {
|
|
$meta = json_decode($info, true);
|
|
$dur = round(($meta['format']['duration'] ?? 0)) . 's';
|
|
}
|
|
// Extract first frame as image for vision
|
|
$tmpImg = tempnam('/tmp', 'vid_') . '.jpg';
|
|
shell_exec("ffmpeg -i " . escapeshellarg($file['tmp_name']) . " -vframes 1 -q:v 2 {$tmpImg} 2>/dev/null");
|
|
if (file_exists($tmpImg) && filesize($tmpImg) > 100) {
|
|
$hasImage = true;
|
|
$imageData = [
|
|
'base64' => base64_encode(file_get_contents($tmpImg)),
|
|
'mime' => 'image/jpeg',
|
|
'name' => $name . ' (frame)'
|
|
];
|
|
unlink($tmpImg);
|
|
}
|
|
$fileContexts[] = "[Vidéo jointe: {$name} ({$mime}, durée: {$dur})]";
|
|
}
|
|
elseif (preg_match('/\.(docx|xlsx)$/', $name)) {
|
|
// Office docs → basic extraction
|
|
$fileContexts[] = "[Document Office joint: {$name} ({$mime}, " . round($size/1024) . "KB)]";
|
|
}
|
|
else {
|
|
$fileContexts[] = "[Fichier joint: {$name} ({$mime}, " . round($size/1024) . "KB)]";
|
|
}
|
|
}
|
|
}
|
|
|
|
// If only files, no text → auto-generate analysis prompt
|
|
if (empty($message) && $hasFiles) {
|
|
if ($hasImage) {
|
|
$message = "Analyse cette image en détail. Décris ce que tu vois.";
|
|
} else {
|
|
$message = "Analyse le contenu de ce fichier.";
|
|
}
|
|
}
|
|
|
|
if (empty($message)) {
|
|
echo json_encode(['error' => 'No message provided']);
|
|
exit;
|
|
}
|
|
|
|
// ── Build context based on capability ──
|
|
$brainContext = '';
|
|
$capContext = '';
|
|
|
|
// Brain enrichment (auto or capability=brain)
|
|
$brainKeywords = ['brain', 'config', 'inbox', 'isp', 'gmail', 'outlook', 'yahoo', 'hotmail',
|
|
't-online', 'gmx', 'ziggo', 'alice', 'winning', 'combo', 'header', 'mua',
|
|
'encoding', 'x-mailer', 'delivra', 'envoi', 'envoyer', 'send', 'spam',
|
|
'warmup', 'reputation', 'blacklist'];
|
|
$isAboutBrain = ($capability === 'brain');
|
|
if (!$isAboutBrain) {
|
|
$msgLower = strtolower($message);
|
|
foreach ($brainKeywords as $kw) {
|
|
if (strpos($msgLower, $kw) !== false) { $isAboutBrain = true; break; }
|
|
}
|
|
}
|
|
|
|
if ($isAboutBrain) {
|
|
try {
|
|
$winners = getBrainWinners();
|
|
$configs = getBrainConfigs();
|
|
$brainContext = "\n\n=== BRAIN ENGINE DATA ===\n";
|
|
$brainContext .= "Winning Configs (" . count($winners) . " actifs):\n";
|
|
foreach ($winners as $w) {
|
|
$brainContext .= "- ISP:{$w['isp_target']} inbox:{$w['inbox_rate']}% tests:{$w['total_tests']} stab:{$w['stability_score']}% cfg#{$w['config_id']}\n";
|
|
}
|
|
$brainContext .= "=========================\n";
|
|
} catch (Exception $e) {}
|
|
}
|
|
|
|
// KB enrichment
|
|
if ($useKB) {
|
|
try {
|
|
$pdo = getHamidDB();
|
|
$kbResults = $pdo->query("SELECT topic, content FROM admin.sentinel_knowledge ORDER BY created_at DESC LIMIT 15")->fetchAll(PDO::FETCH_ASSOC);
|
|
if ($kbResults) {
|
|
$brainContext .= "\n=== KNOWLEDGE BASE ===\n";
|
|
foreach ($kbResults as $k) {
|
|
$brainContext .= "- {$k['topic']}: " . substr($k['content'], 0, 200) . "\n";
|
|
}
|
|
$brainContext .= "=========================\n";
|
|
}
|
|
} catch (Exception $e) {}
|
|
}
|
|
|
|
// File contexts
|
|
if ($fileContexts) {
|
|
$brainContext .= "\n=== FICHIERS JOINTS ===\n" . implode("\n\n", $fileContexts) . "\n=========================\n";
|
|
}
|
|
|
|
// Capability-specific system prompts
|
|
$capPrompts = [
|
|
'normal' => '',
|
|
'brain' => "\nMode BRAIN+: Analyse approfondie des configurations Brain Engine. Recommande les meilleures combinaisons par ISP.",
|
|
'cot' => "\nMode Chain-of-Thought: Raisonne étape par étape avant de donner ta réponse finale. Montre ton processus de réflexion.",
|
|
'tot' => "\nMode Tree-of-Thought: Explore plusieurs branches de raisonnement, évalue chacune, puis choisis la meilleure.",
|
|
'score' => "\nMode Score: Note de 1 à 10 chaque aspect analysé avec justification.",
|
|
'reflect' => "\nMode Réflexion: Après ta réponse, fais une auto-critique et améliore-la.",
|
|
'doclong' => "\nMode Document Long: Fournis une réponse détaillée et exhaustive.",
|
|
'kb' => "\nMode Knowledge Base: Utilise les données de la base de connaissances WEVADS pour répondre.",
|
|
'rag' => "\nMode RAG: Recherche et utilise les informations pertinentes de la base avant de répondre.",
|
|
'denise' => "\nMode Denise: Tu es Denise, assistante chaleureuse et naturelle. Tu tutoies, tu es amicale et directe.",
|
|
];
|
|
|
|
$systemPrompt = "Tu es HAMID, l'assistant IA de WEVADS — expert email marketing et délivrabilité.
|
|
Tu parles naturellement en français, tu tutoies quand c'est approprié, et tu es direct et utile.
|
|
Tu es expert en: Brain Engine configs, ISP targeting, O365 warmup, headers, bypass filtres, tracking, domaines.
|
|
Tu analyses les fichiers joints (images, PDF, vidéos, textes) quand on t'en envoie.
|
|
Tu donnes des réponses concrètes et actionables." . ($capPrompts[$capability] ?? '') . $brainContext;
|
|
|
|
// ── Call AI: Vision or Text ──
|
|
// === HAMID MEMORY V4 ===
|
|
if ($convId || $session !== 'default') {
|
|
try {
|
|
$memPdo = getHamidDB();
|
|
$memStmt = $memPdo->prepare("SELECT content FROM admin.hamid_conversations WHERE session_id = ? AND content IS NOT NULL ORDER BY created_at DESC LIMIT 5");
|
|
$memStmt->execute([$session]);
|
|
$memRows = $memStmt->fetchAll(PDO::FETCH_COLUMN);
|
|
if ($memRows) {
|
|
$memCtx = "\n[HISTORIQUE]\n";
|
|
foreach (array_reverse($memRows) as $mr) {
|
|
$md = json_decode($mr, true);
|
|
if ($md && !empty($md['user'])) {
|
|
$memCtx .= "User: " . substr($md['user'], 0, 400) . "\n";
|
|
if (!empty($md['assistant'])) $memCtx .= "HAMID: " . substr($md['assistant'], 0, 600) . "\n";
|
|
$memCtx .= "---\n";
|
|
}
|
|
}
|
|
$memCtx .= "[FIN]\nNouveau message: ";
|
|
$message = $memCtx . $message;
|
|
}
|
|
} catch (Exception $e) {}
|
|
}
|
|
// === END HAMID MEMORY V4 ===
|
|
|
|
if ($hasImage && $imageData) {
|
|
// Use Gemini for vision (best multimodal support)
|
|
$result = callVision($message, $imageData, $systemPrompt, $provider);
|
|
} else {
|
|
$result = callWithFailover($message, $provider, $systemPrompt);
|
|
}
|
|
|
|
if (isset($result['error'])) {
|
|
echo json_encode(['status'=>'error', 'error'=>$result['error'], 'details'=>$result['details'] ?? null]);
|
|
exit;
|
|
}
|
|
|
|
// Log conversation
|
|
try {
|
|
$pdo = getHamidDB();
|
|
$pdo->prepare("INSERT INTO admin.hamid_conversations (session_id, role, content, provider, title, updated_at, created_at) VALUES (?, 'assistant', ?, ?, LEFT(?,50), NOW(), NOW())")
|
|
->execute([$session, json_encode(["user"=>$message,"assistant"=>substr($result["response"],0,5000),"files"=>count($fileContexts)]), $result["provider"], $message]);
|
|
// Get conversation_id
|
|
$newConvId = $pdo->lastInsertId();
|
|
} catch (Exception $e) { $newConvId = $convId; }
|
|
|
|
echo json_encode([
|
|
'status' => 'success',
|
|
'response' => $result['response'],
|
|
'provider' => $result['provider'],
|
|
'model' => $result['model'] ?? '',
|
|
'latency_ms' => $result['latency'] ?? 0,
|
|
'brain_enriched' => $isAboutBrain,
|
|
'capability' => $capability,
|
|
'files_processed' => count($fileContexts),
|
|
'conversation_id' => $newConvId,
|
|
'tokens' => strlen($result['response']) / 4
|
|
]);
|
|
break;
|
|
|
|
case 'providers':
|
|
$providers = getProviders();
|
|
$list = array_map(function($p) {
|
|
return ['name'=>$p['provider_name'],'model'=>$p['model'],'active'=>(bool)$p['is_active'],'has_key'=>!empty($p['api_key']),'priority'=>(int)$p['priority']];
|
|
}, $providers);
|
|
echo json_encode(['status'=>'success','providers'=>$list]);
|
|
break;
|
|
|
|
case 'brain':
|
|
$isp = $input['isp'] ?? null;
|
|
$winners = getBrainWinners($isp);
|
|
$configs = getBrainConfigs();
|
|
echo json_encode(['status'=>'success','winners'=>$winners,'configs'=>$configs,'total_winners'=>count($winners),'total_configs'=>count($configs)]);
|
|
break;
|
|
|
|
case 'status':
|
|
$providers = getProviders();
|
|
echo json_encode(['status'=>'success','hamid_online'=>true,'providers_count'=>count($providers),'providers'=>array_column($providers, 'provider_name')]);
|
|
break;
|
|
|
|
case 'quick':
|
|
$type = $input['type'] ?? '';
|
|
$quickPrompts = [
|
|
'config_t_online'=>'Donne-moi la meilleure configuration Brain pour T-Online avec inbox rate >90%',
|
|
'domains_ziggo'=>'Recommande les meilleurs domaines pour Ziggo (Pays-Bas)',
|
|
'headers_gmx'=>'Quels headers optimaux pour GMX en 2026?',
|
|
'warmup_o365'=>'Plan de warmup pour 50 comptes O365 neufs',
|
|
'bypass_cloudmark'=>'Stratégies actuelles pour bypass Cloudmark',
|
|
'config_inbox'=>'Quelles sont toutes les configurations winning actuelles avec leur taux inbox?'
|
|
];
|
|
$message = $quickPrompts[$type] ?? $type;
|
|
if (empty($message)) { echo json_encode(['error'=>'Unknown quick action']); exit; }
|
|
$input['message'] = $message;
|
|
$input['action'] = 'chat';
|
|
$_GET['action'] = 'chat';
|
|
include(__FILE__);
|
|
break;
|
|
|
|
case 'list_conversations':
|
|
$pdo = getHamidDB();
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT id, session_id, content, provider, title, created_at FROM admin.hamid_conversations WHERE content IS NOT NULL AND content != '' ORDER BY created_at DESC LIMIT 50");
|
|
$stmt->execute();
|
|
$convs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$grouped = [];
|
|
foreach ($convs as $c) {
|
|
$date = substr($c["created_at"], 0, 10);
|
|
if (!isset($grouped[$date])) $grouped[$date] = ["id"=>$c["id"], "title"=>$c["title"] ?: substr($c["content"],0,50), "date"=>$date, "messages"=>[]];
|
|
$grouped[$date]["messages"][] = $c;
|
|
}
|
|
echo json_encode(["status"=>"success","conversations"=>array_values($grouped)]);
|
|
} catch (Exception $e) { echo json_encode(["status"=>"success","conversations"=>[]]); }
|
|
break;
|
|
|
|
case 'get_conversation':
|
|
$pdo = getHamidDB();
|
|
$cid = $input["conversation_id"] ?? 0;
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT id, session_id, content, provider, title, created_at FROM admin.hamid_conversations WHERE id = ?::int OR DATE(created_at) = ?::date");
|
|
$stmt->execute([$cid, $cid]);
|
|
echo json_encode(["status"=>"success","messages"=>$stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
|
} catch (Exception $e) { echo json_encode(["status"=>"success","messages"=>[]]); }
|
|
break;
|
|
|
|
default:
|
|
echo json_encode(['status'=>'success','message'=>'HAMID API Ready','actions'=>['chat','providers','brain','status','quick'],'brain_connected'=>true]);
|
|
}
|
|
|
|
// ── Vision function: Gemini multimodal ──
|
|
function callVision($message, $imageData, $systemPrompt, $preferredProvider = null) {
|
|
$providers = getProviders();
|
|
|
|
// Try Gemini first (best vision), then Claude, then OpenRouter
|
|
$visionOrder = ['gemini','claude','openrouter'];
|
|
if ($preferredProvider) array_unshift($visionOrder, strtolower($preferredProvider));
|
|
|
|
foreach ($visionOrder as $vName) {
|
|
foreach ($providers as $p) {
|
|
if (strtolower($p['provider_name']) !== $vName || !$p['is_active'] || empty($p['api_key'])) continue;
|
|
|
|
$start = microtime(true);
|
|
|
|
if ($vName === 'gemini') {
|
|
$model = $p['model'] ?: 'gemini-2.0-flash';
|
|
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$p['api_key']}";
|
|
$payload = json_encode([
|
|
'contents' => [[
|
|
'parts' => [
|
|
['text' => $systemPrompt . "\n\nUser: " . $message],
|
|
['inline_data' => ['mime_type' => $imageData['mime'], 'data' => $imageData['base64']]]
|
|
]
|
|
]]
|
|
]);
|
|
$headers = ['Content-Type: application/json'];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>$payload, CURLOPT_HTTPHEADER=>$headers, CURLOPT_TIMEOUT=>30, CURLOPT_SSL_VERIFYPEER=>false]);
|
|
$resp = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($resp && $code < 400) {
|
|
$data = json_decode($resp, true);
|
|
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
|
if ($text) return ['response'=>$text, 'provider'=>'Gemini Vision', 'model'=>$model, 'latency'=>round((microtime(true)-$start)*1000)];
|
|
}
|
|
}
|
|
elseif ($vName === 'claude') {
|
|
$url = 'https://api.anthropic.com/v1/messages';
|
|
$payload = json_encode([
|
|
'model' => $p['model'] ?: 'claude-3-5-sonnet-20241022',
|
|
'max_tokens' => 1024,
|
|
'system' => $systemPrompt,
|
|
'messages' => [['role'=>'user','content'=>[
|
|
['type'=>'image','source'=>['type'=>'base64','media_type'=>$imageData['mime'],'data'=>$imageData['base64']]],
|
|
['type'=>'text','text'=>$message]
|
|
]]]
|
|
]);
|
|
$headers = ['Content-Type: application/json', 'x-api-key: '.$p['api_key'], 'anthropic-version: 2023-06-01'];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>$payload, CURLOPT_HTTPHEADER=>$headers, CURLOPT_TIMEOUT=>30, CURLOPT_SSL_VERIFYPEER=>false]);
|
|
$resp = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($resp && $code < 400) {
|
|
$data = json_decode($resp, true);
|
|
$text = $data['content'][0]['text'] ?? '';
|
|
if ($text) return ['response'=>$text, 'provider'=>'Claude Vision', 'model'=>$p['model'], 'latency'=>round((microtime(true)-$start)*1000)];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: describe the file in text mode
|
|
$fallbackMsg = $message . "\n\n[Image jointe: {$imageData['name']} - l'image ne peut pas être analysée directement, décris ce que tu sais du contexte]";
|
|
return callWithFailover($fallbackMsg, $preferredProvider, $systemPrompt);
|
|
}
|