El asistente está para ayudar a usar el sistema, no a mantenerlo: la documentación técnica, de arquitectura y de operación queda fuera de su contexto incluso para administradores, que pueden leerla directamente en el módulo. El filtrado por rol sigue aplicándose dentro del manual. Al restringirlo apareció que la relevancia era débil: una pregunta sobre respaldos devolvía Recepción y Kiosko porque palabras comunes como "paciente" suman igual en todos los documentos. Ahora cada palabra pesa según en cuántos documentos aparece, y se descartan los resultados que no superen un umbral absoluto ni queden cerca del mejor. Una pregunta fuera del manual devuelve cero fuentes y LIA lo dice, en vez de responder con lo que tenga a mano. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
4.1 KiB
PHP
102 lines
4.1 KiB
PHP
<?php
|
|
/**
|
|
* modules/soporte/api/ai_docs.php
|
|
* POST { pregunta: string, historial?: [{rol,texto},...] }
|
|
*
|
|
* LIA respondiendo únicamente sobre la documentación del sistema. El contexto
|
|
* lo arma DocIndex, que filtra por rol: solo entra lo que este usuario podría
|
|
* leer por su cuenta en el módulo Soporte. No accede a datos de pacientes ni
|
|
* de la operación del día.
|
|
*/
|
|
|
|
require_once __DIR__ . '/../../../config/config.php';
|
|
require_once __DIR__ . '/../../../services/GeminiService.php';
|
|
require_once __DIR__ . '/../DocIndex.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
function salir(array $datos, int $codigo = 200): void
|
|
{
|
|
http_response_code($codigo);
|
|
echo json_encode($datos, JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
if (!isUserLoggedIn()) salir(['ok' => false, 'error' => 'No autenticado'], 401);
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') salir(['ok' => false, 'error' => 'Método no permitido'], 405);
|
|
|
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
$pregunta = trim($body['pregunta'] ?? '');
|
|
|
|
if ($pregunta === '') salir(['ok' => false, 'error' => 'Pregunta vacía'], 400);
|
|
if (mb_strlen($pregunta) > 800) salir(['ok' => false, 'error' => 'Pregunta demasiado larga'], 400);
|
|
|
|
$gemini = new GeminiService(Database::getInstance()->getConnection());
|
|
|
|
if (!$gemini->hayClave()) {
|
|
salir(['ok' => false, 'error' => 'El asistente no está configurado. Avisa a un administrador.'], 503);
|
|
}
|
|
if ($gemini->presupuestoAgotado()) {
|
|
salir(['ok' => false, 'error' => '⚠️ Se agotaron los tokens de LIA. Solicita tokens a soporte para continuar.'], 402);
|
|
}
|
|
|
|
// ── Documentación visible para este usuario, acotada a la pregunta ──
|
|
[$contexto, $titulos] = DocIndex::contextoIA($pregunta);
|
|
|
|
if ($contexto === '') {
|
|
salir([
|
|
'ok' => true,
|
|
'respuesta' => 'Eso no está en el manual de usuario. Pruebe con otras palabras, '
|
|
. 'o revise el índice de la izquierda: puede estar en otra sección '
|
|
. 'de la documentación.',
|
|
'fuentes' => [],
|
|
]);
|
|
}
|
|
|
|
// ── Historial de la conversación ───────────────────────────────
|
|
$historial = [];
|
|
foreach (array_slice(is_array($body['historial'] ?? null) ? $body['historial'] : [], -6) as $h) {
|
|
$texto = trim((string)($h['texto'] ?? ''));
|
|
if ($texto === '') continue;
|
|
$historial[] = [
|
|
'role' => (($h['rol'] ?? '') === 'model') ? 'model' : 'user',
|
|
'parts' => [['text' => mb_substr($texto, 0, 1500)]],
|
|
];
|
|
}
|
|
|
|
$prompt = <<<PROMPT
|
|
Eres LIA, la asistente de documentación del ERP del Laboratorio Clínico Ximena Caicedo.
|
|
|
|
Responde preguntas sobre CÓMO USAR el sistema, apoyándose ÚNICAMENTE en el manual de usuario
|
|
incluido abajo. Reglas:
|
|
|
|
- Si la respuesta no está en la documentación, dilo con claridad. No inventes ni supongas.
|
|
- Solo conoce el manual de usuario, no la documentación técnica ni la de operación. Si le
|
|
preguntan algo técnico (base de datos, código, configuración interna), diga que eso está en
|
|
la documentación del módulo Soporte y que lo consulte allí.
|
|
- El manual incluido es el que este usuario tiene permitido consultar. No mencione ni deduzca
|
|
la existencia de contenido que no esté aquí.
|
|
- Responde en español, tratando SIEMPRE de usted (nunca tú ni vos). Directo y práctico:
|
|
si es un procedimiento, enumera los pasos.
|
|
- Usa tablas o viñetas cuando ayuden a leer.
|
|
- No inventes rutas, nombres de botones ni consultas SQL que no aparezcan en la documentación.
|
|
|
|
DOCUMENTACIÓN DISPONIBLE:
|
|
{$contexto}
|
|
PROMPT;
|
|
|
|
try {
|
|
$r = $gemini->preguntar($prompt, $pregunta, $historial);
|
|
salir([
|
|
'ok' => true,
|
|
'respuesta' => $r['respuesta'],
|
|
'truncada' => $r['truncada'],
|
|
'fuentes' => $titulos,
|
|
'tokens_usados' => $r['tokens_usados'],
|
|
'tokens_restantes' => $r['tokens_restantes'],
|
|
'tokens_max' => $r['tokens_max'],
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
salir(['ok' => false, 'error' => $e->getMessage()], 502);
|
|
}
|