File size: 10,518 Bytes
f810b2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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,
    )