from langchain.tools import Tool
from langchain_core.vectorstores import VectorStoreRetriever
from typing import Optional
from utils.logger import log_info, log_warn, log_error, log_debug
import json
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from utils.classes import Order
from supabase_client import SupabaseOrderManager
import asyncio
import os
from dotenv import load_dotenv
# Cargar variables de entorno
load_dotenv()
# Intentar inicializar Supabase de forma segura
def init_supabase_client():
"""Inicializa el cliente de Supabase de forma segura."""
try:
# Verificar si las variables de entorno están disponibles
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
if not supabase_url or not supabase_key:
log_warn("Variables de entorno de Supabase no encontradas. Funcionando sin base de datos.")
return None
supabase = SupabaseOrderManager()
log_info("Cliente de Supabase inicializado correctamente")
return supabase
except Exception as e:
log_error(f"Error al inicializar el cliente de Supabase: {e}")
log_warn("Continuando sin funcionalidad de base de datos")
return None
# Inicializar cliente
supabase = init_supabase_client()
def create_menu_info_tool(retriever: VectorStoreRetriever) -> Tool:
"""
Crea una herramienta para extraer información relevante del menú del restaurante.
Args:
retriever: Un retriever configurado para buscar en la base de conocimiento del menú
Returns:
Una herramienta de LangChain para consultar información del menú
"""
def extract_text(query: str) -> str:
"""Extrae texto relevante del menú basado en la consulta."""
results = retriever.invoke(query)
log_info("ENTRADA DE EXTRACCIÓN DE TEXTO")
result_texts = []
if results:
log_info("\n=== Fragmentos de documento utilizados para la respuesta ===")
for i, result in enumerate(results):
log_info(f"Fragmento {i+1}: {result.page_content[:100]}...")
# Comprobar si tiene score (algunos retrievers no incluyen este atributo)
if hasattr(result, 'score'):
log_info(f"Score: {result.score}")
result_texts.append(result.page_content)
log_info("=========================================================\n")
# Unir los resultados relevantes
return "\n\n".join(result_texts)
else:
return "Lo siento, no tengo información sobre eso."
return Tool(
name="guest_info_tool",
description="""Herramienta para consultar información detallada del menú del restaurante.
Úsala cuando necesites:
- Buscar platos específicos y verificar su disponibilidad
- Consultar precios exactos de productos
- Obtener información sobre ingredientes, alérgenos o composición de platos
- Explorar secciones del menú (entrantes, principales, postres, bebidas, etc.)
- Verificar la existencia de productos antes de recomendarlos
- Responder preguntas específicas sobre la carta del restaurante
Esta herramienta accede al contenido completo del menú para proporcionar información precisa y actualizada.""",
func=extract_text,
)
def create_send_to_kitchen_tool(llm: ChatOpenAI) -> Tool:
"""
Crea una herramienta para enviar pedidos a la cocina.
Args:
llm: Un modelo de lenguaje para analizar la conversación
Returns:
Una herramienta de LangChain para enviar pedidos a la cocina
"""
def extract_order_from_summary(conversation_summary: str) -> Order:
"""
Usa un LLM para extraer detalles del pedido a partir de un resumen de la conversación.
Args:
conversation_summary: Resumen de la conversación entre cliente y camarero
Returns:
Objeto Order con los detalles del pedido extraído
"""
# Crear mensaje para el LLM
messages = [
SystemMessage(content="""
Eres un asistente especializado en extraer información de pedidos de restaurante.
Analiza el siguiente resumen de conversación entre un cliente y un camarero.
Extrae SOLO los elementos del pedido (platos, bebidas, etc.), cantidades, y cualquier instrucción especial.
Devuelve los resultados en formato JSON entre las etiquetas estrictamente con esta estructura:
{
"table_number": número_de_mesa (entero o "desconocida" si no se especifica),
"items": [
{
"name": "nombre_del_plato",
"quantity": cantidad (entero, por defecto 1),
"variations": "variaciones o personalizaciones"
},
...
],
"special_instructions": "instrucciones especiales generales"
}
No incluyas ninguna otra información o explicación, SOLO el JSON entre las etiquetas.
"""),
HumanMessage(content=f"Resumen de la conversación: {conversation_summary}")
]
# Invocar el LLM para obtener el análisis del pedido
response = llm.invoke(messages)
response_text = response.content
# Extraer el JSON de la respuesta usando las etiquetas
try:
# Buscar contenido entre etiquetas y
import re
order_pattern = re.compile(r'(.*?)', re.DOTALL)
order_match = order_pattern.search(response_text)
if order_match:
# Extraer el contenido JSON de las etiquetas
json_str = order_match.group(1).strip()
order_data = json.loads(json_str)
# Crear objeto Order con los datos extraídos
return Order(
items=order_data.get("items", []),
special_instructions=order_data.get("special_instructions", ""),
table_number=order_data.get("table_number", "desconocida")
)
else:
# Si no hay etiquetas, reportar error
log_error("No se encontraron etiquetas en la respuesta del LLM")
# Devolver un objeto Order vacío con un flag de error
empty_order = Order(table_number="desconocida")
empty_order.error = "NO_TAGS_FOUND"
return empty_order
except json.JSONDecodeError as e:
log_error(f"Error al parsear JSON de la respuesta del LLM: {e}")
log_debug(f"Respuesta problemática: {response_text}")
empty_order = Order(table_number="desconocida")
empty_order.error = "JSON_PARSE_ERROR"
return empty_order
except Exception as e:
log_error(f"Error inesperado al procesar la respuesta: {e}")
log_debug(f"Respuesta completa: {response_text}")
empty_order = Order(table_number="desconocida")
empty_order.error = "UNKNOWN_ERROR"
return empty_order
def send_to_kitchen(conversation_summary: str) -> str:
"""
Procesa el resumen de la conversación para extraer el pedido y enviarlo a la cocina.
Args:
conversation_summary: Resumen de la conversación cliente-camarero
Returns:
Mensaje de confirmación
"""
try:
log_info(f"Procesando resumen para enviar pedido a cocina...")
log_debug(f"Resumen recibido: {conversation_summary}")
# Verificar si Supabase está disponible
if supabase is None:
log_warn("Supabase no está configurado. Simulando envío de pedido.")
# Extraer el pedido para mostrarlo en logs aunque no se envíe
order = extract_order_from_summary(conversation_summary)
# Verificar si hay un error en el procesamiento
if hasattr(order, 'error') and order.error:
return "Lo siento, ha ocurrido un problema al procesar su pedido. Por favor, inténtelo de nuevo."
# Verificar si hay elementos en el pedido
if not order.items:
return "No se pudo identificar ningún artículo en el pedido. ¿Podría repetir su pedido, por favor?"
# Simular el procesamiento del pedido
order_dict = order.to_dict()
log_info(f"PEDIDO PROCESADO (MODO SIMULACIÓN): {json.dumps(order_dict, indent=2, ensure_ascii=False)}")
# Devolver confirmación simulada
return f"Su pedido ha sido procesado correctamente. Mesa: {order.table_number}, Artículos: {len(order.items)} elemento(s). (Modo simulación - Supabase no configurado)"
# Extraer el pedido a partir del resumen
order = extract_order_from_summary(conversation_summary)
# Verificar si hay un error en el procesamiento
if hasattr(order, 'error') and order.error:
if order.error == "NO_TAGS_FOUND":
log_error("No se encontraron las etiquetas en la respuesta del LLM")
return "Lo siento, ha ocurrido un problema al procesar su pedido. Por favor, inténtelo de nuevo."
elif order.error == "JSON_PARSE_ERROR":
log_error("Error al analizar el JSON en las etiquetas ")
return "Ha ocurrido un error técnico al procesar su pedido. ¿Podría repetirlo de otra forma?"
else:
log_error(f"Error desconocido: {order.error}")
return "Lo siento, algo salió mal al procesar su pedido. Por favor, inténtelo de nuevo."
# Verificar si hay elementos en el pedido
if not order.items:
log_warn("No se identificaron artículos en el pedido")
return "No se pudo identificar ningún artículo en el pedido. ¿Podría repetir su pedido, por favor?"
# Enviar a la cocina usando Supabase
order_dict = order.to_dict()
log_info(f"ENVIANDO PEDIDO A COCINA: {json.dumps(order_dict, indent=2, ensure_ascii=False)}")
# Envío real con Supabase
async def async_send_and_get_result(order):
return await supabase.send_order(order)
res = asyncio.run(async_send_and_get_result(order))
if res.get("success"):
log_info(f"Pedido enviado correctamente a la cocina: {res['order_id']}")
return f"Su pedido ha sido enviado a la cocina. ID de pedido: {res['order_id']}"
else:
log_error(f"Error al enviar el pedido a la cocina: {res.get('error', 'Desconocido')}")
return "Lo siento, hubo un problema al enviar su pedido a la cocina. ¿Podría intentarlo de nuevo?"
except Exception as e:
log_error(f"Error al procesar pedido: {e}")
log_debug(f"Error detallado: {str(e)}")
import traceback
log_debug(traceback.format_exc())
return "Lo siento, hubo un problema al procesar su pedido. ¿Podría intentarlo de nuevo?"
# Determinar la descripción basada en si Supabase está disponible
if supabase is None:
description = """
Procesa y confirma el pedido del cliente (MODO SIMULACIÓN - Sin base de datos).
Usa esta herramienta SOLAMENTE cuando el cliente haya terminado de hacer su pedido
completo y esté listo para confirmarlo.
NOTA: Supabase no está configurado, por lo que el pedido será procesado pero no se
enviará a una base de datos real.
Esta herramienta espera recibir un RESUMEN de la conversación que describe los elementos del pedido.
"""
else:
description = """
Envía el pedido completo a la cocina. Usa esta herramienta SOLAMENTE cuando el cliente haya terminado de hacer su pedido
completo y esté listo para enviarlo.
Esta herramienta espera recibir un RESUMEN de la conversación que describe los elementos del pedido.
No envíes la conversación completa, solo un resumen claro de lo que el cliente ha pedido, la mesa,
y cualquier instrucción especial relevante.
"""
# Retornar la herramienta configurada con la función send_to_kitchen
return Tool(
name="send_to_kitchen_tool",
description=description,
func=send_to_kitchen,
)