Initial commit - Last War messaging system
This commit is contained in:
558
src/IA/Agent.php
Executable file
558
src/IA/Agent.php
Executable file
@@ -0,0 +1,558 @@
|
||||
<?php
|
||||
|
||||
namespace IA;
|
||||
|
||||
require_once __DIR__ . '/../../includes/env_loader.php';
|
||||
|
||||
class Agent
|
||||
{
|
||||
private ?\PDO $kbPdo = null;
|
||||
private array $config = [];
|
||||
private array $activeLanguages = [];
|
||||
private \PDO $mainPdo;
|
||||
|
||||
private const KB_LOG_FILE = __DIR__ . '/../../logs/kb_search.log';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfig();
|
||||
}
|
||||
|
||||
private function loadConfig(): void
|
||||
{
|
||||
require_once __DIR__ . '/../../includes/db.php';
|
||||
$this->mainPdo = getDbConnection();
|
||||
|
||||
$stmt = $this->mainPdo->query("SELECT config_key, config_value FROM ia_agent_config");
|
||||
while ($row = $stmt->fetch()) {
|
||||
$this->config[$row['config_key']] = $row['config_value'];
|
||||
}
|
||||
|
||||
$stmt = $this->mainPdo->query("SELECT language_code, language_name FROM supported_languages WHERE is_active = 1");
|
||||
$this->activeLanguages = $stmt->fetchAll(\PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
private function getKbConnection(): \PDO
|
||||
{
|
||||
if ($this->kbPdo !== null) {
|
||||
return $this->kbPdo;
|
||||
}
|
||||
|
||||
$host = $this->config['kb_db_host'] ?? $_ENV['KB_DB_HOST'] ?? getenv('KB_DB_HOST') ?: 'localhost';
|
||||
$port = $this->config['kb_db_port'] ?? $_ENV['KB_DB_PORT'] ?? getenv('KB_DB_PORT') ?: '3306';
|
||||
$dbname = $this->config['kb_db_name'] ?? $_ENV['KB_DB_NAME'] ?? getenv('KB_DB_NAME') ?: 'lastwar2';
|
||||
$user = $this->config['kb_db_user'] ?? $_ENV['KB_DB_USER'] ?? getenv('KB_DB_USER') ?: 'root';
|
||||
$pass = $this->config['kb_db_pass'] ?? $_ENV['KB_DB_PASS'] ?? getenv('KB_DB_PASS') ?: '';
|
||||
|
||||
error_log("KB Connection - Host: {$host}", 3, self::KB_LOG_FILE);
|
||||
error_log("KB Connection - Port: {$port}", 3, self::KB_LOG_FILE);
|
||||
error_log("KB Connection - DBName: {$dbname}", 3, self::KB_LOG_FILE);
|
||||
error_log("KB Connection - User: {$user}", 3, self::KB_LOG_FILE);
|
||||
// Do NOT log password for security reasons
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset=utf8mb4";
|
||||
$this->kbPdo = new \PDO($dsn, $user, $pass, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||||
]);
|
||||
error_log("KB Connection - PDO connection established successfully.", 3, self::KB_LOG_FILE);
|
||||
} catch (\PDOException $e) {
|
||||
error_log("KB Connection Error: " . $e->getMessage(), 3, self::KB_LOG_FILE);
|
||||
throw $e; // Re-throw to ensure the original error is propagated
|
||||
}
|
||||
|
||||
|
||||
return $this->kbPdo;
|
||||
}
|
||||
|
||||
public function getConfig(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->config[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function updateConfig(string $key, string $value): bool
|
||||
{
|
||||
$stmt = $this->mainPdo->prepare("
|
||||
INSERT INTO ia_agent_config (config_key, config_value) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)
|
||||
");
|
||||
return $stmt->execute([$key, $value]);
|
||||
}
|
||||
|
||||
public function getAllConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getActiveLanguages(): array
|
||||
{
|
||||
return $this->activeLanguages;
|
||||
}
|
||||
|
||||
public function isKbEnabled(): bool
|
||||
{
|
||||
return ($this->config['kb_enabled'] ?? '1') === '1';
|
||||
}
|
||||
|
||||
public function searchKnowledgeBase(string $query, string $userLanguage): array
|
||||
{
|
||||
if (!$this->isKbEnabled()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = $this->getKbConnection();
|
||||
$maxResults = (int)($this->config['kb_max_results'] ?? 5);
|
||||
|
||||
error_log("KB Search - User Query: " . $query, 3, self::KB_LOG_FILE);
|
||||
|
||||
$queryMappings = [
|
||||
'hero' => ['Heroes', 'hero'],
|
||||
'heroes' => ['Heroes', 'hero'],
|
||||
'zombie' => ['General', 'zombie'],
|
||||
'zombies' => ['General', 'zombie'],
|
||||
'invasion' => ['General', 'invasion'],
|
||||
'invasión' => ['General', 'zombie invasion'],
|
||||
'ghost' => ['Heroes', 'ghost'],
|
||||
'weapon' => ['Heroes', 'weapon'],
|
||||
'weapons' => ['Heroes', 'weapon'],
|
||||
'skill' => ['Heroes', 'skill'],
|
||||
'skills' => ['Heroes', 'skill'],
|
||||
'season' => ['General', 'season'],
|
||||
'temporada' => ['General', 'season'],
|
||||
'vip' => ['General', 'vip'],
|
||||
'battle' => ['General', 'battle'],
|
||||
'sky' => ['General', 'sky'],
|
||||
'alliance' => ['General', 'alliance'],
|
||||
'alianza' => ['General', 'alliance'],
|
||||
'building' => ['General', 'building'],
|
||||
'edificio' => ['General', 'building'],
|
||||
'troop' => ['General', 'troop'],
|
||||
'tropa' => ['General', 'troop'],
|
||||
];
|
||||
|
||||
$searchQuery = mb_strtolower($query, 'UTF-8');
|
||||
$foundMapping = null;
|
||||
|
||||
foreach ($queryMappings as $key => $mapping) {
|
||||
if (strpos($searchQuery, $key) !== false) {
|
||||
$foundMapping = $mapping;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
error_log("KB Search - Found Mapping: " . ($foundMapping ? json_encode($foundMapping) : 'None'), 3, self::KB_LOG_FILE);
|
||||
|
||||
if ($foundMapping) {
|
||||
list($topic, $term) = $foundMapping;
|
||||
|
||||
if (in_array($topic, ['General', 'Season 1', 'Season 2', 'Season 3', 'Season 4', 'Season 5']) &&
|
||||
(strpos($searchQuery, 'season') !== false || strpos($searchQuery, 'temporada') !== false)) {
|
||||
if (preg_match('/(season|temporada)\s*(\d+)/i', $searchQuery, $matches)) {
|
||||
$term = $matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
error_log("KB Search - Term after season adjustment: " . $term, 3, self::KB_LOG_FILE);
|
||||
|
||||
// If user language is Spanish and query is for a season, translate term to English
|
||||
if ($userLanguage === 'es' && (strpos($term, 'temporada') !== false)) {
|
||||
if (preg_match('/(temporada)\s*(\d+)/i', $term, $matches)) {
|
||||
$seasonNumber = $matches[2];
|
||||
$term = "season " . $seasonNumber;
|
||||
error_log("KB Search - Translated Spanish season term to English: " . $term, 3, self::KB_LOG_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT id, topic, entity_type, entity_name, content_preview, article_url, article_title
|
||||
FROM knowledge_base
|
||||
WHERE
|
||||
(entity_name = ? OR article_title LIKE ? OR content_preview LIKE ?)
|
||||
OR (entity_name LIKE ? OR article_title LIKE ? OR content_preview LIKE ?)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN entity_name = ? THEN 0
|
||||
WHEN article_title LIKE ? THEN 1
|
||||
WHEN entity_name LIKE ? THEN 2
|
||||
WHEN article_title LIKE ? THEN 3
|
||||
WHEN content_preview LIKE ? THEN 4
|
||||
ELSE 5
|
||||
END,
|
||||
LENGTH(content_preview) DESC,
|
||||
LENGTH(article_title) ASC
|
||||
LIMIT {$maxResults}
|
||||
";
|
||||
|
||||
$params = [
|
||||
$term, // Exact match for entity_name
|
||||
"{$term}%", // Prefix match for article_title
|
||||
"%{$term}%", // Broad match for content_preview
|
||||
"%{$term}%", // Broad match for entity_name
|
||||
"%{$term}%", // Broad match for article_title
|
||||
"%{$term}%", // Broad match for content_preview
|
||||
$term, // For ORDER BY: Exact match for entity_name
|
||||
"{$term}%", // For ORDER BY: Prefix match for article_title
|
||||
"%{$term}%", // For ORDER BY: Broad match for entity_name
|
||||
"%{$term}%", // For ORDER BY: Broad match for article_title
|
||||
"%{$term}%" // For ORDER BY: Broad match for content_preview
|
||||
];
|
||||
|
||||
error_log("KB Search - Mapped SQL: " . $sql, 3, self::KB_LOG_FILE);
|
||||
error_log("KB Search - Mapped Params: " . json_encode($params), 3, self::KB_LOG_FILE);
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$results = $stmt->fetchAll();
|
||||
|
||||
error_log("KB Search - Mapped Results: " . json_encode($results), 3, self::KB_LOG_FILE);
|
||||
|
||||
if (!empty($results)) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
$stopWords = ['me', 'puedes', 'el', 'la', 'los', 'las', 'un', 'una', 'de', 'del', 'en', 'con', 'para', 'por', 'sobre', 'que', 'es', 'son', 'this', 'the', 'a', 'an', 'is', 'are', 'can', 'could', 'would', 'tell', 'name', 'list', 'show', 'what', 'how', 'can', 'you', 'say', 'tell'];
|
||||
$words = explode(' ', $searchQuery);
|
||||
$words = array_filter($words, fn($w) => !in_array($w, $stopWords) && strlen($w) > 2);
|
||||
$searchTerms = array_values($words);
|
||||
|
||||
if (empty($searchTerms)) {
|
||||
$searchTerms = [$searchQuery];
|
||||
}
|
||||
|
||||
error_log("KB Search - Fallback Search Terms: " . json_encode($searchTerms), 3, self::KB_LOG_FILE);
|
||||
|
||||
$primaryTerm = $searchTerms[0];
|
||||
|
||||
$sql = "
|
||||
SELECT id, topic, entity_type, entity_name, content_preview, article_url, article_title
|
||||
FROM knowledge_base
|
||||
WHERE " . implode(' AND ', array_map(fn($term) => "(entity_name LIKE ? OR article_title LIKE ? OR content_preview LIKE ?)", $searchTerms)) . "
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN entity_name LIKE ? THEN 0
|
||||
WHEN article_title LIKE ? THEN 1
|
||||
WHEN content_preview LIKE ? THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
LENGTH(content_preview) DESC
|
||||
LIMIT {$maxResults}
|
||||
";
|
||||
|
||||
$params = [];
|
||||
foreach ($searchTerms as $term) {
|
||||
$params[] = "%{$term}%";
|
||||
$params[] = "%{$term}%";
|
||||
$params[] = "%{$term}%";
|
||||
}
|
||||
$params[] = "%{$primaryTerm}%";
|
||||
$params[] = "%{$primaryTerm}%";
|
||||
$params[] = "%{$primaryTerm}%";
|
||||
|
||||
error_log("KB Search - Fallback SQL: " . $sql, 3, self::KB_LOG_FILE);
|
||||
error_log("KB Search - Fallback Params: " . json_encode($params), 3, self::KB_LOG_FILE);
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
$results = $stmt->fetchAll();
|
||||
error_log("KB Search - Fallback Results: " . json_encode($results), 3, self::KB_LOG_FILE);
|
||||
|
||||
return $stmt->fetchAll();
|
||||
} catch (\Exception $e) {
|
||||
error_log("KB Search Error: " . $e->getMessage(), 3, self::KB_LOG_FILE);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function generateResponse(string $userMessage): string
|
||||
{
|
||||
$userLanguage = $this->detectLanguage($userMessage);
|
||||
|
||||
$kbResults = $this->searchKnowledgeBase($userMessage, $userLanguage);
|
||||
|
||||
if (empty($kbResults)) {
|
||||
$kbResults = $this->searchKnowledgeBase('hero zombie ghost', $userLanguage);
|
||||
}
|
||||
|
||||
if (empty($kbResults)) {
|
||||
return $userLanguage === 'es'
|
||||
? "Soy tu asistente del juego Last War: Survival. Puedo ayudarte con información sobre héroes, eventos, estrategias y más del juego. ¿Sobre qué tema específico quieres saber más?"
|
||||
: "I'm your assistant for the game Last War: Survival. I can help you with information about heroes, events, strategies and more. What specific topic would you like to know more about?";
|
||||
}
|
||||
|
||||
return $this->generateWithAI($userMessage, $kbResults, $userLanguage);
|
||||
}
|
||||
|
||||
private function generateWithAI(string $question, array $kbResults, string $userLanguage): string
|
||||
{
|
||||
if (empty($kbResults)) {
|
||||
return $this->buildFallbackResponse($kbResults, $userLanguage);
|
||||
}
|
||||
|
||||
$context = $this->buildContext($kbResults);
|
||||
|
||||
$lastWarTerms = ['last war', 'lastwar', 'last war survival', 'lw', 'last war: survival', 'heroes', 'hero', 'héroe', 'héroes', 'zombie', 'invasion', 'battle', 'alliance', 'alianza', 'edificio', 'building', 'tropa', 'troop', 'temporada', 'season', 'skill', 'weapon', 'vip'];
|
||||
$questionLower = mb_strtolower($question, 'UTF-8');
|
||||
|
||||
$isAboutLastWar = false;
|
||||
foreach ($lastWarTerms as $term) {
|
||||
if (strpos($questionLower, $term) !== false) {
|
||||
$isAboutLastWar = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$isGenericQuestion = preg_match('/^(que|what|me|dime|tell|como|how|cual|which|dame|about|cuales|cual es|como se|cuales son)\b/i', trim($question));
|
||||
|
||||
if (!$isAboutLastWar && !$isGenericQuestion) {
|
||||
if ($userLanguage === 'es') {
|
||||
return "Solo puedo ayudarte con información sobre el juego **Last War: Survival**. ¿Tienes alguna pregunta sobre este juego?";
|
||||
} elseif ($userLanguage === 'pt') {
|
||||
return "Só posso ajudá-lo com informações sobre o jogo **Last War: Survival**. Você tem alguma pergunta sobre este jogo?";
|
||||
} else {
|
||||
return "I can only help you with information about the game **Last War: Survival**. Do you have any questions about this jogo?";
|
||||
}
|
||||
}
|
||||
|
||||
$langNames = [
|
||||
'es' => 'ESPAÑOL',
|
||||
'en' => 'ENGLISH',
|
||||
'pt' => 'PORTUGUÊS',
|
||||
'fr' => 'FRANÇAIS',
|
||||
'de' => 'DEUTSCH',
|
||||
'it' => 'ITALIANO',
|
||||
'ru' => 'РУССКИЙ',
|
||||
'zh' => '中文',
|
||||
'ja' => '日本語',
|
||||
'ko' => '한국어'
|
||||
];
|
||||
|
||||
$langName = $langNames[$userLanguage] ?? strtoupper($userLanguage);
|
||||
$langInstruction = "IMPORTANTE: El usuario escribe en {$langName}. Debes responder SIEMPRE en {$langName}. NO mezcles idiomas. Toda la respuesta debe ser en {$langName}.";
|
||||
|
||||
$customPrompt = $this->config['system_prompt'] ?? '';
|
||||
|
||||
$systemPrompt = $langInstruction . "\n\n" . $customPrompt;
|
||||
|
||||
$userPrompt = match($userLanguage) {
|
||||
'es' => "Pregunta del jugador: {$question}\n\nInformación del juego:\n{$context}\n\nResponde en ESPAÑOL de forma clara y útil:",
|
||||
'pt' => "Pergunta do jogador: {$question}\n\nInformações do jogo:\n{$context}\n\nResponda em PORTUGUÊS de forma clara e útil:",
|
||||
'fr' => "Question du joueur: {$question}\n\nInformations sur le jeu:\n{$context}\n\nRépondez en FRANÇAIS clairement et utilement:",
|
||||
'de' => "Frage des Spielers: {$question}\n\nSpielinformationen:\n{$context}\n\nAntworten Sie klar und hilfreich auf DEUTSCH:",
|
||||
'it' => "Domanda del giocatore: {$question}\n\nInformazioni sul gioco:\n{$context}\n\nRispondi in ITALIANO in modo chiaro e utile:",
|
||||
default => "Player question: {$question}\n\nGame information:\n{$context}\n\nAnswer in ENGLISH clearly and helpfully:"
|
||||
};
|
||||
|
||||
return $this->callGroqAPI($systemPrompt, $userPrompt, $kbResults, $userLanguage);
|
||||
}
|
||||
|
||||
private function callGroqAPI(string $systemPrompt, string $userPrompt, array $kbResults, string $userLanguage, int $attempt = 0): string
|
||||
{
|
||||
$freeModels = ['llama-3.1-8b-instant', 'mixtral-8x7b-32768', 'llama3-70b-8192'];
|
||||
|
||||
$currentModel = $this->config['ai_model'] ?? 'llama-3.1-8b-instant';
|
||||
|
||||
if (!in_array($currentModel, $freeModels)) {
|
||||
$currentModel = 'llama-3.1-8b-instant';
|
||||
}
|
||||
|
||||
try {
|
||||
$apiKey = $_ENV['GROQ_API_KEY'] ?? getenv('GROQ_API_KEY');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
return $this->buildFallbackResponse($kbResults, $userLanguage);
|
||||
}
|
||||
|
||||
$url = 'https://api.groq.com/openai/v1/chat/completions';
|
||||
|
||||
$data = [
|
||||
'model' => $currentModel,
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $userPrompt]
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 1024,
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $apiKey
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
$result = json_decode($response, true);
|
||||
$aiResponse = $result['choices'][0]['message']['content'] ?? '';
|
||||
|
||||
if (!empty($aiResponse)) {
|
||||
return $aiResponse;
|
||||
}
|
||||
} elseif ($httpCode === 429 || strpos($response, 'quota') !== false) {
|
||||
error_log("Groq API: Rate limit or quota exceeded, trying next model");
|
||||
|
||||
if ($attempt < count($freeModels) - 1) {
|
||||
$nextModel = $freeModels[$attempt + 1];
|
||||
$this->updateConfig('ai_model', $nextModel);
|
||||
return $this->callGroqAPI($systemPrompt, $userPrompt, $kbResults, $userLanguage, $attempt + 1);
|
||||
}
|
||||
} else {
|
||||
error_log("Groq API Error: HTTP {$httpCode} - {$response}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Groq Error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->buildFallbackResponse($kbResults, $userLanguage);
|
||||
}
|
||||
|
||||
private function buildContext(array $results): string
|
||||
{
|
||||
$context = "INFORMACIÓN DE LA BASE DE CONOCIMIENTOS DEL JUEGO:\n\n";
|
||||
|
||||
$context .= "TEMAS PRINCIPALES DEL JUEGO:\n";
|
||||
$context .= "- Héroes: Mason (Raging Marksman), Murphy, Kimberly, Blade, Shadow Marshall, Striker Carlie, Thunder Tesla, Williams, Schuyler, Morrison, Lucius, DVA\n";
|
||||
$context .= "- Tipos de héroes: Defense (Defensa), Attack (Ataque), Support (Apoyo)\n";
|
||||
$context .= "- Eventos: Zombie Invasion, Ghost Ops, Sky Battlefront, Season events\n\n";
|
||||
|
||||
$uniqueArticles = [];
|
||||
foreach ($results as $result) {
|
||||
$url = $result['article_url'] ?? '';
|
||||
if (!isset($uniqueArticles[$url])) {
|
||||
$uniqueArticles[$url] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uniqueArticles as $result) {
|
||||
$title = $result['article_title'] ?? 'Sin título';
|
||||
$content = $result['content_preview'] ?? '';
|
||||
$topic = $result['topic'] ?? '';
|
||||
|
||||
if (!empty($content)) {
|
||||
$context .= "--- {$topic}: {$title} ---\n";
|
||||
$context .= $content . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
private function buildFallbackResponse(array $kbResults, string $userLanguage): string
|
||||
{
|
||||
$context = $this->buildContext($kbResults);
|
||||
|
||||
if ($userLanguage === 'es') {
|
||||
return "Aquí te comparto información sobre lo que preguntaste:\n\n" . $context;
|
||||
}
|
||||
|
||||
return "Here's the information I found:\n\n" . $context;
|
||||
}
|
||||
|
||||
private function detectLanguage(string $text): string
|
||||
{
|
||||
$textLower = mb_strtolower($text, 'UTF-8');
|
||||
|
||||
$scores = [];
|
||||
|
||||
foreach ($this->activeLanguages as $code => $name) {
|
||||
$scores[$code] = 0;
|
||||
|
||||
switch ($code) {
|
||||
case 'pt':
|
||||
$words = ['você', 'dizer', 'jogo', 'pode', 'tem', 'quer', 'quando', 'onde', 'quem', 'ajuda', 'obrigado', 'olá', 'dos', 'das', 'seu', 'sua', 'nosso', 'nossa', 'é', 'são', 'está', 'estão', 'disse', 'fazer', 'essa', 'esse', 'bem', 'aqui', 'muito', 'pelo', 'pela', 'diz', 'sobre', 'heroes'];
|
||||
$scores[$code] = preg_match_all('/\b(' . implode('|', $words) . ')\b/i', $textLower, $matches);
|
||||
if (preg_match('/[ãõç]/i', $text)) $scores[$code] += 5;
|
||||
if (strpos($textLower, 'diz') !== false || strpos($textLower, 'você') !== false) $scores[$code] += 3;
|
||||
break;
|
||||
|
||||
case 'es':
|
||||
$words = ['que', 'del', 'los', 'las', 'para', 'como', 'juego', 'puedo', 'tengo', 'quiero', 'dime', 'me', 'cuando', 'cual', 'donde', 'quien', 'por', 'porque', 'ayuda', 'gracias', 'hola', 'decir', 'qué', 'este', 'esta', 'están', 'son', 'está', 'hay', 'hacer', 'tienes', 'aquí', 'más', 'muy', 'heroes'];
|
||||
$scores[$code] = preg_match_all('/\b(' . implode('|', $words) . ')\b/i', $textLower, $matches);
|
||||
if (preg_match('/[áéíóúñü]/i', $text) && !preg_match('/[ãõç]/i', $text)) $scores[$code] += 2;
|
||||
break;
|
||||
|
||||
case 'en':
|
||||
$words = ['what', 'can', 'you', 'tell', 'me', 'about', 'the', 'game', 'how', 'when', 'where', 'who', 'is', 'are', 'has', 'have', 'hello', 'thanks', 'hero', 'heroes', 'this', 'that', 'these', 'those'];
|
||||
$scores[$code] = preg_match_all('/\b(' . implode('|', $words) . ')\b/i', $textLower, $matches);
|
||||
if (!preg_match('/[áéíóúñãõçàèéìíòóùâêîôû]/i', $text)) $scores[$code] += 1;
|
||||
break;
|
||||
|
||||
case 'fr':
|
||||
$words = ['vous', 'dire', 'jeu', 'peut', 'est', 'sont', 'avez', 'bonjour', 'merci', 'quoi', 'comment', 'où', 'quand', 'les', 'des', 'pour', 'une', 'sur', 'cette', 'ce', 'ces', 'plus', 'héros'];
|
||||
$scores[$code] = preg_match_all('/\b(' . implode('|', $words) . ')\b/i', $textLower, $matches);
|
||||
if (preg_match('/[àâçéèêëîïôùûü]/i', $text)) $scores[$code] += 3;
|
||||
break;
|
||||
|
||||
case 'de':
|
||||
$words = ['sie', 'können', 'spiel', 'ist', 'sind', 'haben', 'danke', 'hallo', 'was', 'wie', 'wo', 'wann', 'der', 'die', 'das', 'ein', 'eine', 'für', 'dieser', 'diese', 'dieses', 'helden'];
|
||||
$scores[$code] = preg_match_all('/\b(' . implode('|', $words) . ')\b/i', $textLower, $matches);
|
||||
if (preg_match('/[äöüß]/i', $text)) $scores[$code] += 3;
|
||||
break;
|
||||
|
||||
case 'it':
|
||||
$words = ['puoi', 'gioco', 'è', 'sono', 'hai', 'grazie', 'ciao', 'cosa', 'come', 'dove', 'quando', 'gli', 'che', 'del', 'per', 'una', 'questo', 'questa', 'più', 'eroi'];
|
||||
$scores[$code] = preg_match_all('/\b(' . implode('|', $words) . ')\b/i', $textLower, $matches);
|
||||
if (preg_match('/[àèéìíòóù]/i', $text)) $scores[$code] += 3;
|
||||
break;
|
||||
|
||||
case 'ru':
|
||||
$scores[$code] = preg_match_all('/[а-я]/i', $text, $matches);
|
||||
break;
|
||||
|
||||
case 'zh':
|
||||
$scores[$code] = preg_match_all('/[\x{4e00}-\x{9fff}]/u', $text, $matches);
|
||||
break;
|
||||
|
||||
case 'ja':
|
||||
$scores[$code] = preg_match_all('/[\x{3040}-\x{309f}\x{30a0}-\x{30ff}]/u', $text, $matches);
|
||||
break;
|
||||
|
||||
case 'ko':
|
||||
$scores[$code] = preg_match_all('/[\x{ac00}-\x{d7af}\x{1100}-\x{11ff}]/u', $text, $matches);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$maxScore = 0;
|
||||
$detectedLang = 'es';
|
||||
|
||||
foreach ($scores as $code => $score) {
|
||||
if ($score > $maxScore) {
|
||||
$maxScore = $score;
|
||||
$detectedLang = $code;
|
||||
}
|
||||
}
|
||||
|
||||
return $detectedLang;
|
||||
}
|
||||
|
||||
public function testKbConnection(): array
|
||||
{
|
||||
try {
|
||||
$pdo = $this->getKbConnection();
|
||||
$stmt = $pdo->query("SELECT COUNT(*) as count FROM knowledge_base");
|
||||
$result = $stmt->fetch();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Conexión exitosa. Total de artículos en KB: " . $result['count']
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Error de conexión: " . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
134
src/Translate.php
Executable file
134
src/Translate.php
Executable file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace src;
|
||||
|
||||
class Translate
|
||||
{
|
||||
private string $url;
|
||||
private array $supportedLanguages = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->url = $_ENV['LIBRETRANSLATE_URL'] ?? getenv('LIBRETRANSLATE_URL') ?? 'http://localhost:5000';
|
||||
}
|
||||
|
||||
public function detectLanguage(string $text): ?string
|
||||
{
|
||||
try {
|
||||
$response = $this->request('/detect', ['q' => $text]);
|
||||
|
||||
if (!empty($response)) {
|
||||
return $response[0]['language'] ?? null;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Language detection error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function translate(string $text, string $sourceLang, string $targetLang): ?string
|
||||
{
|
||||
if ($sourceLang === $targetLang) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
try {
|
||||
$lines = explode("\n", $text);
|
||||
$translatedLines = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (trim($line) === '') {
|
||||
$translatedLines[] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$response = $this->request('/translate', [
|
||||
'q' => trim($line),
|
||||
'source' => $sourceLang,
|
||||
'target' => $targetLang,
|
||||
'format' => 'text'
|
||||
]);
|
||||
|
||||
$translatedLines[] = $response['translatedText'] ?? trim($line);
|
||||
}
|
||||
|
||||
return implode("\n", $translatedLines);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Translation error: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function translateToMultiple(string $text, string $sourceLang, array $targetLangs): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($targetLangs as $lang) {
|
||||
$results[$lang] = $this->translate($text, $sourceLang, $lang);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getSupportedLanguages(): array
|
||||
{
|
||||
if (!empty($this->supportedLanguages)) {
|
||||
return $this->supportedLanguages;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->request('/languages');
|
||||
$this->supportedLanguages = $response;
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Get languages error: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function isLanguageSupported(string $langCode): bool
|
||||
{
|
||||
$languages = $this->getSupportedLanguages();
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
if ($lang['code'] === $langCode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function request(string $endpoint, array $data = []): array
|
||||
{
|
||||
$url = $this->url . $endpoint;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
if (!empty($data)) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/x-www-form-urlencoded'
|
||||
]);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new \Exception("LibreTranslate API Error: HTTP $httpCode");
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($result === null) {
|
||||
throw new \Exception("Invalid JSON response from LibreTranslate");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user