Spaces:
Sleeping
Sleeping
File size: 14,554 Bytes
f810b2f 1b3cdee 66febf2 f810b2f 1b3cdee f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 1b3cdee f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 1b3cdee 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 f810b2f 66febf2 1b3cdee 66febf2 1b3cdee 66febf2 1b3cdee 66febf2 1b3cdee f810b2f 1b3cdee |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
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
import re # Asegúrate de importar re
# 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:
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
supabase = init_supabase_client()
def create_menu_info_tool(retriever: VectorStoreRetriever) -> Tool:
"""
Crea una herramienta para extraer información relevante del menú del restaurante.
"""
def extract_text(query: str) -> str:
"""Extrae texto relevante del menú basado en la consulta."""
log_info(f"MENUTOOL: Recibida consulta: '{query}'")
results = retriever.invoke(query)
result_texts = []
if results:
log_info("\n=== MENUTOOL: Fragmentos de documento utilizados para la respuesta ===")
for i, result in enumerate(results):
log_info(f"Fragmento {i+1}: {result.page_content[:100]}...")
if hasattr(result, 'score'):
log_info(f"Score: {result.score}")
result_texts.append(result.page_content)
log_info("=========================================================\n")
return "\n\n".join(result_texts)
else:
log_info("MENUTOOL: No se encontraron resultados para la consulta.")
return "Lo siento, no tengo información sobre eso en el menú."
return Tool(
name="restaurant_menu_lookup_tool",
description="""
Herramienta para consultar información detallada sobre el menú del restaurante.
Esencial para responder preguntas de los clientes sobre platos, ingredientes, precios, alérgenos, opciones dietéticas (vegetarianas, sin gluten, etc.), y disponibilidad de artículos.
DEBES usar esta herramienta cuando:
- Un cliente te pide un plato (e.g., "Quiero una tortilla de patatas").
- Un cliente pregunte directamente sobre un plato específico (e.g., "¿Tienen lasaña?", "¿Qué lleva la ensalada César?").
- Necesites verificar la existencia o detalles de un producto del menú antes de hacer una recomendación o confirmar una elección.
- Un cliente pregunte por precios (e.g., "¿Cuánto cuesta la hamburguesa?").
- Un cliente tenga dudas sobre ingredientes o alérgenos (e.g., "¿La paella lleva marisco?", "¿Este postre tiene frutos secos?").
- Necesites buscar opciones que cumplan ciertos criterios dietéticos o de preferencia (e.g., "platos vegetarianos", "postres sin lactosa", "algo picante").
- El cliente quiera explorar secciones del menú (e.g., "¿Qué tienen de entrantes?", "¿Qué cervezas ofrecen?").
Cómo funciona:
Toma una pregunta o consulta en lenguaje natural sobre el menú como entrada (input) y devuelve la información relevante encontrada en la base de datos del menú como salida (output).
""",
func=extract_text,
)
def create_send_to_kitchen_tool(llm: ChatOpenAI) -> Tool:
"""
Crea una herramienta para procesar y enviar pedidos a la cocina.
"""
def extract_order_from_summary(conversation_summary: str) -> Order:
messages = [
SystemMessage(content="""
Eres un asistente experto en extraer información de pedidos de restaurante a partir de un resumen de conversación.
Analiza el siguiente resumen. Extrae ÚNICAMENTE los artículos del pedido (platos, bebidas), sus cantidades, y cualquier instrucción o variación especial.
También extrae el número de mesa si está presente.
Debes devolver los resultados en formato JSON estrictamente entre las etiquetas <order> y </order>.
El JSON debe seguir esta estructura exacta:
{
"table_number": número_de_mesa (entero o la cadena "desconocida" si no se especifica),
"items": [
{
"name": "nombre_del_plato_o_bebida",
"quantity": cantidad_del_articulo (entero, por defecto 1 si no se especifica),
"variations": "variaciones, personalizaciones o notas para este artículo específico" (cadena vacía si no hay)
}
],
"special_instructions": "instrucciones especiales generales para todo el pedido" (cadena vacía si no hay)
}
Si no puedes identificar ningún artículo o el resumen no parece un pedido, devuelve un JSON con "items" como una lista vacía.
No incluyas ninguna explicación, saludo o texto adicional fuera de las etiquetas <order> </order>. SOLO el JSON.
Ejemplo de un buen input de resumen: "Mesa 5, una pizza margarita, dos cocacolas, una sin hielo. La pizza bien hecha."
Ejemplo de un buen output JSON:
<order>
{
"table_number": 5,
"items": [
{"name": "pizza margarita", "quantity": 1, "variations": "bien hecha"},
{"name": "cocacola", "quantity": 2, "variations": "una sin hielo"}
],
"special_instructions": ""
}
</order>
"""),
HumanMessage(content=f"Resumen de la conversación para extraer el pedido: {conversation_summary}")
]
response = llm.invoke(messages)
response_text = response.content
log_debug(f"KITCHENTOOL_LLM_RESPONSE: {response_text}")
try:
order_pattern = re.compile(r'<order>(.*?)</order>', re.DOTALL)
order_match = order_pattern.search(response_text)
if order_match:
json_str = order_match.group(1).strip()
log_debug(f"KITCHENTOOL_JSON_EXTRACTED: {json_str}")
order_data = json.loads(json_str)
# Validaciones adicionales
if not isinstance(order_data.get("items"), list):
log_warn("KITCHENTOOL: 'items' no es una lista o falta en el JSON. Forzando a lista vacía.")
order_data["items"] = []
return Order(
items=order_data.get("items", []),
special_instructions=order_data.get("special_instructions", ""),
table_number=order_data.get("table_number", "desconocida")
)
else:
log_error("KITCHENTOOL: No se encontraron etiquetas <order> en la respuesta del LLM para extraer el pedido.")
empty_order = Order(table_number="desconocida")
empty_order.error = "NO_TAGS_FOUND"
return empty_order
except json.JSONDecodeError as e:
log_error(f"KITCHENTOOL: Error al parsear JSON de la respuesta del LLM: {e}")
empty_order = Order(table_number="desconocida")
empty_order.error = "JSON_PARSE_ERROR"
return empty_order
except Exception as e:
log_error(f"KITCHENTOOL: Error inesperado al procesar la respuesta del LLM para pedido: {e}")
empty_order = Order(table_number="desconocida")
empty_order.error = "UNKNOWN_ERROR_LLM_ORDER_EXTRACTION"
return empty_order
def send_to_kitchen(conversation_summary_for_order: str) -> str:
"""
Procesa el resumen de la conversación para extraer el pedido y enviarlo/simularlo.
"""
try:
log_info(f"KITCHENTOOL: Iniciando procesamiento de pedido con resumen.")
log_debug(f"KITCHENTOOL: Resumen recibido para pedido: {conversation_summary_for_order}")
order = extract_order_from_summary(conversation_summary_for_order)
if hasattr(order, 'error') and order.error:
log_error(f"KITCHENTOOL: Error en la extracción del pedido: {order.error}")
if order.error == "NO_TAGS_FOUND":
return "Lo siento, tuve un problema técnico al intentar entender el pedido. ¿Podrías repetirlo claramente, por favor?"
elif order.error == "JSON_PARSE_ERROR":
return "Lo siento, tuve un problema técnico al procesar los detalles del pedido. ¿Podrías decírmelo de otra manera?"
return "Lo siento, algo salió mal al procesar el pedido. Por favor, inténtalo de nuevo."
if not order.items:
log_warn("KITCHENTOOL: No se identificaron artículos en el pedido tras la extracción.")
return "No pude identificar ningún artículo en tu pedido. ¿Podrías decirme qué te gustaría pedir, por favor?"
order_dict_for_log = order.to_dict() # Para logging
if supabase is None:
log_warn("KITCHENTOOL: Supabase no configurado. Simulando envío de pedido.")
log_info(f"PEDIDO PROCESADO (MODO SIMULACIÓN): {json.dumps(order_dict_for_log, indent=2, ensure_ascii=False)}")
return (f"He procesado tu pedido (en modo simulación ya que la cocina no está conectada ahora mismo). "
f"Mesa: {order.table_number}. Artículos: {len(order.items)}. "
f"¿Hay algo más en lo que pueda ayudarte?")
log_info(f"KITCHENTOOL: Enviando pedido a cocina (Supabase): {json.dumps(order_dict_for_log, indent=2, ensure_ascii=False)}")
async def async_send_and_get_result(order_obj):
return await supabase.send_order(order_obj) # Pasa el objeto Order
res = asyncio.run(async_send_and_get_result(order)) # Pasar el objeto order
if res.get("success"):
log_info(f"KITCHENTOOL: Pedido enviado correctamente a la cocina. ID: {res['order_id']}")
return f"¡Perfecto! Tu pedido ha sido enviado a la cocina. El ID de tu pedido es {res['order_id']}. ¿Necesitas algo más?"
else:
log_error(f"KITCHENTOOL: Error al enviar el pedido a la cocina vía Supabase: {res.get('error', 'Desconocido')}")
return "Lo siento, hubo un problema al enviar tu pedido a la cocina. Por favor, intenta confirmarlo de nuevo en un momento."
except Exception as e:
log_error(f"KITCHENTOOL: Error general al procesar/enviar pedido: {e}")
import traceback
log_debug(traceback.format_exc())
return "Lo siento, ocurrió un error inesperado al procesar tu pedido. Por favor, inténtalo de nuevo."
tool_description_base = """
Procesa y envía el pedido confirmado y finalizado por el cliente a la cocina.
Utiliza esta herramienta EXCLUSIVAMENTE cuando el cliente haya confirmado verbalmente todos los artículos de su pedido y esté listo para que se tramite. Es el paso final para registrar la orden.
Qué hace la herramienta:
1. Analiza un resumen del pedido proporcionado para extraer: artículos, cantidades, número de mesa e instrucciones especiales.
2. Formatea esta información en una orden estructurada.
3. {action_description}
Cuándo DEBES usarla:
- El cliente dice explícitamente: "Eso es todo", "Listo para pedir", "Envíalo a la cocina", "Confirmo el pedido", o frases similares después de haber detallado todos los artículos de su pedido.
- Has repasado y confirmado con el cliente la lista completa de artículos y cantidades y el cliente da su aprobación final.
Qué información necesita como entrada (input):
- Un RESUMEN CONCISO de la conversación que detalle CLARAMENTE el pedido final. Este resumen DEBE incluir:
- Lista de artículos (platos, bebidas) con sus respectivas CANTIDADES.
- Número de MESA (si se especificó o se conoce, de lo contrario se marcará como "desconocida").
- Cualquier INSTRUCCIÓN ESPECIAL o variación para artículos específicos o para el pedido general (e.g., "sin cebolla en la hamburguesa", "la carne bien hecha", "todo para llevar").
- NO envíes la transcripción completa de la conversación, solo el resumen del pedido finalizado.
- NO envíes preguntas sobre el menú a esta herramienta.
Qué NO hacer:
- NO la uses si el cliente todavía está explorando el menú, haciendo preguntas sobre platos o añadiendo/modificando artículos. Para consultas sobre el menú, usa 'restaurant_menu_lookup_tool'.
- NO la uses si el pedido no está completo o el cliente no lo ha confirmado explícitamente.
- NO la uses para pedir información, solo para enviar un pedido finalizado.
"""
if supabase is None:
description = tool_description_base.format(action_description="SIMULA el envío de la orden, ya que la conexión con la cocina no está activa. El pedido se registrará internamente para fines de demostración.")
description += "\n\nNOTA IMPORTANTE: Actualmente en MODO SIMULACIÓN. El pedido será procesado pero NO se enviará a una cocina real."
else:
description = tool_description_base.format(action_description="Envía la orden al sistema real de la cocina.")
return Tool(
name="send_order_to_kitchen_tool",
description=description,
func=send_to_kitchen,
) |