ItzRoBeerT commited on
Commit
f810b2f
·
1 Parent(s): 2ec61dd

init project

Browse files
Files changed (11) hide show
  1. README.md +97 -11
  2. agent.py +115 -0
  3. app.py +414 -0
  4. data/carta.md +247 -0
  5. model.py +77 -0
  6. requirements.txt +29 -0
  7. supabase_client.py +99 -0
  8. tools.py +226 -0
  9. utils/classes.py +35 -0
  10. utils/functions.py +60 -0
  11. utils/logger.py +36 -0
README.md CHANGED
@@ -1,13 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: WAIter
3
- emoji: 🏢
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 5.31.0
8
- app_file: app.py
9
- pinned: false
10
- short_description: A conversational assistant with voice capabilities and tools
11
- ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ # 🧑‍🍳 WAIter: Voice Chatbot for Restaurants
2
+
3
+ WAIter es un chatbot interactivo con capacidades de voz diseñado para escenarios de restaurantes. Simula un camarero amigable y servicial que puede responder a las preguntas habladas de los clientes en tiempo real, utilizando tecnologías de reconocimiento de voz, modelos de lenguaje avanzados y síntesis de voz natural.
4
+
5
+ ## 🛠️ Stack Tecnológico
6
+
7
+ - **Procesamiento de Voz**:
8
+ - 🎙️ [FastRTC](https://fastrtc.org) para streaming de audio en tiempo real
9
+ - 🔊 [ElevenLabs](https://elevenlabs.io/) para síntesis de voz expresiva (TTS)
10
+ - 🗣️ [Whisper](https://openai.com/research/whisper) (vía Groq) para reconocimiento de voz
11
+
12
+ - **Inteligencia Artificial de Lenguaje**:
13
+ - 💬 [LangChain](https://www.langchain.com/) para orquestación de LLMs
14
+ - 🔄 [LangGraph](https://github.com/langchain-ai/langgraph) para flujos de trabajo de agentes
15
+ - 🧠 [Gemini 2.5](https://deepmind.google/technologies/gemini/) (vía OpenRouter) para procesamiento de lenguaje natural
16
+
17
+ - **Gestión de Datos**:
18
+ - 🔍 Embeddings vectoriales con HuggingFace (BAAI/bge-m3)
19
+ - 💾 Datos de menú almacenados en formato Markdown
20
+ - 🗄️ [Supabase](https://supabase.com/) para almacenamiento y procesamiento de pedidos
21
+
22
+ - **Interfaz**:
23
+ - 🧪 [Gradio](https://www.gradio.app/) para la interfaz web interactiva
24
+
25
+ ## 🚀 Características
26
+
27
+ - **Interacción por voz en tiempo real** con un asistente virtual de camarero
28
+ - **Consulta de información del menú** con respuestas contextuales sobre platos, precios y opciones
29
+ - **Procesamiento de pedidos** con capacidad para enviar órdenes directamente a sistemas de cocina
30
+ - **Flujo conversacional natural** usando LangGraph para orquestación compleja de agentes
31
+ - **Integración de base de conocimientos** con RAG (Retrieval Augmented Generation)
32
+ - **Memoria de conversación multi-turno** para respuestas contextuales
33
+ - **Gestión de pedidos** con almacenamiento en Supabase y sistema de seguimiento
34
+ - **Síntesis de voz expresiva** con ElevenLabs para respuestas naturales y fluidas
35
+
36
+ ## 🚀 Primeros Pasos
37
+
38
+ 1. Clona este repositorio
39
+ 2. Instala las dependencias: `pip install -r requirements.txt` (requiere Python 3.10+)
40
+ 3. Crea un archivo `.env` con tus claves API:
41
+ ```
42
+ OPENROUTER_API_KEY=tu_clave_openrouter
43
+ OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
44
+ GROQ_API_KEY=tu_clave_groq
45
+ ELEVENLABS_API_KEY=tu_clave_elevenlabs
46
+ HELICONE_API_KEY=tu_clave_helicone_opcional
47
+ SUPABASE_URL=tu_url_supabase
48
+ SUPABASE_KEY=tu_clave_supabase
49
+ ```
50
+ 4. Ejecuta la aplicación: `python app.py`
51
+ 5. Abre la interfaz de Gradio en tu navegador
52
+
53
+ ## 🗂️ Estructura del Proyecto
54
+
55
+ - `app.py` - Punto de entrada principal con interfaz Gradio
56
+ - `agent.py` - Implementación del agente LangGraph para asistente de restaurante
57
+ - `model.py` - Gestor para la creación y configuración de modelos de lenguaje
58
+ - `tools.py` - Herramientas personalizadas para consulta de menú y envío de pedidos
59
+ - `supabase_client.py` - Cliente para operaciones con la base de datos Supabase
60
+ - `data/carta.md` - Datos del menú del restaurante en formato Markdown
61
+ - `utils/` - Módulos de utilidad:
62
+ - `functions.py` - Funciones auxiliares para manejo de mensajes y modelos
63
+ - `logger.py` - Sistema de registro con colores para depuración
64
+ - `classes.py` - Clases de datos como `Order` para representar pedidos
65
+
66
+ ## 🧠 Cómo Funciona
67
+
68
+ 1. **Reconocimiento de Voz**: FastRTC captura audio del micrófono del usuario y lo envía a la API Whisper de Groq para transcripción
69
+ 2. **Procesamiento de Lenguaje Natural**: El texto transcrito se pasa al agente LangGraph
70
+ 3. **Procesamiento RAG**: El agente utiliza búsqueda vectorial para encontrar información relevante del menú
71
+ 4. **Herramientas del Agente**: El sistema utiliza herramientas especializadas para:
72
+ - Búsqueda de información en el menú (vectores)
73
+ - Envío de pedidos a cocina (integración Supabase)
74
+ 5. **Generación de Respuesta**: Gemini 2.5 (a través de OpenRouter) genera una respuesta contextual y útil
75
+ 6. **Síntesis de Voz**: ElevenLabs convierte la respuesta de texto a voz natural
76
+ 7. **Interfaz Web**: Gradio renderiza la conversación y reproduce la respuesta de audio
77
+
78
+ ## 📦 Requisitos del Sistema
79
+
80
+ Los requisitos detallados se encuentran en el archivo `requirements.txt`. Las principales dependencias incluyen:
81
+
82
+ - Python 3.10+
83
+ - gradio >= 4.26.0
84
+ - langchain >= 0.1.0 (con varios componentes adicionales)
85
+ - fastrtc >= 0.6.0
86
+ - elevenlabs >= 0.2.24
87
+ - groq >= 0.4.0
88
+ - supabase >= 2.0.0
89
+ - sentence-transformers >= 2.2.2
90
+
91
+ ## 📝 Licencia
92
+
93
+ [MIT License](LICENSE)
94
+
95
  ---
 
 
 
 
 
 
 
 
 
 
96
 
97
+ 🌐 Desarrollado por Roberto - 2024
98
+
99
+
agent.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Annotated, List, Dict, Any
2
+ from langgraph.graph.message import add_messages
3
+ from langchain_core.messages import AnyMessage, SystemMessage
4
+ from langgraph.prebuilt import ToolNode
5
+ from langgraph.graph import START, StateGraph
6
+ from langgraph.prebuilt import tools_condition
7
+ from langchain_openai import ChatOpenAI
8
+ from langchain.tools import Tool
9
+
10
+ from utils.logger import log_info, log_warn, log_error, log_debug
11
+
12
+ class RestaurantAgent:
13
+ def __init__(self, llm: ChatOpenAI, restaurant_name: str, tools: List[Tool]):
14
+ """
15
+ Inicializa el agente del restaurante con LangGraph.
16
+
17
+ Args:
18
+ llm: Modelo de lenguaje a utilizar
19
+ restaurant_name: Nombre del restaurante
20
+ tools: Lista de herramientas para el agente
21
+ """
22
+ self.restaurant_name = restaurant_name
23
+ self.tools = tools
24
+
25
+ # Prompt para el asistente
26
+ self.system_prompt = f"""
27
+ Eres un camarero virtual profesional de {restaurant_name}, atendiendo la mesa 1. Combinas la calidez y simpatía gaditana con un servicio excelente y eficiente.
28
+
29
+ ## Tu personalidad
30
+ - Profesional pero cercano, con el encanto natural de Cádiz
31
+ - Confiable, simpático y resolutivo
32
+ - Transmites seguridad en cada recomendación
33
+ - Hablas con naturalidad, como si fueras un camarero experimentado
34
+
35
+ ## Comunicación (optimizada para TTS)
36
+ - Frases naturales, claras y conversacionales
37
+ - Tono directo pero amable, sin rodeos innecesarios
38
+ - Evita símbolos especiales, comillas o emojis
39
+ - Respuestas concisas que fluyan bien al ser leídas en voz alta
40
+
41
+ ## Protocolo de servicio OBLIGATORIO
42
+ 1. **SIEMPRE verifica** la disponibilidad de platos en el menú antes de confirmar pedidos
43
+ 2. **NUNCA recomiendes** productos sin consultarlos primero en la carta
44
+ 3. **Informa inmediatamente** si algo no está disponible y ofrece alternativas
45
+ 4. **Confirma cada pedido** antes de enviarlo a cocina
46
+ 5. **Despídete cordialmente** tras completar el servicio
47
+
48
+ ## Manejo de consultas del menú
49
+ - Cuando pregunten por opciones disponibles: proporciona un resumen claro y natural
50
+ - Para platos específicos: verifica existencia, precio e ingredientes principales
51
+ - Si desconoces algo: sé transparente y consulta la información necesaria
52
+ - Presenta las opciones de forma atractiva pero honesta
53
+
54
+ ## Gestión de pedidos
55
+ - Confirma cada plato solicitado existe en el menú
56
+ - Repite el pedido completo antes de enviarlo
57
+ - Informa el tiempo estimado si es relevante
58
+ - Mantén un registro mental del estado del pedido
59
+
60
+ Recuerda: tu objetivo es brindar una experiencia gastronómica excepcional combinando profesionalidad, eficiencia y ese toque especial gaditano que hace sentir como en casa.
61
+ """
62
+
63
+ # Configurar el LLM con las herramientas
64
+ self.llm_with_tools = llm.bind_tools(tools=tools)
65
+
66
+ # Construir el grafo
67
+ self.graph = self._build_graph()
68
+
69
+ def _build_graph(self) -> StateGraph:
70
+ """Construye el grafo de estados del agente."""
71
+
72
+ # Definición del tipo de estado para nuestro grafo
73
+ class AgentState(TypedDict):
74
+ """Tipo para el estado del agente de LangGraph."""
75
+ messages: Annotated[list[AnyMessage], add_messages]
76
+
77
+ # Nodo para el asistente que invoca al LLM
78
+ def assistant(state: AgentState):
79
+ """Procesa los mensajes usando el LLM y devuelve una respuesta."""
80
+ log_info("Assistant processing messages")
81
+
82
+ # Añadir el mensaje del sistema al principio de la lista de mensajes
83
+ messages = [SystemMessage(content=self.system_prompt)] + state["messages"]
84
+
85
+ return {
86
+ "messages": [self.llm_with_tools.invoke(messages)],
87
+ }
88
+
89
+ # Crear el grafo con una estructura mucho más simple
90
+ builder = StateGraph(AgentState)
91
+
92
+ # Definir nodos: el asistente y el nodo para herramientas
93
+ builder.add_node("assistant", assistant)
94
+ builder.add_node("tools", ToolNode(self.tools))
95
+
96
+ # Definir bordes con enrutamiento condicional automático
97
+ builder.add_edge(START, "assistant")
98
+ builder.add_conditional_edges(
99
+ "assistant",
100
+ # Si el último mensaje requiere una herramienta, enrutar a "tools"
101
+ # De lo contrario, terminar el flujo y devolver la respuesta
102
+ tools_condition,
103
+ )
104
+ builder.add_edge("tools", "assistant")
105
+
106
+ # Compilar y retornar el grafo
107
+ return builder.compile()
108
+
109
+ def invoke(self, messages: List[AnyMessage]) -> Dict[str, Any]:
110
+ """
111
+ Procesa la consulta del usuario y genera una respuesta usando el grafo LangGraph.
112
+ """
113
+ log_info(f"Processing query: {messages}")
114
+
115
+ return self.graph.invoke({"messages": messages})
app.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from os import getenv, environ
3
+ from dotenv import load_dotenv
4
+ import os
5
+ from model import ModelManager
6
+ from utils.functions import fetch_openrouter_models
7
+ # Configurar la variable de entorno para evitar advertencias de tokenizers (huggingface opcional)
8
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
9
+
10
+ from groq import AsyncClient
11
+ from fastrtc import WebRTC, ReplyOnPause, audio_to_bytes, AdditionalOutputs
12
+ import numpy as np
13
+ import asyncio
14
+ from elevenlabs.client import ElevenLabs
15
+ from elevenlabs import VoiceSettings
16
+
17
+ from langchain_text_splitters.markdown import MarkdownHeaderTextSplitter
18
+ from langchain_huggingface import HuggingFaceEmbeddings
19
+ from langchain_core.vectorstores import InMemoryVectorStore
20
+ from langchain_core.messages import HumanMessage, AIMessage
21
+
22
+ from agent import RestaurantAgent
23
+ # Importar las herramientas
24
+ from tools import create_menu_info_tool, create_send_to_kitchen_tool
25
+
26
+ from utils.logger import log_info, log_warn, log_error, log_success, log_debug
27
+
28
+ load_dotenv()
29
+
30
+ # Constantes
31
+ RESTAURANT = "Bar paco"
32
+
33
+ # Initialize clients and models to None, will be set during runtime
34
+ groq_client = None
35
+ eleven_client = None
36
+ llm = None
37
+ waiter_agent = None
38
+
39
+ # region RAG
40
+ md_path = "data/carta.md"
41
+
42
+ with open(md_path, "r", encoding="utf-8") as file:
43
+ md_content = file.read()
44
+
45
+ splitter = MarkdownHeaderTextSplitter(
46
+ headers_to_split_on=[
47
+ ("#", "seccion_principal"),
48
+ ("##", "subseccion"),
49
+ ("###", "apartado")
50
+ ],
51
+ strip_headers=False)
52
+ splits = splitter.split_text(md_content)
53
+ embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-m3", model_kwargs = {'device': 'cpu'})
54
+ vector_store = InMemoryVectorStore.from_documents(splits, embeddings)
55
+
56
+ retriever = vector_store.as_retriever(search_kwargs={"k": 4})
57
+ # endregion
58
+
59
+ # Initialize tools to None
60
+ guest_info_tool = None
61
+ send_to_kitchen_tool = None
62
+ tools = None
63
+
64
+ # Function to initialize all components with provided API keys
65
+ def initialize_components(openrouter_key, groq_key, elevenlabs_key, model_name):
66
+ global groq_client, eleven_client, llm, waiter_agent, guest_info_tool, send_to_kitchen_tool, tools
67
+
68
+ log_info("Initializing components with provided API keys...")
69
+
70
+ # Initialize clients with provided keys
71
+ if groq_key:
72
+ groq_client = AsyncClient(api_key=groq_key)
73
+
74
+ if elevenlabs_key:
75
+ eleven_client = ElevenLabs(api_key=elevenlabs_key)
76
+
77
+ if openrouter_key:
78
+ # Initialize LLM
79
+ model_manager = ModelManager(
80
+ api_key=openrouter_key,
81
+ api_base=getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
82
+ model_name=model_name,
83
+ helicone_api_key=getenv("HELICONE_API_KEY", "")
84
+ )
85
+ llm = model_manager.create_model()
86
+
87
+ # Initialize tools
88
+ guest_info_tool = create_menu_info_tool(retriever)
89
+ send_to_kitchen_tool = create_send_to_kitchen_tool(llm=llm)
90
+ tools = [guest_info_tool, send_to_kitchen_tool]
91
+
92
+ # Initialize the agent
93
+ waiter_agent = RestaurantAgent(
94
+ llm=llm,
95
+ restaurant_name=RESTAURANT,
96
+ tools=tools
97
+ )
98
+
99
+ log_success("Components initialized successfully.")
100
+ else:
101
+ log_warn("OpenRouter API key is required for LLM initialization.")
102
+
103
+ return {
104
+ "groq_client": groq_client is not None,
105
+ "eleven_client": eleven_client is not None,
106
+ "llm": llm is not None,
107
+ "agent": waiter_agent is not None
108
+ }
109
+
110
+ # region FUNCTIONS
111
+ async def handle_text_input(message, history, openrouter_key, groq_key, elevenlabs_key, model_name):
112
+ """Handles text input, generates response, updates chat history."""
113
+ global waiter_agent, llm
114
+
115
+ # Initialize components if needed
116
+ if waiter_agent is None or llm is None or model_name != getattr(llm, "model_name", ""):
117
+ status = initialize_components(openrouter_key, groq_key, elevenlabs_key, model_name)
118
+ if not status["agent"]:
119
+ return history + [
120
+ {"role": "user", "content": message},
121
+ {"role": "assistant", "content": "Error: Could not initialize the agent. Please check your API keys."}
122
+ ]
123
+
124
+ current_history = history if isinstance(history, list) else []
125
+ log_info("-" * 20)
126
+ log_info(f"Received text input: '{message}', current history: {current_history}")
127
+
128
+ try:
129
+ # 1. Actualizar el historial con el mensaje del usuario
130
+ user_message = {"role": "user", "content": message}
131
+ history_with_user = current_history + [user_message]
132
+
133
+ # 2. Invocar al agente con la consulta del usuario
134
+ log_info("Iniciando procesamiento con LangGraph...")
135
+
136
+ # Invocar el agente con el texto de la consulta
137
+ langchain_messages = []
138
+ for msg in current_history:
139
+ if msg["role"] == "user":
140
+ langchain_messages.append(HumanMessage(content=msg["content"]))
141
+ elif msg["role"] == "assistant":
142
+ langchain_messages.append(AIMessage(content=msg["content"]))
143
+
144
+ langchain_messages.append(HumanMessage(content=message))
145
+
146
+ graph_result = waiter_agent.invoke(langchain_messages)
147
+
148
+ log_debug(f"Resultado del agente: {graph_result}")
149
+
150
+ messages = graph_result.get("messages", [])
151
+ assistant_text = ""
152
+
153
+ for msg in reversed(messages):
154
+ # LangChain puede devolver diferentes clases de mensajes
155
+ if hasattr(msg, "__class__") and msg.__class__.__name__ == "AIMessage":
156
+ assistant_text = msg.content
157
+ break
158
+
159
+ if not assistant_text:
160
+ log_warn("No se encontró respuesta del asistente en los mensajes.")
161
+ assistant_text = "Lo siento, no sé cómo responder a eso."
162
+
163
+ log_info(f"Assistant text: '{assistant_text}'")
164
+
165
+ # 3. Actualizar el historial con el mensaje del asistente
166
+ assistant_message = {"role": "assistant", "content": assistant_text}
167
+ final_history = history_with_user + [assistant_message]
168
+
169
+ log_success("Tarea completada con éxito.")
170
+ return final_history
171
+
172
+ except Exception as e:
173
+ log_error(f"Error in handle_text_input function: {e}")
174
+ import traceback
175
+ traceback.print_exc()
176
+ return current_history + [
177
+ {"role": "user", "content": message},
178
+ {"role": "assistant", "content": f"Error: {str(e)}"}
179
+ ]
180
+
181
+ async def response(audio: tuple[int, np.ndarray], history, openrouter_key, groq_key, elevenlabs_key, model_name):
182
+ """Handles audio input, generates response, yields UI updates and audio."""
183
+ global waiter_agent, llm, groq_client, eleven_client
184
+
185
+ # Initialize components if needed
186
+ if waiter_agent is None or llm is None or groq_client is None or eleven_client is None or model_name != getattr(llm, "model_name", ""):
187
+ status = initialize_components(openrouter_key, groq_key, elevenlabs_key, model_name)
188
+ if not status["groq_client"]:
189
+ yield AdditionalOutputs(history + [{"role": "assistant", "content": "Error: Groq API key is required for audio processing."}])
190
+ return
191
+ if not status["eleven_client"]:
192
+ yield AdditionalOutputs(history + [{"role": "assistant", "content": "Error: ElevenLabs API key is required for audio processing."}])
193
+ return
194
+ if not status["agent"]:
195
+ yield AdditionalOutputs(history + [{"role": "assistant", "content": "Error: Could not initialize the agent. Please check your OpenRouter API key."}])
196
+ return
197
+
198
+ current_history = history if isinstance(history, list) else []
199
+ log_info("-" * 20)
200
+ log_info(f"Received audio, current history: {current_history}")
201
+
202
+ try:
203
+ # 1. Transcribir el audio a texto
204
+ audio_bytes = audio_to_bytes(audio)
205
+ transcript = await groq_client.audio.transcriptions.create(
206
+ file=("audio-file.mp3", audio_bytes),
207
+ model="whisper-large-v3-turbo",
208
+ response_format="verbose_json",
209
+ )
210
+ user_text = transcript.text.strip()
211
+
212
+ log_info(f"Transcription: '{user_text}'")
213
+
214
+ # 2. Actualizar el historial con el mensaje del usuario
215
+ user_message = {"role": "user", "content": user_text}
216
+ history_with_user = current_history + [user_message]
217
+
218
+ log_info(f"Yielding user message update to UI: {history_with_user}")
219
+ yield AdditionalOutputs(history_with_user)
220
+ await asyncio.sleep(0.04) # Permite que la UI se actualice antes de continuar
221
+
222
+ # 4. Invocar al agente con la consulta del usuario
223
+ log_info("Iniciando procesamiento con LangGraph...")
224
+
225
+ langchain_messages = []
226
+ for msg in current_history:
227
+ if msg["role"] == "user":
228
+ langchain_messages.append(HumanMessage(content=msg["content"]))
229
+ elif msg["role"] == "assistant":
230
+ langchain_messages.append(AIMessage(content=msg["content"]))
231
+
232
+ langchain_messages.append(HumanMessage(content=user_text))
233
+
234
+ graph_result = waiter_agent.invoke(langchain_messages)
235
+
236
+ log_debug(f"Resultado del agente: {graph_result}")
237
+
238
+ # Extraer la respuesta del último mensaje del asistente
239
+ messages = graph_result.get("messages", [])
240
+ assistant_text = ""
241
+
242
+ # Buscar el último mensaje del asistente
243
+ for msg in reversed(messages):
244
+ if hasattr(msg, "__class__") and msg.__class__.__name__ == "AIMessage":
245
+ assistant_text = msg.content
246
+ break
247
+
248
+ if not assistant_text:
249
+ log_warn("No se encontró respuesta del asistente en los mensajes.")
250
+ assistant_text = "Lo siento, no sé cómo responder a eso."
251
+
252
+ log_info(f"Assistant text: '{assistant_text}'")
253
+
254
+ # 5. Actualizar el historial con el mensaje del asistente
255
+ assistant_message = {"role": "assistant", "content": assistant_text}
256
+ final_history = history_with_user + [assistant_message]
257
+
258
+ # 6. Generar la respuesta de voz
259
+ log_info("Generating TTS...")
260
+ TARGET_SAMPLE_RATE = 24000 # <<< --- Tasa de muestreo deseada
261
+ tts_stream_generator = eleven_client.text_to_speech.convert(
262
+ text=assistant_text,
263
+ voice_id="Nh2zY9kknu6z4pZy6FhD",
264
+ model_id="eleven_flash_v2_5",
265
+ output_format="pcm_24000",
266
+ voice_settings=VoiceSettings(
267
+ stability=0.0,
268
+ similarity_boost=1.0,
269
+ style=0.0,
270
+ use_speaker_boost=True,
271
+ speed=1.1,
272
+ )
273
+ )
274
+
275
+ # --- Procesar los chunks a medida que llegan ---
276
+ log_info("Receiving and processing TTS audio chunks...")
277
+ audio_chunks = []
278
+ total_bytes = 0
279
+
280
+ for chunk in tts_stream_generator:
281
+ total_bytes += len(chunk)
282
+
283
+ # Convertir chunk actual de bytes PCM (int16) a float32 normalizado
284
+ if chunk:
285
+ audio_int16 = np.frombuffer(chunk, dtype=np.int16)
286
+ audio_float32 = audio_int16.astype(np.float32) / 32768.0
287
+ audio_float32 = np.clip(audio_float32, -1.0, 1.0) # Asegurar rango
288
+ audio_chunks.append(audio_float32)
289
+
290
+ log_info(f"Received {total_bytes} bytes of TTS audio in total.")
291
+
292
+ # Concatenar todos los chunks procesados
293
+ if audio_chunks:
294
+ final_audio = np.concatenate(audio_chunks)
295
+ log_info(f"Processed {len(final_audio)} audio samples.")
296
+ else:
297
+ log_warn("Warning: TTS returned empty audio stream.")
298
+ final_audio = np.array([], dtype=np.float32)
299
+
300
+ # Crear la tupla final
301
+ tts_output_tuple = (TARGET_SAMPLE_RATE, final_audio)
302
+
303
+ log_debug(f"TTS output: {tts_output_tuple}")
304
+ log_success("Tarea completada con éxito.")
305
+ yield tts_output_tuple
306
+ yield AdditionalOutputs(final_history)
307
+
308
+ except Exception as e:
309
+ log_error(f"Error in response function: {e}")
310
+ import traceback
311
+ traceback.print_exc()
312
+
313
+ yield np.array([]).astype(np.int16).tobytes()
314
+ yield AdditionalOutputs(current_history + [{"role": "assistant", "content": f"Error: {str(e)}"}])
315
+
316
+ def load_model_ids():
317
+ # Use asyncio to run the async function
318
+ try:
319
+ models = asyncio.run(fetch_openrouter_models())
320
+ # Extract model IDs and names
321
+ model_ids = [model["id"] for model in models]
322
+ return model_ids
323
+ except Exception as e:
324
+ log_error(f"Error loading model IDs: {e}")
325
+ return ["openai/gpt-4o-mini", "google/gemini-2.5-flash-preview", "anthropic/claude-3-5-sonnet"] # Fallback models
326
+ # endregion
327
+
328
+ with gr.Blocks() as demo:
329
+ gr.Markdown("# WAIter Chatbot")
330
+ with gr.Row():
331
+ text_openrouter_api_key = gr.Textbox(
332
+ label="OpenRouter API Key (required)",
333
+ placeholder="Enter your OpenRouter API key",
334
+ value=getenv("OPENROUTER_API_KEY") or "",
335
+ type="password",
336
+ )
337
+ text_groq_api_key = gr.Textbox(
338
+ label="Groq API Key (required for audio)",
339
+ placeholder="Enter your Groq API key",
340
+ value=getenv("GROQ_API_KEY") or "",
341
+ type="password",
342
+ )
343
+ text_elevenlabs_api_key = gr.Textbox(
344
+ label="Elevenlabs API Key (required for audio)",
345
+ placeholder="Enter your Elevenlabs API key",
346
+ value=getenv("ELEVENLABS_API_KEY") or "",
347
+ type="password",
348
+ )
349
+
350
+ chatbot = gr.Chatbot(
351
+ label="Agent",
352
+ type="messages",
353
+ value=[],
354
+ avatar_images=(
355
+ None, # User avatar
356
+ "https://em-content.zobj.net/source/twitter/376/hugging-face_1f917.png", # Assistant
357
+ ),
358
+ )
359
+
360
+ with gr.Row():
361
+ model_dropdown = gr.Dropdown(
362
+ label="Select Model",
363
+ choices=load_model_ids(),
364
+ value=getenv("MODEL") or "openai/gpt-4o-mini",
365
+ interactive=True
366
+ )
367
+
368
+ text_input = gr.Textbox(
369
+ label="Type your message",
370
+ placeholder="Type here and press Enter...",
371
+ show_label=True,
372
+ )
373
+
374
+ audio = WebRTC(
375
+ label="Speak Here",
376
+ mode="send-receive",
377
+ modality="audio",
378
+ )
379
+
380
+ text_input.submit(
381
+ fn=handle_text_input,
382
+ inputs=[
383
+ text_input,
384
+ chatbot,
385
+ text_openrouter_api_key,
386
+ text_groq_api_key,
387
+ text_elevenlabs_api_key,
388
+ model_dropdown
389
+ ],
390
+ outputs=[chatbot],
391
+ api_name="submit_text"
392
+ ).then(
393
+ fn=lambda: "", # Limpiar el campo de texto
394
+ outputs=[text_input]
395
+ )
396
+
397
+ # Se encarga de manejar la entrada de audio
398
+ audio.stream(
399
+ fn=ReplyOnPause(
400
+ response,
401
+ can_interrupt=True,
402
+ ),
403
+ inputs=[audio, chatbot, text_openrouter_api_key, text_groq_api_key, text_elevenlabs_api_key, model_dropdown],
404
+ outputs=[audio],
405
+ )
406
+
407
+ # Actualiza el historial de la conversación
408
+ audio.on_additional_outputs(
409
+ fn=lambda history_update: history_update, # Envia el historial actualizado
410
+ outputs=[chatbot], # Actualiza el chatbot
411
+ )
412
+
413
+ if __name__ == "__main__":
414
+ demo.launch()
data/carta.md ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BAR PACO
2
+ *Tradición y Sabor en el Corazón de la Ciudad*
3
+
4
+ 📍 Calle Ficticia, 123, Madrid | 🕛 Abierto todos los días: 12:00 - 00:00
5
+ ---
6
+
7
+ ## 🐖 LAS CHACINAS DE CARDESA
8
+ *Selección de los mejores ibéricos y quesos, cortados al momento.*
9
+
10
+ - **Bandeja de Jamón Ibérico de Bellota D.O. Extremadura** – **24,00 €**
11
+ *(100% Raza Ibérica, curación mínima 36 meses)*
12
+ (½ ración **14,00 €**)
13
+ - **Bandeja de Caña de Lomo Ibérico de Bellota D.O. Guijuelo** – **24,00 €**
14
+ *(Embuchado natural, sabor intenso y delicado)*
15
+ (½ ración **12,50 €**)
16
+ - **Tabla de Ibéricos de Bellota Variados** – **24,00 €**
17
+ *(Jamón, lomo, chorizo y salchichón ibérico de bellota)*
18
+ (½ ración **14,00 €**)
19
+ - **Plato de Cecina de León I.G.P. "Reserva"** – **20,00 €**
20
+ *(Curada al aire de la montaña leonesa, con un toque ahumado)*
21
+ (½ ración **12,00 €**)
22
+ - **Plato de Queso Curado de Oveja Artesano** – **16,00 €**
23
+ *(Selección de quesos manchegos y zamoranos con D.O.)*
24
+ (Porción individual **9,00 €**)
25
+ - **Mojama de Atún Extra de Barbate con Almendras Marconas** – **18,00 €**
26
+
27
+ > *Nuestras chacinas se sirven con picos camperos y regañás. Para una experiencia superior, recomendamos nuestro Pan de Cristal con Tomate Rallado y AOVE Premium – **4,00 €***
28
+
29
+ ---
30
+
31
+ ## 🍽️ PARA COMPARTIR (Tapas y Raciones)
32
+
33
+ **DE LA HUERTA Y CLÁSICOS:**
34
+ - **Ensalada Bar Paco** – **13,50 €**
35
+ *(Mezclum de lechugas, tomate, cebolla tierna, atún, huevo duro, espárragos y aceitunas)*
36
+ - **Tomates Rosados de Temporada con Ventresca de Bonito del Norte** – **15,90 €**
37
+ - **Tomate Rosado con Berberechos Gallegos al Natural** – **15,90 €**
38
+ - **Tomate Rosado con Burrata Fresca, Pesto Genovés Casero y Rúcula** – **17,00 €**
39
+ - **Tomate "Huevo de Toro" Aliñado (en temporada)** – **11,00 €**
40
+ *(Con ajo, comino, orégano y AOVE)*
41
+ - **Parrillada de Verduras de Temporada a la Brasa** – **16,90 €**
42
+ *(Espárragos, calabacín, berenjena, pimientos, cebolla, champiñones... con salsa romesco casera)*
43
+ - **Flores de Alcachofas Frescas a la Plancha con Lascas de Jamón Ibérico** – **17,90 €**
44
+ - **Alcachofas Confitadas a la Andaluza con Reducción de Pedro Ximénez** – **16,90 €**
45
+ - **Ensaladilla Rusa "Estilo Piera"** – **8,90 €**
46
+ *(Nuestra receta tradicional con ingredientes frescos y mayonesa casera)*
47
+ - **Patatas Bravas "Estilo Paco"** - **8,50 €**
48
+ *(Con nuestra salsa brava secreta y alioli suave)*
49
+ - **Morcilla de Burgos con Piñones Frita y Pimientos de Padrón** – **8,00 €**
50
+ - **Torreznos de Soria Crujientes D.O.** – **11,90 €**
51
+ - **Croquetas Cremosas Caseras de Jamón Ibérico de Bellota (8 uds.)** – **12,00 €**
52
+ - **Croquetas Caseras de Boletus Edulis y Trufa (8 uds.)** – **13,00 €**
53
+ - **Degustación de Croquetas (4 Jamón, 4 Boletus)** – **12,50 €**
54
+ - **Huevos Estrellados con Jamón Ibérico y Patatas Paja** – **15,50 €**
55
+ - **Huevos Estrellados con Gulas del Norte Salteadas y Gambas al Ajillo** – **15,90 €**
56
+ - **Huevos Estrellados con Chistorra de Navarra y Pimientos del Piquillo** – **14,90 €**
57
+
58
+ **DEL MAR:**
59
+ - **Anchoas del Cantábrico "00" en Salazón (8 lomos)** – **18,90 €**
60
+ *(Sobre lecho de tomate rallado y pan tostado)*
61
+ - **Boquerones de Málaga Frescos Fritos a la Andaluza** – **10,90 €**
62
+ - **Pulpo a la Brasa sobre Parmentier de Patata y Pimentón de la Vera** – **22,90 €**
63
+ - **Pulpo Frito con Alioli de Ajo Negro** - **22,90 €**
64
+ - **Chipirones Encebollados a Fuego Lento con Tinta y Arroz Blanco** – **18,90 €**
65
+ - **Chipirones Frescos Fritos a la Andaluza con Limón** – **18,90 €**
66
+ (½ ración **10,50 €**)
67
+ - **Calamar de Potera Fresco a la Plancha con Ajo y Perejil** – **20,50 €**
68
+ - **Calamar de Potera Fresco Frito a la Andaluza** – **19,00 €**
69
+ (½ ración **12,00 €**)
70
+ - **Mejillones de las Rías Baixas al Vapor o a la Marinera** – **12,90 €**
71
+ - **Gamba Blanca de Huelva Cocida, a la Plancha o al Ajillo (200g)** – **21,00 €**
72
+ - **Zamburiñas Frescas Gratinadas con Jamón Ibérico y Cebolla Caramelizada (6 uds.)** – **19,90 €**
73
+ - **Almejas Finas a la Marinera o al Ajillo** – **21,50 €**
74
+
75
+ ---
76
+
77
+ ## 🍲 LOS PUCHEROS DE LA ABUELA
78
+ *Platos de cuchara con sabor a tradición. Disponibles según temporada.*
79
+
80
+ - **Fabada Asturiana Tradicional con su Compango Casero** – **15,50 €**
81
+ - **Sopa Castellana con Huevo Escalfado y Virutas de Jamón** – **9,50 €**
82
+ - **Lentejas Estofadas con Chorizo y Verduras de la Huerta** – **12,00 €**
83
+ - **Callos a la Madrileña (receta de la casa)** – **14,50 €**
84
+ - **Potaje de Garbanzos con Espinacas y Bacalao (Viernes de Cuaresma y temporada)** - **13,50 €**
85
+
86
+ > *En verano, no te pierdas nuestro **Salmorejo Cordobés Casero** con virutas de jamón y huevo duro – **7,50 €** y nuestro **Gazpacho Andaluz Tradicional** - **7,00 €***
87
+
88
+ ---
89
+
90
+ ## 🥩 NUESTRAS CARNES A LA BRASA Y GUISADAS
91
+ *Selección de las mejores carnes nacionales, maduradas y cocinadas en nuestra parrilla de carbón de encina o guisadas con mimo.*
92
+ *(Todas nuestras carnes a la brasa se sirven con patatas fritas caseras y pimientos de Padrón)*
93
+
94
+ - **Entrecot de Vaca Madurada (350g aprox.)** – **23,00 €**
95
+ *(Maduración dry-aged mínima 30 días)*
96
+ - **Chuletón de Vaca Madurada (1,300 kg aprox., para 2 personas)** – **49,00 €**
97
+ *(Maduración dry-aged mínima 45 días, trinchado en mesa)*
98
+ - **Solomillo de Ternera Charra a la Parrilla (250g aprox.)** – **25,00 €**
99
+ - **Chuletillas de Cordero Lechal de Aranda D.O.** – **24,50 €**
100
+ - **Presa Ibérica de Bellota a la Parrilla** – **20,50 €**
101
+ *(Con mojo picón casero)*
102
+ - **Secreto Ibérico de Bellota a la Parrilla** – **19,50 €**
103
+ *(Con chimichurri artesanal)*
104
+ - **Parrillada de Cordero Lechal Deshuesado** – **19,50 €**
105
+ - **Rabo de Toro Estofado al Vino Tinto (Disponible según mercado, consultar)** – **25,00 €**
106
+ - **Mollejas de Cordero Lechal Salteadas al Ajillo o Empanadas** – **15,90 €**
107
+ - **Steak Tartar de Solomillo de Ternera (Preparado al momento a su gusto)** – **24,50 €**
108
+ - **Cochinillo Confitado y Crujiente (Cochifrito)** – **18,90 €**
109
+ - **Huevos Fritos de Corral con Jamón Ibérico de Bellota y Patatas Fritas Caseras** – **16,90 €**
110
+ - **Carrilleras de Cerdo Ibérico Estofadas al Pedro Ximénez con Puré de Boniato** – **18,50 €**
111
+
112
+ ---
113
+
114
+ ## 🔥 NUESTROS ASADOS TRADICIONALES (POR ENCARGO)
115
+ *Asados en horno de leña. Se recomienda encargar con 24h de antelación.*
116
+
117
+ - **Cordero Lechal Asado Entero (para 4-6 personas)** – Consultar precio
118
+ *(Cuarto delantero o trasero – **48,50 €**)*
119
+ - **Paletilla de Cordero Lechal Asada Individual** – **24,90 €**
120
+ - **¼ de Cabrito Lechal Asado (para 2-3 personas)** – **48,50 €**
121
+ - **Cochinillo Asado Entero (para 4-6 personas)** – Consultar precio
122
+ - **Ración de Cochinillo Tostón Asado** – **24,90 €**
123
+
124
+ > *Todos los asados se acompañan de patatas panaderas y ensalada mixta.*
125
+
126
+ ---
127
+
128
+ ## 🐟 PESCADOS FRESCOS DEL DÍA
129
+ *Directos de lonja, preparados a la brasa, al horno o según la tradición. Pregunte por nuestras sugerencias fuera de carta.*
130
+
131
+ - **Rodaballo Salvaje a la Parrilla con Bilbaína (pieza entera, para 2 pers.)** – S.P.M. (Según Precio de Mercado)
132
+ - **Bacalao Confitado a Baja Temperatura sobre Vizcaína o al Pil Pil** – **23,00 €**
133
+ - **Merluza de Pincho del Cantábrico a la Gallega, a la Romana o a la Plancha** – **19,90 €**
134
+ - **Lomo de Atún Rojo de Almadraba a la Plancha con Salsa de Soja y Sésamo** – **24,90 €**
135
+ - **Tartar de Atún Rojo de Almadraba con Aguacate y Mango** – **24,90 €**
136
+ - **Fritura Malagueña Variada (Boquerones, calamares, salmonetes, adobo...)** – **22,50 €**
137
+ (Para 2 personas)
138
+ - **Dados de Bacalao Fresco Fritos con Pimientos Asados** – **19,90 €**
139
+ - **Rodaballo Salvaje al Horno con Patatas Panaderas y Verduras (mín. 2-4 personas, pieza entera)** – S.P.M.
140
+ - **Lubina Salvaje a la Sal o a la Espalda (mín. 2 personas, pieza entera)** – **44,00 €**
141
+ - **Pargo Salvaje al Horno con Limón y Hierbas Provenzales (pieza entera)** – S.P.M.
142
+ - **Lenguado Fresco a la Plancha "Meunière"** – S.P.M.
143
+
144
+ ---
145
+
146
+ ## 🍚 ARROCES Y PAELLAS (MÍNIMO 2 PERSONAS)
147
+ *Nuestros arroces se elaboran al momento con ingredientes frescos. Tiempo de preparación aprox. 30-40 min. Precio por persona (P/P).*
148
+
149
+ - **Arroz a Banda Tradicional** – **17,90 €** P/P
150
+ *(Con delicioso fumet de pescado de roca y marisco, servido con alioli casero)*
151
+ - **Arroz del Senyoret (Marisco Pelado)** – **19,50 €** P/P
152
+ - **Arroz Negro con Sepia de Potera y Chipirones** – **18,50 €** P/P
153
+ - **Paella Valenciana Auténtica (Pollo de corral, conejo, judía verde y garrofó)** – **18,90 €** P/P
154
+ *(Por encargo preferiblemente)*
155
+ - **Paella de Marisco Fresco (Gambas, cigalas, mejillones, almejas...)** – **21,90 €** P/P
156
+ - **Arroz con Secreto Ibérico, Setas de Temporada y Espárragos Trigueros** – **17,90 €** P/P
157
+ - **Fideuá de Gandía con Marisco y Alioli** - **18,90 €** P/P
158
+
159
+ **ARROCES CALDOSOS Y MELOSOS:**
160
+ - **Arroz Caldoso de Bogavante Azul** – **23,90 €** P/P
161
+ - **Arroz Meloso de Carabineros y Nécoras** – **26,90 €** P/P
162
+ - **Arroz Meloso con Verduras de Temporada y Alcachofas** – **16,50 €** P/P (Opción Vegetariana)
163
+
164
+ ---
165
+
166
+ ## 🍰 POSTRES CASEROS
167
+ *El broche de oro perfecto para tu comida.*
168
+
169
+ - **Tarta de Queso Cremosa Casera al Horno con Frutos Rojos** – **7,50 €**
170
+ - **Flan de Huevo Casero con Nata Montada y Caramelo** – **6,50 €**
171
+ - **Arroz con Leche Asturiano Tradicional Caramelizado** – **7,00 €**
172
+ - **Tarta Fina de Manzana Reineta con Helado de Vainilla Bourbon** – **7,50 €**
173
+ - **Coulant de Chocolate Negro Belga (70%) con Corazón Fluido y Helado de Mandarina** – **7,50 €**
174
+ - **Milhojas de Hojaldre Crujiente con Crema Pastelera y Nata** – **7,50 €**
175
+ - **Torrijas Caseras de Leche y Canela con Helado de Leche Merengada (en temporada)** – **7,90 €**
176
+ - **Selección de Helados Artesanos (dos bolas a elegir)** – **6,50 €**
177
+ *(Vainilla, Chocolate, Fresa, Limón, Turrón, Leche Merengada)*
178
+ - **Sorbete de Limón al Cava o Mandarina** – **6,00 €**
179
+ - **Fruta Fresca de Temporada Preparada** – **5,50 €**
180
+
181
+ ---
182
+
183
+ ## 🥤 BEBIDAS
184
+
185
+ **AGUAS Y REFRESCOS:**
186
+ - Agua Mineral Natural (50cl) – **2,00 €**
187
+ - Agua con Gas (25cl) – **2,50 €**
188
+ - Refrescos (Coca-cola, Cola, Fanta Naranja, Limón, Tónica, etc.) – **2,80 €**
189
+ - Zumos Naturales (Naranja recién exprimido) – **4,00 €**
190
+ - Zumos Embotellados (Melocotón, Piña, Tomate) – **2,80 €**
191
+
192
+ **CERVEZAS:**
193
+ - Caña (Mahou Clásica / Tostada 0,0) – **2,50 €**
194
+ - Doble (Mahou Clásica / Tostada 0,0) – **3,20 €**
195
+ - Tercio Nacional (Mahou Cinco Estrellas, Alhambra Reserva 1925, Estrella Galicia) – **3,50 €**
196
+ - Cerveza Artesanal Local (Consultar selección) – desde **4,50 €**
197
+ - Clara con Limón o Casera – **3,00 €**
198
+ - Cerveza Sin Alcohol / Sin Gluten – **3,50 €**
199
+
200
+ **VINOS (Consultar nuestra Carta de Vinos completa):**
201
+ - **Vino de la Casa (Tinto, Blanco, Rosado D.O. La Mancha)**
202
+ - Copa – **2,80 €**
203
+ - Botella – **14,00 €**
204
+ - **Generosos de Jerez y Montilla-Moriles:**
205
+ - Fino, Manzanilla – Copa **2,50 €**
206
+ - Amontillado, Oloroso, Palo Cortado – Copa desde **3,50 €**
207
+ - Pedro Ximénez – Copa **4,00 €**
208
+ - *Amplia selección de D.O. Rioja, Ribera del Duero, Rueda, Rías Baixas, Toro, Priorat, Cava, Champagne, etc.*
209
+
210
+ **VERMUTS Y APERITIVOS:**
211
+ - Vermut de Grifo Casero (Rojo o Blanco) – **3,50 €**
212
+ - Selección de Vermuts Premium (Yzaguirre, Lustau, Miró...) – desde **4,50 €**
213
+ - Aperol Spritz, Campari Soda – **7,00 €**
214
+
215
+ **CAFÉS E INFUSIONES:**
216
+ - Café Solo, Cortado, Americano – **1,80 €**
217
+ - Café con Leche, Capuccino – **2,20 €**
218
+ - Carajillo (Brandy, Ron, Whisky, Anís) – **3,00 €**
219
+ - Infusiones Variadas (Té, Manzanilla, Poleo-Menta) – **2,00 €**
220
+
221
+ **LICORES Y COMBINADOS:**
222
+ - Chupito de Licor de Hierbas, Orujo, Pacharán – **3,00 €**
223
+ - Combinados Nacionales (Ron, Ginebra, Whisky) – **7,00 €**
224
+ - Combinados Premium (Consultar) – desde **9,00 €**
225
+
226
+ ---
227
+
228
+ ## 📝 INFORMACIÓN ADICIONAL
229
+
230
+ - **MENÚ DEL DÍA:** Disponible de Lunes a Viernes (excepto festivos).
231
+ *Incluye primero, segundo, pan, bebida y postre o café. Consultar pizarra.* – **14,50 €**
232
+ - **SUGERENCIAS DEL CHEF:** Pregunte por nuestras especialidades fuera de carta y productos de temporada.
233
+ - **PAN Y APERITIVO:** Servicio de pan artesano y aperitivo de la casa por persona – **1,80 €** (Opcional, comuníquelo si no lo desea).
234
+ - **ALÉRGENOS:** Este establecimiento dispone de información detallada sobre la presencia de alérgenos en nuestros productos. Por favor, consulte a nuestro personal.
235
+ - **RESERVAS:** Se recomienda reservar, especialmente fines de semana y festivos.
236
+ - **WIFI:** Disponemos de Wi-Fi gratuito para clientes.
237
+ - **IVA INCLUIDO** en todos los precios.
238
+
239
+ ---
240
+
241
+ 📞 **Reservas:** 91 579 10 18
242
+ 📧 **Email:** [email protected]
243
+ 🌐 **Web:** [www.elpacorestaurante.com](http://www.elpacorestaurante.com)
244
+ 📷 **Instagram:** @barpacorestaurante
245
+ 🐦 **Twitter:** @barpacorestaurante
246
+
247
+ *¡Gracias por su visita y buen provecho!*
model.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from os import getenv
2
+ from typing import Optional
3
+ from langchain_openai import ChatOpenAI
4
+
5
+ class ModelManager:
6
+ """
7
+ A class to manage the creation and configuration of language model instances.
8
+ Handles API keys and provides fallbacks if environment variables are not found.
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ api_key: Optional[str] = None,
14
+ api_base: Optional[str] = None,
15
+ model_name: str = "google/gemini-2.5-flash-preview",
16
+ helicone_api_key: Optional[str] = None,
17
+ ):
18
+ """
19
+ Initialize the ModelManager with the specified parameters.
20
+
21
+ Args:
22
+ api_key: The API key for the language model service. If None, will try to get from environment.
23
+ api_base: The base URL for the language model service. If None, will try to get from environment.
24
+ model_name: The name of the model to use.
25
+ helicone_api_key: The Helicone API key for logging. If None, will try to get from environment.
26
+ """
27
+
28
+ self.api_key = api_key
29
+ if not self.api_key:
30
+ print("Warning: No API key found.")
31
+
32
+ self.api_base = api_base
33
+ if not self.api_base:
34
+ print("Warning: No API base URL found.")
35
+
36
+ self.helicone_api_key = helicone_api_key
37
+
38
+ self.model_name = model_name
39
+
40
+ def create_model(self, **kwargs) -> Optional[ChatOpenAI]:
41
+ """
42
+ Create and return a configured language model instance.
43
+
44
+ Args:
45
+ **kwargs: Additional keyword arguments to pass to the model constructor.
46
+
47
+ Returns:
48
+ A configured ChatOpenAI instance, or None if required parameters are missing.
49
+ """
50
+ # Check if required parameters are available
51
+ if not self.api_key or not self.api_base:
52
+ print("Error: Cannot create model. Missing required API key or base URL.")
53
+ return None
54
+
55
+ # Prepare model kwargs
56
+ model_kwargs = kwargs.get("model_kwargs", {})
57
+
58
+ # Add Helicone headers if available
59
+ if self.helicone_api_key:
60
+ extra_headers = model_kwargs.get("extra_headers", {})
61
+ extra_headers["Helicone-Auth"] = f"Bearer {self.helicone_api_key}"
62
+ model_kwargs["extra_headers"] = extra_headers
63
+
64
+ # Update kwargs with new model_kwargs
65
+ kwargs["model_kwargs"] = model_kwargs
66
+
67
+ # Create and return the model
68
+ try:
69
+ return ChatOpenAI(
70
+ openai_api_key=self.api_key,
71
+ openai_api_base=self.api_base,
72
+ model_name=self.model_name,
73
+ **kwargs
74
+ )
75
+ except Exception as e:
76
+ print(f"Error creating model: {str(e)}")
77
+ return None
requirements.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WAIter dependencies
2
+
3
+ # Core
4
+ gradio>=4.26.0
5
+ langchain>=0.1.0
6
+ langchain-openai>=0.0.5
7
+ langchain-huggingface>=0.0.2
8
+ langchain-text-splitters>=0.0.1
9
+ langchain-core>=0.1.17
10
+
11
+ # Language models
12
+ openai>=1.10.0
13
+ groq>=0.4.0
14
+
15
+ # Vector store
16
+ sentence-transformers>=2.2.2
17
+
18
+ # Audio processing
19
+ fastrtc>=0.6.0
20
+ elevenlabs>=0.2.24
21
+ numpy>=1.24.0
22
+
23
+ # Database
24
+ supabase>=2.0.0
25
+
26
+ # Utilities
27
+ python-dotenv>=1.0.0
28
+ qrcode>=7.4.2
29
+ pillow>=10.0.0
supabase_client.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase import create_client
2
+ import os
3
+ from dotenv import load_dotenv
4
+ from typing import Dict, List, Any, Optional
5
+ import json
6
+ import logging
7
+
8
+ from utils.classes import Order
9
+ # Configurar logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class SupabaseOrderManager:
14
+ def __init__(self, url: Optional[str] = None, key: Optional[str] = None):
15
+ """
16
+ Inicializa el gestor de órdenes con Supabase.
17
+
18
+ Args:
19
+ url: URL de Supabase (opcional, por defecto usa variables de entorno)
20
+ key: Clave de API de Supabase (opcional, por defecto usa variables de entorno)
21
+ """
22
+ # Cargar variables de entorno
23
+ load_dotenv()
24
+
25
+ # Usar parámetros proporcionados o variables de entorno
26
+ self.supabase_url = url or os.getenv("SUPABASE_URL")
27
+ self.supabase_key = key or os.getenv("SUPABASE_KEY")
28
+
29
+ if not self.supabase_url or not self.supabase_key:
30
+ raise ValueError("SUPABASE_URL y SUPABASE_KEY deben estar definidos como variables de entorno o proporcionados como parámetros")
31
+
32
+ # Inicializar cliente
33
+ self.supabase = create_client(self.supabase_url, self.supabase_key)
34
+ logger.info("Cliente Supabase inicializado correctamente")
35
+
36
+ async def send_order(self, order: Order) -> Dict[str, Any]:
37
+ """
38
+ Envía una orden a Supabase.
39
+
40
+ Args:
41
+ order: Objeto Order del sistema del restaurante
42
+
43
+ Returns:
44
+ Diccionario con el resultado de la operación
45
+ """
46
+ try:
47
+ # Convertir el objeto Order a un diccionario
48
+ order_dict = order.to_dict()
49
+
50
+ # 1. Insertar la orden principal
51
+ order_data = {
52
+ "order_id": order_dict["order_id"],
53
+ "table_number": str(order_dict["table_number"]),
54
+ "special_instructions": order_dict["special_instructions"],
55
+ "status": "pending"
56
+ }
57
+
58
+ # Realizar la inserción de la orden
59
+ order_response = self.supabase.table("orders").insert(order_data).execute()
60
+
61
+ if not order_response.data:
62
+ raise Exception("Error al insertar orden en Supabase")
63
+
64
+ # Obtener el ID de la orden insertada
65
+ db_order_id = order_response.data[0]["id"]
66
+
67
+ # 2. Insertar los elementos de la orden
68
+ items_to_insert = []
69
+ for item in order_dict["items"]:
70
+ items_to_insert.append({
71
+ "order_id": db_order_id,
72
+ "name": item["name"],
73
+ "quantity": item.get("quantity", 1),
74
+ "variations": item.get("variations", "")
75
+ })
76
+
77
+ # Realizar la inserción de los elementos
78
+ if items_to_insert:
79
+ items_response = self.supabase.table("order_items").insert(items_to_insert).execute()
80
+
81
+ if not items_response.data:
82
+ # Si falla la inserción de items, eliminamos la orden para mantener consistencia
83
+ self.supabase.table("orders").delete().eq("id", db_order_id).execute()
84
+ raise Exception("Error al insertar elementos de la orden en Supabase")
85
+
86
+ logger.info(f"Orden {order_dict['order_id']} enviada correctamente a Supabase")
87
+ return {
88
+ "success": True,
89
+ "order_id": order_dict["order_id"],
90
+ "database_id": db_order_id
91
+ }
92
+
93
+ except Exception as e:
94
+ logger.error(f"Error al enviar orden a Supabase: {str(e)}")
95
+ return {
96
+ "success": False,
97
+ "error": str(e)
98
+ }
99
+
tools.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.tools import Tool
2
+ from langchain_core.vectorstores import VectorStoreRetriever
3
+ from typing import Optional
4
+
5
+ from utils.logger import log_info, log_warn, log_error, log_debug
6
+ import json
7
+ from langchain_core.messages import HumanMessage, SystemMessage
8
+ from langchain_openai import ChatOpenAI
9
+ from utils.classes import Order
10
+
11
+ from supabase_client import SupabaseOrderManager
12
+ import asyncio
13
+
14
+ try:
15
+ supabase = SupabaseOrderManager()
16
+ except Exception as e:
17
+ log_error(f"Error al inicializar el cliente de Supabase: {e}")
18
+ supabase = None
19
+
20
+
21
+ def create_menu_info_tool(retriever: VectorStoreRetriever) -> Tool:
22
+ """
23
+ Crea una herramienta para extraer información relevante del menú del restaurante.
24
+
25
+ Args:
26
+ retriever: Un retriever configurado para buscar en la base de conocimiento del menú
27
+
28
+ Returns:
29
+ Una herramienta de LangChain para consultar información del menú
30
+ """
31
+ def extract_text(query: str) -> str:
32
+ """Extrae texto relevante del menú basado en la consulta."""
33
+ results = retriever.invoke(query)
34
+ log_info("ENTRADA DE EXTRACCIÓN DE TEXTO")
35
+
36
+ result_texts = []
37
+ if results:
38
+ log_info("\n=== Fragmentos de documento utilizados para la respuesta ===")
39
+ for i, result in enumerate(results):
40
+ log_info(f"Fragmento {i+1}: {result.page_content[:100]}...")
41
+
42
+ # Comprobar si tiene score (algunos retrievers no incluyen este atributo)
43
+ if hasattr(result, 'score'):
44
+ log_info(f"Score: {result.score}")
45
+
46
+ result_texts.append(result.page_content)
47
+ log_info("=========================================================\n")
48
+
49
+ # Unir los resultados relevantes
50
+ return "\n\n".join(result_texts)
51
+ else:
52
+ return "Lo siento, no tengo información sobre eso."
53
+
54
+ return Tool(
55
+ name="guest_info_tool",
56
+ description="""Herramienta para consultar información detallada del menú del restaurante.
57
+ Úsala cuando necesites:
58
+ - Buscar platos específicos y verificar su disponibilidad
59
+ - Consultar precios exactos de productos
60
+ - Obtener información sobre ingredientes, alérgenos o composición de platos
61
+ - Explorar secciones del menú (entrantes, principales, postres, bebidas, etc.)
62
+ - Verificar la existencia de productos antes de recomendarlos
63
+ - Responder preguntas específicas sobre la carta del restaurante
64
+
65
+ Esta herramienta accede al contenido completo del menú para proporcionar información precisa y actualizada.""",
66
+ func=extract_text,
67
+ )
68
+
69
+ def create_send_to_kitchen_tool(llm: ChatOpenAI) -> Tool:
70
+ """
71
+ Crea una herramienta para enviar pedidos a la cocina.
72
+
73
+ Args:
74
+ llm: Un modelo de lenguaje para analizar la conversación
75
+
76
+ Returns:
77
+ Una herramienta de LangChain para enviar pedidos a la cocina
78
+ """
79
+ def extract_order_from_summary(conversation_summary: str) -> Order:
80
+ """
81
+ Usa un LLM para extraer detalles del pedido a partir de un resumen de la conversación.
82
+
83
+ Args:
84
+ conversation_summary: Resumen de la conversación entre cliente y camarero
85
+
86
+ Returns:
87
+ Objeto Order con los detalles del pedido extraído
88
+ """
89
+ # Crear mensaje para el LLM
90
+ messages = [
91
+ SystemMessage(content="""
92
+ Eres un asistente especializado en extraer información de pedidos de restaurante.
93
+ Analiza el siguiente resumen de conversación entre un cliente y un camarero.
94
+ Extrae SOLO los elementos del pedido (platos, bebidas, etc.), cantidades, y cualquier instrucción especial.
95
+ Devuelve los resultados en formato JSON entre las etiquetas <order> </order> estrictamente con esta estructura:
96
+ {
97
+ "table_number": número_de_mesa (entero o "desconocida" si no se especifica),
98
+ "items": [
99
+ {
100
+ "name": "nombre_del_plato",
101
+ "quantity": cantidad (entero, por defecto 1),
102
+ "variations": "variaciones o personalizaciones"
103
+ },
104
+ ...
105
+ ],
106
+ "special_instructions": "instrucciones especiales generales"
107
+ }
108
+ No incluyas ninguna otra información o explicación, SOLO el JSON entre las etiquetas.
109
+ """),
110
+ HumanMessage(content=f"Resumen de la conversación: {conversation_summary}")
111
+ ]
112
+
113
+ # Invocar el LLM para obtener el análisis del pedido
114
+ response = llm.invoke(messages)
115
+ response_text = response.content
116
+
117
+ # Extraer el JSON de la respuesta usando las etiquetas <order></order>
118
+ try:
119
+ # Buscar contenido entre etiquetas <order> y </order>
120
+ import re
121
+ order_pattern = re.compile(r'<order>(.*?)</order>', re.DOTALL)
122
+ order_match = order_pattern.search(response_text)
123
+
124
+ if order_match:
125
+ # Extraer el contenido JSON de las etiquetas
126
+ json_str = order_match.group(1).strip()
127
+ order_data = json.loads(json_str)
128
+
129
+ # Crear objeto Order con los datos extraídos
130
+ return Order(
131
+ items=order_data.get("items", []),
132
+ special_instructions=order_data.get("special_instructions", ""),
133
+ table_number=order_data.get("table_number", "desconocida")
134
+ )
135
+ else:
136
+ # Si no hay etiquetas, reportar error
137
+ log_error("No se encontraron etiquetas <order> en la respuesta del LLM")
138
+ # Devolver un objeto Order vacío con un flag de error
139
+ empty_order = Order(table_number="desconocida")
140
+ empty_order.error = "NO_TAGS_FOUND"
141
+ return empty_order
142
+
143
+ except json.JSONDecodeError as e:
144
+ log_error(f"Error al parsear JSON de la respuesta del LLM: {e}")
145
+ log_debug(f"Respuesta problemática: {response_text}")
146
+ empty_order = Order(table_number="desconocida")
147
+ empty_order.error = "JSON_PARSE_ERROR"
148
+ return empty_order
149
+ except Exception as e:
150
+ log_error(f"Error inesperado al procesar la respuesta: {e}")
151
+ log_debug(f"Respuesta completa: {response_text}")
152
+ empty_order = Order(table_number="desconocida")
153
+ empty_order.error = "UNKNOWN_ERROR"
154
+ return empty_order
155
+
156
+ def send_to_kitchen(conversation_summary: str) -> str:
157
+ """
158
+ Procesa el resumen de la conversación para extraer el pedido y enviarlo a la cocina.
159
+
160
+ Args:
161
+ conversation_summary: Resumen de la conversación cliente-camarero
162
+
163
+ Returns:
164
+ Mensaje de confirmación
165
+ """
166
+ try:
167
+ log_info(f"Procesando resumen para enviar pedido a cocina...")
168
+ log_debug(f"Resumen recibido: {conversation_summary}")
169
+
170
+ # Extraer el pedido a partir del resumen
171
+ order = extract_order_from_summary(conversation_summary)
172
+
173
+ # Verificar si hay un error en el procesamiento
174
+ if hasattr(order, 'error') and order.error:
175
+ if order.error == "NO_TAGS_FOUND":
176
+ log_error("No se encontraron las etiquetas <order> en la respuesta del LLM")
177
+ return "Lo siento, ha ocurrido un problema al procesar su pedido. Por favor, inténtelo de nuevo."
178
+ elif order.error == "JSON_PARSE_ERROR":
179
+ log_error("Error al analizar el JSON en las etiquetas <order>")
180
+ return "Ha ocurrido un error técnico al procesar su pedido. ¿Podría repetirlo de otra forma?"
181
+ else:
182
+ log_error(f"Error desconocido: {order.error}")
183
+ return "Lo siento, algo salió mal al procesar su pedido. Por favor, inténtelo de nuevo."
184
+
185
+ # Verificar si hay elementos en el pedido
186
+ if not order.items:
187
+ log_warn("No se identificaron artículos en el pedido")
188
+ return "No se pudo identificar ningún artículo en el pedido. ¿Podría repetir su pedido, por favor?"
189
+
190
+ # Simular envío a la cocina
191
+ order_dict = order.to_dict()
192
+ log_info(f"ENVIANDO PEDIDO A COCINA: {json.dumps(order_dict, indent=2, ensure_ascii=False)}")
193
+
194
+ # Aquí iría la integración real con el sistema de la cocina
195
+ # Por ejemplo, enviar a una API, base de datos, etc.
196
+ async def async_send_and_get_result(order):
197
+ return await supabase.send_order(order)
198
+
199
+ res = asyncio.run(async_send_and_get_result(order))
200
+ if res.get("success"):
201
+ log_info(f"Pedido enviado correctamente a la cocina: {res['order_id']}")
202
+ return f"Su pedido ha sido enviado a la cocina. ID de pedido: {res['order_id']}"
203
+ else:
204
+ log_error(f"Error al enviar el pedido a la cocina: {res.get('error', 'Desconocido')}")
205
+ return "Lo siento, hubo un problema al enviar su pedido a la cocina. ¿Podría intentarlo de nuevo?"
206
+
207
+ except Exception as e:
208
+ log_error(f"Error al procesar pedido: {e}")
209
+ log_debug(f"Error detallado: {str(e)}")
210
+ import traceback
211
+ log_debug(traceback.format_exc())
212
+ return "Lo siento, hubo un problema al procesar su pedido. ¿Podría intentarlo de nuevo?"
213
+
214
+ # Retornar la herramienta configurada con la función send_to_kitchen
215
+ return Tool(
216
+ name="send_to_kitchen_tool",
217
+ description="""
218
+ Envía el pedido completo a la cocina. Usa esta herramienta SOLAMENTE cuando el cliente haya terminado de hacer su pedido
219
+ completo y esté listo para enviarlo.
220
+
221
+ Esta herramienta espera recibir un RESUMEN de la conversación que describe los elementos del pedido.
222
+ No envíes la conversación completa, solo un resumen claro de lo que el cliente ha pedido, la mesa,
223
+ y cualquier instrucción especial relevante.
224
+ """,
225
+ func=send_to_kitchen,
226
+ )
utils/classes.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from datetime import datetime
3
+
4
+ class Order:
5
+ """Representa un pedido de comida para la cocina."""
6
+ def __init__(self, items=None, special_instructions=None, table_number=None):
7
+ self.items = items or []
8
+ self.special_instructions = special_instructions or ""
9
+ self.table_number = table_number
10
+ self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
11
+ self.order_id = f"ORD-{datetime.now().strftime('%Y%m%d%H%M%S')}"
12
+ self.error = None # Campo para indicar si hay un error
13
+
14
+ def add_item(self, item: Dict[str, Any]):
15
+ """Añade un elemento al pedido."""
16
+ self.items.append(item)
17
+
18
+ def to_dict(self):
19
+ """Convierte el pedido a un diccionario."""
20
+ return {
21
+ "order_id": self.order_id,
22
+ "timestamp": self.timestamp,
23
+ "table_number": self.table_number,
24
+ "items": self.items,
25
+ "special_instructions": self.special_instructions,
26
+ "error": self.error
27
+ }
28
+
29
+ def __str__(self):
30
+ """Representación en texto del pedido."""
31
+ if self.error:
32
+ return f"Pedido con error: {self.error}"
33
+
34
+ items_str = ", ".join([f"{item.get('quantity', 1)}x {item.get('name', 'item')}" for item in self.items])
35
+ return f"Pedido {self.order_id} - Mesa {self.table_number}: {items_str}"
utils/functions.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ from typing import List, Dict, Any
3
+
4
+ async def fetch_openrouter_models() -> List[Dict[str, Any]]:
5
+ """
6
+ Asynchronously fetch available models from the OpenRouter API.
7
+
8
+ Returns:
9
+ A list of dictionaries containing model information.
10
+ Empty list if the request fails.
11
+ """
12
+ # Define the endpoint
13
+ models_endpoint = "https://openrouter.ai/api/v1/models"
14
+
15
+ try:
16
+ # Prepare headers
17
+ headers = {
18
+ "Content-Type": "application/json"
19
+ }
20
+
21
+ # Make the async request
22
+ async with aiohttp.ClientSession() as session:
23
+ async with session.get(models_endpoint, headers=headers) as response:
24
+ # Check if request was successful
25
+ if response.status == 200:
26
+ data = await response.json()
27
+ return data.get("data", [])
28
+ else:
29
+ error_text = await response.text()
30
+ print(f"Error fetching models: HTTP {response.status} - {error_text}")
31
+ return []
32
+
33
+ except Exception as e:
34
+ print(f"Error fetching models: {str(e)}")
35
+ return []
36
+
37
+
38
+ async def list_openrouter_models_summary() -> List[Dict[str, Any]]:
39
+ """
40
+ Asynchronously returns a simplified list of available OpenRouter models with key information.
41
+
42
+ Returns:
43
+ A list of dictionaries with model ID, name, context length, and pricing.
44
+ """
45
+ models = await fetch_openrouter_models()
46
+
47
+ summary = []
48
+ for model in models:
49
+ summary.append({
50
+ "id": model.get("id"),
51
+ "name": model.get("name"),
52
+ "context_length": model.get("context_length"),
53
+ "modality": model.get("architecture", {}).get("modality"),
54
+ "pricing": {
55
+ "prompt": model.get("pricing", {}).get("prompt"),
56
+ "completion": model.get("pricing", {}).get("completion")
57
+ }
58
+ })
59
+
60
+ return summary
utils/logger.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de logging para la aplicación wAIter.
3
+ Proporciona funciones para imprimir logs en colores según su importancia.
4
+ """
5
+
6
+ class Colors:
7
+ """Constantes de colores ANSI para los logs en terminal."""
8
+ RESET = "\033[0m"
9
+ RED = "\033[91m"
10
+ GREEN = "\033[92m"
11
+ YELLOW = "\033[93m"
12
+ BLUE = "\033[94m"
13
+ MAGENTA = "\033[95m"
14
+ CYAN = "\033[96m"
15
+ WHITE = "\033[97m"
16
+ BOLD = "\033[1m"
17
+
18
+ def log_info(msg):
19
+ """Log información general (azul cian)."""
20
+ print(f"{Colors.CYAN}[INFO] {msg}{Colors.RESET}")
21
+
22
+ def log_warn(msg):
23
+ """Log advertencias (amarillo)."""
24
+ print(f"{Colors.YELLOW}[WARN] {msg}{Colors.RESET}")
25
+
26
+ def log_error(msg):
27
+ """Log errores (rojo)."""
28
+ print(f"{Colors.RED}[ERROR] {msg}{Colors.RESET}")
29
+
30
+ def log_success(msg):
31
+ """Log éxitos/completados (verde)."""
32
+ print(f"{Colors.GREEN}[SUCCESS] {msg}{Colors.RESET}")
33
+
34
+ def log_debug(msg):
35
+ """Log depuración detallada (magenta)."""
36
+ print(f"{Colors.MAGENTA}[DEBUG] {msg}{Colors.RESET}")