$l->ID ?? null, 'name' => $l->name ?? null, 'fullname' => $l->fullname ?? null, 'address' => $l->address ?? null, 'address1' => $l->address1 ?? null, 'address2' => $l->address2 ?? null, 'mapslink' => $l->mapslink ?? null, ]; } if (!empty($locations)) { return $locations; } } catch (Exception $e) { // fallback below } } try { $db = ensure_efisio_db(); $stmt = $db->query("SELECT * FROM efisio_locations ORDER BY `order`, ID"); $rows = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; foreach ($rows as $r) { $locations[] = [ 'id' => isset($r['ID']) ? intval($r['ID']) : null, 'name' => $r['name'] ?? null, 'fullname' => $r['fullname'] ?? null, 'address' => $r['address'] ?? null, 'address1' => $r['address1'] ?? null, 'address2' => $r['address2'] ?? null, 'mapslink' => $r['mapslink'] ?? null, ]; } } catch (Exception $e) { return []; } return $locations; } function get_location_details($clinica_id = null, $clinica_name = null) { if ($clinica_id !== null) { $clinica_id = intval($clinica_id); } if (class_exists('App_Location')) { $loc = null; if ($clinica_id !== null && $clinica_id >= 0) { $loc = App_Location::get($clinica_id); } else if (!empty($clinica_name)) { $loc = App_Location::get($clinica_name); } if ($loc) { return [ 'id' => $loc->ID ?? null, 'name' => $loc->name ?? null, 'fullname' => $loc->fullname ?? null, 'address' => $loc->address ?? null, 'address1' => $loc->address1 ?? null, 'address2' => $loc->address2 ?? null, 'mapslink' => $loc->mapslink ?? null, 'address_note' => $loc->address_note ?? null, ]; } } try { $db = ensure_efisio_db(); if ($clinica_id !== null && $clinica_id >= 0) { $stmt = $db->prepare("SELECT * FROM efisio_locations WHERE ID = ? LIMIT 1"); $stmt->execute([$clinica_id]); } else if (!empty($clinica_name)) { $stmt = $db->prepare("SELECT * FROM efisio_locations WHERE name LIKE ? OR fullname LIKE ? LIMIT 1"); $like = '%' . $clinica_name . '%'; $stmt->execute([$like, $like]); } else { return null; } $r = $stmt->fetch(PDO::FETCH_ASSOC); if ($r) { return [ 'id' => isset($r['ID']) ? intval($r['ID']) : null, 'name' => $r['name'] ?? null, 'fullname' => $r['fullname'] ?? null, 'address' => $r['address'] ?? null, 'address1' => $r['address1'] ?? null, 'address2' => $r['address2'] ?? null, 'mapslink' => $r['mapslink'] ?? null, 'address_note' => $r['address_note'] ?? null, ]; } } catch (Exception $e) { return null; } return null; } function recommend_workers_for_symptoms($symptoms, $especialidad = null, $clinica_id = null, $limit = 5) { $symptoms = trim((string)$symptoms); $especialidad = trim((string)($especialidad ?? '')); if ($symptoms === '' && $especialidad === '') return []; try { require_once __DIR__ . '/../workers/expertise.php'; $db = ensure_efisio_db(); $query = $symptoms; if ($especialidad !== '') { $query = trim($query . ' ' . $especialidad); } $results = recommend_workers($db, $query, $limit); if ($clinica_id !== null) { $clinica_id = intval($clinica_id); $results = array_values(array_filter($results, function($r) use ($clinica_id) { return isset($r['location_id']) && intval($r['location_id']) === $clinica_id; })); } return $results; } catch (Exception $e) { return []; } } function get_favorite_worker_info($user_id) { $user_id = intval($user_id); if ($user_id <= 0) return null; try { require_once __DIR__ . '/../appointments/favorites.php'; require_once __DIR__ . '/../appointments/worker.php'; require_once __DIR__ . '/../../libwp/appointments/includes/model/class_app.php'; $fav = get_favorite_worker($user_id); if ($fav > 0 && class_exists('App_Worker')) { $worker = App_Worker::get($fav); if ($worker) { return [ 'worker_id' => $worker->ID ?? $fav, 'worker_name' => $worker->name ?? '', 'worker_fullname' => method_exists($worker, 'getFullName') ? $worker->getFullName() : '', 'location_id' => $worker->location ?? null, 'location_name' => $worker->location_name ?? '', 'role' => $worker->role ?? '', ]; } } } catch (Exception $e) { return null; } return null; } function get_user_future_appointments($user_id, $limit = 5) { $user_id = intval($user_id); if ($user_id <= 0) return []; try { $db = ensure_efisio_db(); $stmt = $db->prepare(" SELECT ID, start, end, status, service, worker, location FROM wp_app_appointments WHERE user = ? AND start >= NOW() AND status != 'removed' ORDER BY start ASC LIMIT $limit "); $stmt->execute([$user_id]); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } catch (Exception $e) { return []; } } function get_user_past_appointments($user_id, $limit = 5) { $user_id = intval($user_id); if ($user_id <= 0) return []; try { $db = ensure_efisio_db(); $stmt = $db->prepare(" SELECT ID, start, end, status, service, worker, location FROM wp_app_appointments WHERE user = ? AND start < NOW() AND status != 'removed' ORDER BY start DESC LIMIT $limit "); $stmt->execute([$user_id]); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } catch (Exception $e) { return []; } } function get_chat_system_prompt($user_context = []) { $fecha = date("Y-m-d"); if (class_exists('IntlDateFormatter')) { setlocale(LC_TIME, "es_ES.UTF-8", "es_ES", "spanish"); $formatter = new IntlDateFormatter("es_ES", IntlDateFormatter::FULL, IntlDateFormatter::NONE, null, null, "EEEE"); $dia_semana = $formatter->format(strtotime($fecha)); } else { $dias_semana = ['domingo','lunes','martes','miércoles','jueves','viernes','sábado']; $dia_semana = $dias_semana[intval(date('w'))]; } // Ubicaciones desde App_Location / DB $locations = get_locations_for_prompt(); // Servicios principales $services = [ ['id' => 545, 'name' => 'Fisioterapia', 'price' => 50, 'duration' => 50], ['id' => 2509, 'name' => 'Drenaje Linfático', 'price' => 55, 'duration' => 60], ['id' => 29811, 'name' => 'Suelo Pélvico', 'price' => 60, 'duration' => 60], ['id' => 2082, 'name' => 'Osteopatía', 'price' => 60, 'duration' => 60], ]; $user_info = ""; if (!empty($user_context['name'])) { $user_info = "El usuario se llama {$user_context['name']}."; } if (!empty($user_context['phone'])) { $user_info .= " Su teléfono es {$user_context['phone']}."; } if (!empty($user_context['favfisio_name'])) { $user_info .= " Fisio habitual: {$user_context['favfisio_name']}."; } if (!empty($user_context['upcoming_appointments'])) { $user_info .= " Citas futuras: " . json_encode($user_context['upcoming_appointments']) . "."; } if (!empty($user_context['recent_appointments'])) { $user_info .= " Historial reciente: " . json_encode($user_context['recent_appointments']) . "."; } return "Eres el asistente virtual de eFISIO, una red de clínicas de fisioterapia en Madrid. PERSONALIDAD: - Cercano y profesional (tutea al usuario) - Conciso (respuestas cortas y claras) - Empático con quienes tienen dolor - Proactivo para ayudar a reservar cita FECHA ACTUAL: $fecha ($dia_semana) CLÍNICAS: " . json_encode($locations) . " SERVICIOS PRINCIPALES: " . json_encode($services) . " BONOS: - 5 sesiones: 225€ (45€/sesión) - 10 sesiones: 400€ (40€/sesión) CONTACTO: - Teléfono: 910 052 363 - WhatsApp: 644 054 800 $user_info REGLAS: - NO des diagnósticos médicos - Si el usuario quiere reservar, guíale preguntando: servicio, clínica preferida, y disponibilidad - Ofrece siempre opciones claras - Cuando muestres fechas, incluye el día de la semana - Responde en español - Si el usuario es existente (hay user_id) y pide cita o cancelación, primero consulta \"citas_futuras\" y, si no tiene, consulta \"fisio_habitual\" para recomendar con su fisio de confianza. - Si el usuario pide CAMBIAR la cita y aporta una fecha/hora (o franja), debes consultar disponibilidad real con la herramienta \"ver_disponibilidad\" usando: - \"clinica_id\" y \"servicio_id\" de la próxima cita (si existen) - \"worker_id\" del fisio de esa cita (si existe) o su fisio habitual - \"desde\" y \"hasta\" alrededor de la fecha sugerida Responde directamente con opciones de horas libres (máx 3) o indica que no hay y ofrece alternativa. FORMATO DE RESPUESTA: Responde SIEMPRE en este formato JSON: { \"respuesta\": \"tu mensaje aquí\", \"acciones\": [\"opción 1\", \"opción 2\"] } Las acciones son botones que el usuario puede pulsar para continuar la conversación."; } function ejecutar_agente_v3($mensaje, $history = [], $user_context = []) { $system_prompt = get_chat_system_prompt($user_context); $messages = [ ["role" => "system", "content" => $system_prompt] ]; // Añadir historial (máximo 6 mensajes) foreach (array_slice($history, -6) as $h) { $messages[] = [ "role" => $h["role"], "content" => $h["content"] ]; } // Añadir mensaje actual $messages[] = ["role" => "user", "content" => $mensaje]; // Usar Claude Sonnet via OpenRouter (rápido y económico) $response = ask_ia($messages, "google/gemini-2.0-flash-001"); if (!$response) { return [ "respuesta" => "Disculpa, estoy teniendo problemas técnicos. ¿Puedes intentarlo de nuevo o llamarnos al 910 052 363?", "acciones" => ["Llamar al 910 052 363", "Intentar de nuevo"] ]; } // Intentar parsear JSON $parsed = extract_json_from_agent_response($response); if ($parsed && isset($parsed['respuesta'])) { return $parsed; } // Si no es JSON válido, devolver como texto return [ "respuesta" => $response, "acciones" => [] ]; } /** * Versión con herramientas para consultas de disponibilidad */ function ejecutar_agente_v3_tools($mensaje, $history = [], $user_context = []) { $system_prompt = get_chat_system_prompt($user_context); // Añadir instrucciones de herramientas $system_prompt .= " HERRAMIENTAS DISPONIBLES: Si necesitas consultar disponibilidad real, responde con: { \"herramienta\": \"ver_disponibilidad\", \"parametros\": { \"clinica_id\": 2, \"desde\": \"2024-01-20\", \"hasta\": \"2024-01-27\", \"servicio_id\": 545, \"worker_id\": 123 } } Si necesitas citas futuras del usuario, responde con: { \"herramienta\": \"citas_futuras\", \"parametros\": { \"user_id\": 12345 } } Si necesitas historial de citas del usuario, responde con: { \"herramienta\": \"citas_historial\", \"parametros\": { \"user_id\": 12345 } } Si necesitas detalles de una clínica, responde con: { \"herramienta\": \"detalle_clinica\", \"parametros\": { \"clinica_id\": 2, \"clinica_nombre\": \"Chamberí\" } } Si necesitas recomendar un fisio por patología/síntomas y clínica, responde con: { \"herramienta\": \"recomendar_fisio\", \"parametros\": { \"sintomas\": \"dolor lumbar, ciática\", \"especialidad\": \"suelo pélvico\", \"clinica_id\": 2 } } Si necesitas el fisio habitual del usuario, responde con: { \"herramienta\": \"fisio_habitual\", \"parametros\": { \"user_id\": 12345 } } PAUSA Te responderé con los resultados de la herramienta."; $messages = [ ["role" => "system", "content" => $system_prompt] ]; foreach (array_slice($history, -6) as $h) { $messages[] = ["role" => $h["role"], "content" => $h["content"]]; } $messages[] = ["role" => "user", "content" => $mensaje]; $max_turnos = 3; $turno = 0; $tool_trace = []; while ($turno < $max_turnos) { $response = ask_ia($messages, "google/gemini-2.0-flash-001"); if (!$response) { return [ "respuesta" => "Disculpa, estoy teniendo problemas. Llámanos al 910 052 363.", "acciones" => ["Llamar"] ]; } $parsed = extract_json_from_agent_response($response); // Si tiene respuesta final, devolverla if ($parsed && isset($parsed['respuesta'])) { if (!empty($user_context['debug_tools'])) { $parsed['debug_tools'] = $tool_trace; } return $parsed; } // Si pide herramienta if ($parsed && isset($parsed['herramienta'])) { $herramienta = $parsed['herramienta']; $params = $parsed['parametros'] ?? []; $result = null; if ($herramienta == 'ver_disponibilidad') { $location_id = $params['clinica_id'] ?? null; $service_id = $params['servicio_id'] ?? 545; $worker_id = $params['worker_id'] ?? 0; $desde = $params['desde'] ?? date('Y-m-d'); $hasta = $params['hasta'] ?? date('Y-m-d', strtotime('+7 days')); $result = null; if (class_exists('App_WeeklyAdm')) { $raw = App_WeeklyAdm::apps_free($location_id, strtotime($desde), strtotime($hasta), $service_id, $worker_id); $result = $raw; } else { $query = http_build_query([ 'from' => $desde, 'to' => $hasta, 'l' => $location_id, 's' => $service_id, 'w' => $worker_id ]); $url = "https://www.efisio.es/api/appsfree?" . $query; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); $resp = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($resp && $http >= 200 && $http < 300) { $json = json_decode($resp, true); if (is_array($json) && isset($json['apps'])) { $result = $json['apps']; } } } if (!$result) { $result = ['error' => 'tool_unavailable']; } else { $simplified = []; if (!is_array($result) && !is_object($result)) $result = []; foreach ($result as $key => $slots) { $date = is_numeric($key) ? date('Y-m-d', intval($key)) : $key; if (!isset($simplified[$date])) $simplified[$date] = []; foreach ($slots as $s) { $status = $s['status'] ?? ($s->status ?? ''); if ($status && $status !== 'available') continue; $wkr = $s['worker'] ?? ($s->worker ?? null); if ($worker_id && intval($wkr) !== intval($worker_id)) continue; $start = $s['start'] ?? ($s->start ?? null); if (!$start) continue; $simplified[$date][] = [ 'hora' => date('H:i', strtotime($start)), 'inicio' => $start, 'fisio' => $s['worker_name'] ?? ($s->worker_name ?? ''), 'fisio_id' => $wkr, 'clinica' => $s['location'] ?? ($s->location ?? $location_id) ]; if (count($simplified[$date]) >= 5) break; } } $result = $simplified; } } else if ($herramienta == 'citas_futuras') { $user_id = $params['user_id'] ?? ($user_context['user_id'] ?? null); $result = get_user_future_appointments($user_id, 5); } else if ($herramienta == 'citas_historial') { $user_id = $params['user_id'] ?? ($user_context['user_id'] ?? null); $result = get_user_past_appointments($user_id, 5); } else if ($herramienta == 'detalle_clinica') { $clinica_id = $params['clinica_id'] ?? null; $clinica_name = $params['clinica_nombre'] ?? null; $result = get_location_details($clinica_id, $clinica_name); } else if ($herramienta == 'recomendar_fisio') { $sintomas = $params['sintomas'] ?? ''; $especialidad = $params['especialidad'] ?? ''; $clinica_id = $params['clinica_id'] ?? null; $result = recommend_workers_for_symptoms($sintomas, $especialidad, $clinica_id, 5); } else if ($herramienta == 'fisio_habitual') { $user_id = $params['user_id'] ?? ($user_context['user_id'] ?? null); $result = get_favorite_worker_info($user_id); } if ($result) { if (!empty($user_context['debug_tools'])) { $tool_trace[] = [ 'tool' => $herramienta, 'params' => $params, 'result' => $result ]; } $messages[] = ["role" => "assistant", "content" => $response]; $messages[] = ["role" => "user", "content" => "Resultado: " . json_encode($result, JSON_PRETTY_PRINT)]; } } else { // Respuesta en texto plano $res = [ "respuesta" => $response, "acciones" => [] ]; if (!empty($user_context['debug_tools'])) { $res['debug_tools'] = $tool_trace; } return $res; } $turno++; } $res = [ "respuesta" => "No pude completar tu consulta. ¿Prefieres que te llamemos?", "acciones" => ["Sí, llamadme", "Intentar de nuevo"] ]; if (!empty($user_context['debug_tools'])) { $res['debug_tools'] = $tool_trace; } return $res; } /** * Helper para extraer JSON de texto (local a este archivo) */ function extract_json_from_agent_response($text) { if (!$text) return null; // Buscar JSON en code blocks if (preg_match('/```(?:json)?\s*(\{.*?\})\s*```/s', $text, $matches)) { $json = json_decode($matches[1], true); if ($json) return $json; } // Buscar JSON directo if (preg_match('/\{[^{}]*"respuesta"[^{}]*\}/s', $text, $matches)) { $json = json_decode($matches[0], true); if ($json) return $json; } // Intentar parsear todo el texto $json = json_decode($text, true); if ($json) return $json; return null; }