Files
wevads-platform/scripts/hamid-api.php
2026-02-26 04:53:11 +01:00

379 lines
18 KiB
PHP
Executable File

<?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 ──
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);
}