Spaces:
Sleeping
Sleeping
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 | |
try: | |
supabase = SupabaseOrderManager() | |
except Exception as e: | |
log_error(f"Error al inicializar el cliente de Supabase: {e}") | |
supabase = None | |
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 <order> </order> 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 <order></order> | |
try: | |
# Buscar contenido entre etiquetas <order> y </order> | |
import re | |
order_pattern = re.compile(r'<order>(.*?)</order>', 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 <order> 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}") | |
# 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 <order> 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 <order>") | |
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?" | |
# Simular envío a la cocina | |
order_dict = order.to_dict() | |
log_info(f"ENVIANDO PEDIDO A COCINA: {json.dumps(order_dict, indent=2, ensure_ascii=False)}") | |
# Aquí iría la integración real con el sistema de la cocina | |
# Por ejemplo, enviar a una API, base de datos, etc. | |
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?" | |
# Retornar la herramienta configurada con la función send_to_kitchen | |
return Tool( | |
name="send_to_kitchen_tool", | |
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. | |
""", | |
func=send_to_kitchen, | |
) | |