Returning an error string caused the bot to send "Lo siento, tengo problemas para procesar tu mensaje" to the user whenever OpenAI/Gemini was unavailable or misconfigured. Now it returns null so BotRouter can fall back to the category greeting menu instead of sending the error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 lines
6.9 KiB
PHP
200 lines
6.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
class AiBot
|
|
{
|
|
public static function process(array $company, array $context, string $input): ?array
|
|
{
|
|
$botCtx = ConversationContext::getOrCreate((int)$company['id'], $context['from'], 'ai');
|
|
$ctxId = (int)$botCtx['id'];
|
|
|
|
$config = self::getConfig($company);
|
|
$systemPrompt = $config['ai_prompt'] ?? self::defaultPrompt($company);
|
|
|
|
ConversationContext::addAiMessage($ctxId, ['role' => 'user', 'content' => $input]);
|
|
|
|
$result = self::callLlm($systemPrompt, $ctxId, $company);
|
|
|
|
if ($result === null) {
|
|
return null;
|
|
}
|
|
|
|
ConversationContext::addAiMessage($ctxId, ['role' => 'assistant', 'content' => $result['content'] ?? '']);
|
|
|
|
return [
|
|
'action' => 'send',
|
|
'type' => 'text',
|
|
'to' => $context['from'],
|
|
'payload' => json_encode(['text' => $result['content'] ?? '']),
|
|
];
|
|
}
|
|
|
|
private static function callLlm(string $systemPrompt, int $ctxId, array $company): ?array
|
|
{
|
|
$provider = env('AI_PROVIDER', 'openai');
|
|
$history = ConversationContext::getAiHistory($ctxId);
|
|
|
|
return match ($provider) {
|
|
'openai' => self::callOpenAI($systemPrompt, $history, $company),
|
|
'gemini' => self::callGemini($systemPrompt, $history),
|
|
'mock' => self::mockResponse($history),
|
|
default => self::callOpenAI($systemPrompt, $history, $company),
|
|
};
|
|
}
|
|
|
|
private static function callOpenAI(string $systemPrompt, array $history, array $company): ?array
|
|
{
|
|
$apiKey = env('OPENAI_API_KEY', '');
|
|
if ($apiKey === '') {
|
|
return null;
|
|
}
|
|
|
|
$model = env('OPENAI_MODEL', 'gpt-4o-mini');
|
|
|
|
$messages = [
|
|
['role' => 'system', 'content' => $systemPrompt],
|
|
];
|
|
|
|
foreach ($history as $msg) {
|
|
$messages[] = [
|
|
'role' => $msg['role'] ?? 'user',
|
|
'content' => $msg['content'] ?? '',
|
|
];
|
|
}
|
|
|
|
$payload = json_encode([
|
|
'model' => $model,
|
|
'messages' => $messages,
|
|
'max_tokens' => (int)env('AI_MAX_TOKENS', '500'),
|
|
'temperature' => 0.7,
|
|
]);
|
|
|
|
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $apiKey,
|
|
],
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200 || $response === false) {
|
|
WpWebhook::log('ERROR', "OpenAI API error: {$error} HTTP:{$httpCode}");
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
$text = $data['choices'][0]['message']['content'] ?? null;
|
|
|
|
return $text !== null ? ['content' => $text] : null;
|
|
}
|
|
|
|
private static function callGemini(string $systemPrompt, array $history): ?array
|
|
{
|
|
$apiKey = env('GEMINI_API_KEY', '');
|
|
if ($apiKey === '') return null;
|
|
|
|
$model = env('GEMINI_MODEL', 'gemini-2.0-flash');
|
|
$maxTokens = (int)env('AI_MAX_TOKENS', '500');
|
|
|
|
// Gemini usa "contents" con roles "user"/"model"
|
|
$contents = [];
|
|
foreach ($history as $msg) {
|
|
$role = ($msg['role'] ?? 'user') === 'assistant' ? 'model' : 'user';
|
|
$contents[] = ['role' => $role, 'parts' => [['text' => $msg['content'] ?? '']]];
|
|
}
|
|
// Si el historial está vacío o el último es del model, Gemini requiere que el último sea user
|
|
if (empty($contents)) {
|
|
$contents[] = ['role' => 'user', 'parts' => [['text' => '']]];
|
|
}
|
|
|
|
$payload = json_encode([
|
|
'system_instruction' => ['parts' => [['text' => $systemPrompt]]],
|
|
'contents' => $contents,
|
|
'generationConfig' => ['maxOutputTokens' => $maxTokens, 'temperature' => 0.7],
|
|
]);
|
|
|
|
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200 || $response === false) {
|
|
WpWebhook::log('ERROR', "Gemini API error: {$error} HTTP:{$httpCode}");
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
|
|
|
return $text !== null ? ['content' => $text] : null;
|
|
}
|
|
|
|
private static function mockResponse(array $history): array
|
|
{
|
|
$last = end($history);
|
|
$input = $last['content'] ?? '';
|
|
|
|
$responses = [
|
|
'hola' => '¡Hola! ¿En qué puedo ayudarte hoy?',
|
|
'gracias' => '¡De nada! Si necesitas algo más, estoy aquí.',
|
|
'adios' => '¡Hasta luego! Que tengas un excelente día.',
|
|
'default' => "Gracias por tu mensaje. He recibido: \"{$input}\". Un asesor se pondrá en contacto contigo pronto.",
|
|
];
|
|
|
|
$normalized = mb_strtolower(trim($input));
|
|
$found = $responses['default'];
|
|
|
|
foreach ($responses as $keyword => $response) {
|
|
if (str_contains($normalized, $keyword)) {
|
|
$found = $response;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ['content' => $found];
|
|
}
|
|
|
|
private static function defaultPrompt(array $company): string
|
|
{
|
|
$name = $company['display_name'] ?? $company['name'] ?? 'la empresa';
|
|
return <<<PROMPT
|
|
Eres un asistente virtual de {$name}. Tu rol es:
|
|
|
|
1. Responder preguntas sobre los servicios y productos de {$name}.
|
|
2. Ayudar a los clientes con información general.
|
|
3. Ser amable, profesional y responder siempre en español.
|
|
4. Si no sabes la respuesta, indica que un asesor se comunicará.
|
|
5. No inventes información. Si no sabes algo, dilo honestamente.
|
|
|
|
Mantén las respuestas concisas (máximo 3 párrafos).
|
|
PROMPT;
|
|
}
|
|
|
|
private static function getConfig(array $company): array
|
|
{
|
|
$json = $company['config_json'] ?? '';
|
|
if ($json === '') return [];
|
|
$config = json_decode($json, true);
|
|
return is_array($config) ? $config : [];
|
|
}
|
|
}
|