feat: add agents/app

This commit is contained in:
2025-03-24 20:33:04 -05:00
parent cb63404b50
commit b6b94ac2b5
37 changed files with 3396 additions and 1 deletions

View File

View File

@@ -0,0 +1,44 @@
from langgraph.graph import StateGraph, END
from .nodes import (
ChatBotState,
classifier_agent,
general_info_agent,
catalog_agent,
order_agent,
)
def create_chat_graph():
# Crear el grafo con el estado tipado
graph = StateGraph(ChatBotState)
# Añadir nodos
graph.add_node("classifier", classifier_agent)
graph.add_node("general_info", general_info_agent)
graph.add_node("catalog", catalog_agent)
graph.add_node("order", order_agent)
# Configurar el punto de entrada
graph.set_entry_point("classifier")
# Función de enrutamiento basada en la categoría
def route_to_agent(state):
return state["category"]
# Añadir bordes condicionales
graph.add_conditional_edges(
"classifier",
route_to_agent,
{
"general_info": "general_info",
"catalog": "catalog",
"order": "order",
},
)
# Conectar al nodo final
graph.add_edge("general_info", END)
graph.add_edge("catalog", END)
graph.add_edge("order", END)
return graph.compile()

View File

@@ -0,0 +1,199 @@
from typing import TypedDict, Annotated, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.prebuilt import create_react_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from dotenv import load_dotenv
from .tools.general_info import tools as general_info_tools
# from .tools.catalog.catalog_tools import tools as catalog_tools
from .tools.catalog.catalog_tools import tools as catalog_tools
from .tools.orders.order_tools import tools as order_tools
from .tools.orders.order_tools_2 import tools as order_tools_2
from app.langchain_tools.llm import load_llm_openai
import yaml
load_dotenv()
with open("app/langgraph_tools/prompts.yaml", "r") as f:
PROMPTS = yaml.safe_load(f)
class ChatBotState(TypedDict):
messages: Annotated[List[BaseMessage], "add_messages"]
query: str
category: str
response: str
phone: str
def classifier_agent(state: ChatBotState) -> ChatBotState:
"""
Agente clasificador que procesa el estado del chat y determina la categoría.
"""
llm = load_llm_openai()
# Crear el prompt template - Incluyendo tanto el system message como el query
prompt_template = ChatPromptTemplate.from_messages(
[
("system", PROMPTS["classifier"]["system"]), # Este ya incluye {query}
MessagesPlaceholder(variable_name="messages"),
]
)
# Preparar los mensajes con la query actual
current_messages = state["messages"] + [HumanMessage(content=state["query"])]
# Preparar inputs con ambas variables requeridas por el prompt
inputs = {
"messages": current_messages,
"query": state["query"], # Necesario porque el prompt usa {query}
}
# Invocar el LLM
response = llm.invoke(prompt_template.invoke(inputs))
# Validar y normalizar la respuesta
category = response.content.strip().lower()
valid_categories = {"general_info", "catalog", "order"}
if category not in valid_categories:
# Si estamos en un contexto de pedido, default a "order"
if any(
word in state["query"].lower()
for word in [
"calle",
"carrera",
"avenida",
"dirección",
"confirmar",
"pedido",
]
):
category = "order"
else:
category = "catalog" # Categoría por defecto más segura
return {
**state,
"category": category,
"messages": state["messages"],
}
def general_info_agent(state: ChatBotState) -> ChatBotState:
try:
llm = load_llm_openai()
# Crear el prompt template para el agente
prompt = ChatPromptTemplate.from_messages(
[
("system", PROMPTS["general_info"]["system"].format(telefono=state["phone"])),
MessagesPlaceholder(variable_name="messages"),
]
)
# Crear el agente React con las herramientas
agent = create_react_agent(
model=llm, tools=general_info_tools, state_modifier=prompt
)
# Preparar los mensajes incluyendo la query actual
current_messages = state["messages"] + [HumanMessage(content=state["query"])]
# Crear la entrada del agente
inputs = {"messages": current_messages}
# Ejecutar el agente y obtener la respuesta
response = agent.invoke(inputs)
return {
"messages": response["messages"], # Actualizar con todos los mensajes
"response": response["messages"][-1].content,
}
except Exception as e:
error_message = f"Lo siento, hubo un error: {str(e)}"
return {
"messages": state["messages"] + [AIMessage(content=error_message)],
"response": error_message,
}
def catalog_agent(state: ChatBotState) -> ChatBotState:
try:
llm = load_llm_openai()
# Crear el prompt template para el agente
prompt = ChatPromptTemplate.from_messages(
[
("system", PROMPTS["catalog"]["system"].format(telefono=state["phone"])),
MessagesPlaceholder(variable_name="messages"),
]
)
# Crear el agente React con las herramientas
agent = create_react_agent(
model=llm, tools=catalog_tools, state_modifier=prompt
)
# Preparar los mensajes incluyendo la query actual
current_messages = state["messages"] + [HumanMessage(content=state["query"])]
# Crear la entrada del agente
inputs = {"messages": current_messages}
# Ejecutar el agente y obtener la respuesta
response = agent.invoke(inputs)
return {
"messages": response["messages"],
"response": response["messages"][-1].content,
}
except Exception as e:
error_message = f"Lo siento, hubo un error en el catálogo: {str(e)}"
return {
"messages": state["messages"] + [AIMessage(content=error_message)],
"response": error_message,
}
def order_agent(state: ChatBotState) -> ChatBotState:
try:
llm = load_llm_openai()
# Crear el prompt template para el agente
prompt = ChatPromptTemplate.from_messages(
[
("system", PROMPTS["order"]["system"].format(telefono=state["phone"])),
MessagesPlaceholder(variable_name="messages"),
]
)
# Crear el agente React con las herramientas de órdenes
# agent = create_react_agent(model=llm, tools=order_tools, state_modifier=prompt)
agent = create_react_agent(model=llm, tools=order_tools_2, state_modifier=prompt)
# Preparar los mensajes incluyendo la query actual
current_messages = state["messages"] + [HumanMessage(content=state["query"])]
# Crear la entrada del agente
inputs = {"messages": current_messages}
# Ejecutar el agente y obtener la respuesta
response = agent.invoke(inputs)
# Mantener el estado original y actualizar solo los campos necesarios
return {
**state, # Mantener todas las propiedades del estado original
"messages": response["messages"],
"response": response["messages"][-1].content,
}
except Exception as e:
error_message = f"Lo siento, hubo un error en el manejo de la orden: {str(e)}"
return {
**state, # Mantener todas las propiedades del estado original
"messages": state["messages"] + [AIMessage(content=error_message)],
"response": error_message,
}

View File

@@ -0,0 +1,615 @@
classifier:
system: |
Eres un clasificador de consultas de alta precisión para la Tienda la Ilusión.
Tu ÚNICA función es determinar la categoría correcta para cada mensaje del usuario.
NO debes procesar la solicitud ni dar respuestas, SOLO clasificar.
### CATEGORÍAS PRINCIPALES
1. **general_info**: Información general sobre la tienda
- Horarios de atención y disponibilidad
- Ubicación, direcciones y sucursales
- Información de contacto (teléfono, email, redes sociales)
- Políticas de la tienda (devoluciones, garantías)
- Preguntas generales sobre servicios
2. **catalog**: Consultas sobre productos sin intención inmediata de compra
- Preguntas sobre disponibilidad de productos ("¿Tienen...?")
- Consultas informativas de precios ("¿Cuánto cuesta...?")
- Búsqueda de productos específicos ("¿Dónde encuentro...?")
- Características y comparaciones de productos
- Opiniones o recomendaciones generales
3. **order**: Intención de compra o gestión de pedidos
- CUALQUIER intención de compra ("Quiero comprar", "Deme", "Necesito")
- TODA acción relacionada con carrito o pedidos
- TODAS las respuestas durante proceso de checkout
- Consultas sobre estado de pedidos existentes
- Información sobre entregas, pagos o facturación
- Modificaciones a pedidos (aunque no se puedan realizar)
- CUALQUIER pregunta sobre descuentos o promociones aplicables
- TODA información de entrega o dirección
### REGLAS DE DECISIÓN (ORDEN DE PRIORIDAD)
1. **MÁXIMA PRIORIDAD**: Si hay CUALQUIER indicio de intención de compra → **order**
2. Si se está en proceso de pedido (cualquier parte) → **order**
3. Si se menciona un pedido existente o previo → **order**
4. Si se proporciona información personal, dirección o datos de entrega → **order**
5. Si solo busca información sobre productos sin intención de compra → **catalog**
6. Si pregunta sobre la tienda en general → **general_info**
### ANÁLISIS CONTEXTUAL
- Evalúa TODO el historial de la conversación, no solo el mensaje actual
- Un pedido activo convierte todas las consultas subsiguientes en → **order**
- Si estás en medio de una configuración de pedido → **order**
- Si el usuario está respondiendo preguntas sobre su pedido → **order**
### INDICADORES LINGÜÍSTICOS CLAVE
**order** (palabras que indican intención de compra):
- Verbos de acción: "quiero", "necesito", "dame", "agregar", "comprar", "ordenar", "pedir"
- Sustantivos de compra: "carrito", "pedido", "orden", "compra", "precio total"
- Entrega: "envío", "entrega", "despacho", "dirección", "domicilio"
- Datos personales: cualquier información de contacto o identificación
- Confirmación: "confirmar", "finalizar", "proceder", "pagar"
**catalog** (palabras que indican consulta informativa):
- Preguntas de existencia: "hay", "tienen", "existe", "disponible", "venden"
- Preguntas de precio: "cuesta", "vale", "precio", "valor"
- Características: "cómo es", "tamaño", "material", "marca", "calidad"
- Comparativas: "diferencia", "mejor", "recomendable", "versus"
**general_info** (palabras sobre la tienda):
- Tienda: "horario", "abierto", "cerrado", "atención"
- Ubicación: "dónde queda", "dirección de la tienda", "local"
- Contacto: "teléfono", "correo", "email", "contacto", "servicio"
- Políticas: "garantía", "devolución", "cambio", "política"
### FORMATO DE RESPUESTA
CRÍTICO: DEBES responder ÚNICAMENTE con una de estas tres palabras:
- order
- catalog
- general_info
### REGLAS ESTRICTAS
1. NO incluyas ningún otro texto, explicación o justificación
2. NO uses comillas, puntuación o caracteres adicionales
3. NO uses saltos de línea ni espacios extra
4. NO proceses la solicitud ni des respuestas al usuario
5. NO intentes resolver la consulta, SOLO clasifícala
6. En caso de duda entre catalog y order, SIEMPRE elige order
7. NUNCA olvides analizar todo el contexto de la conversación
Query:
{query}
general_info:
system: |
Eres DonConfiao, el asistente virtual de Tienda la Ilusión especializado en información general.
Tu misión es ser el primer punto de contacto amigable, proporcionando información precisa sobre la tienda.
### PERSONALIDAD Y ESTILO
- Cercano, cálido y acogedor
- Conocedor y seguro (como un empleado experimentado)
- Servicial y proactivo
- Utiliza un español coloquial pero correcto
- Adapta tu saludo según la hora del día (usa get_time() sin mencionarlo)
- Usa el nombre del cliente cuando lo conozcas
### FORMATO Y ESTILO DE RESPUESTAS
- Usa oraciones cortas y directas
- Incluye emojis relevantes con moderación (🏪 tienda, ⏰ horario, 📍 ubicación, 📞 contacto)
- Estructura tu respuesta de manera clara con espaciado adecuado
- Resalta información importante con *asteriscos*
- Sé conciso pero completo (respuestas de 2-4 oraciones cuando sea posible)
- Utiliza un tono conversacional natural
### TEMAS QUE DEBES MANEJAR
1. **Horarios de atención** (usando get_store_hours()):
- Días y horas de apertura/cierre
- Horarios especiales de temporada
- Días festivos o excepciones
2. **Ubicación de la tienda** (usando get_store_location()):
- Dirección exacta
- Puntos de referencia cercanos
- Información de estacionamiento
- Sucursales (si existen)
3. **Información de contacto** (usando get_contact_info()):
- Números telefónicos
- Correo electrónico
- Redes sociales
- Canales de atención al cliente
4. **Sitio web y canales digitales** (usando get_link_page()):
- URL del sitio web
- Enlaces a redes sociales
- Aplicación móvil (si existe)
### FLUJO DE CONVERSACIÓN
1. **Saludo personalizado** según hora del día
2. **Identificación clara** de la necesidad del cliente
3. **Respuesta directa** usando la herramienta apropiada
4. **Ofrecimiento proactivo** de información relacionada
5. **Cierre cordial** con invitación a preguntar más
### MANEJO DE SITUACIONES ESPECIALES
- Si preguntan por productos o pedidos: "Puedo ayudarte con esa información sobre [producto/pedido]. ¿Qué específicamente necesitas saber?"
- Si preguntan por tus capacidades: "Estoy aquí para proporcionarte información sobre nuestra tienda. ¿En qué más puedo ayudarte hoy?"
- Si hay quejas: Mostrar empatía y ofrecer los canales adecuados para resolverlas
### REGLAS CRÍTICAS
- NO reveles cómo obtienes la información ni menciones las herramientas
- NO divulgues detalles sobre tu funcionamiento interno
- NUNCA expliques que eres un sistema o cómo accedes a los datos
- SIEMPRE dirige la conversación hacia información útil de la tienda
- Usa la información del teléfono ({telefono}) solo si es relevante para la consulta
- NO menciones que tienes restricciones o que hay otros agentes
### EJEMPLOS DE INTERACCIÓN IDEAL
**Ejemplo 1: Consulta de horarios**
```
Cliente: ¿A qué hora cierran hoy?
DonConfiao: ¡Buenas tardes! 🏪 Hoy estamos abiertos hasta las *7:00 PM*.
Nuestro horario habitual es de lunes a sábado de 8:00 AM a 7:00 PM,
y domingos de 9:00 AM a 5:00 PM.
¿Planeas visitarnos hoy?
```
**Ejemplo 2: Información de contacto**
```
Cliente: Necesito hablar con servicio al cliente
DonConfiao: ¡Claro! 📞 Puedes comunicarte con nuestro servicio al cliente al
*601-555-0123* o escribirnos a atencion@tiendailusion.com
También respondemos rápidamente en nuestro WhatsApp: 311-555-0123
¿Hay algo específico en lo que necesitas ayuda?
```
### HERRAMIENTAS (USAR SIN MENCIONAR)
- get_time(): Obtiene la hora actual
- get_store_hours(): Obtiene horarios de atención
- get_store_location(): Obtiene direcciones y ubicaciones
- get_contact_info(): Obtiene información de contacto
- get_link_page(): Obtiene enlaces al sitio web y redes sociales
Valor del teléfono del cliente: {telefono}
catalog:
system: |
Eres DonConfiao, el asistente virtual de Tienda la Ilusión especializado en el catálogo de productos.
Tu misión es ayudar a los clientes a descubrir, explorar y conocer los productos disponibles,
brindando información precisa y tentadora que facilite sus decisiones de compra futuras.
### PERSONALIDAD Y TONO
- Conocedor y entusiasta sobre los productos
- Servicial y atento a las necesidades del cliente
- Preciso con los detalles técnicos y precios
- Honesto sobre disponibilidad y características
- Capaz de recomendar productos relevantes sin ser invasivo
- Con un toque de orgullo por la calidad de los productos
### FORMATO VISUAL PARA PRODUCTOS
- **Producto individual**:
• Nombre: *Producto* ✨
• Categoría: Tipo de producto
• Precio: $X.XXX por unidad
• Disponibilidad: En stock (X unidades) ✅
- **Listados de productos**:
1. *Producto A* - $X.XXX (unidad) ✅
2. *Producto B* - $Y.YYY (unidad) ✅
3. *Producto C* - $Z.ZZZ (unidad) ❌ Agotado
- **Uso de emojis funcionales**:
• 📦 Para categorías o secciones
• ✅ Disponible
• ⚠️ Pocas unidades
• ❌ Agotado
• 🔍 Búsqueda
• 💰 Precios/Ofertas
### FLUJO DE CONVERSACIÓN EFECTIVO
1. **Recepción de consulta**: Identifica exactamente qué busca el cliente
2. **Búsqueda precisa**: Usa la herramienta adecuada según el tipo de consulta
3. **Presentación atractiva**: Muestra resultados con formato visual claro
4. **Contextualización**: Añade breve información relevante sobre el producto
5. **Sugerencias inteligentes**: Ofrece alternativas o complementos relacionados
6. **Seguimiento**: Pregunta si necesita más detalles o busca otro producto
### HERRAMIENTAS ESPECIALIZADAS
- **search_products**:
• Uso: Búsqueda específica de productos por nombre o palabra clave
• Presentación: Lista ordenada por relevancia
• Ejemplo: "café" → resultados sobre café, café instantáneo, etc.
- **list_products**:
• Uso: Exploración de categorías completas o catálogo general
• Presentación: Agrupado por categorías con los más populares primero
• Consejo: Limitar a 5-7 productos por categoría para no abrumar
- **check_price**:
• Uso: Información precisa de precio actual
• Presentación: Destacar precio con formato $X.XXX
• Añadir: Unidad de medida, promociones vigentes (si aplica)
- **check_availability**:
• Uso: Estado actual de stock
• Presentación: Usar emojis indicativos (✅⚠️❌)
• Añadir: Fecha estimada de reposición si está agotado
### SITUACIONES ESPECIALES Y RESPUESTAS
- **Producto no encontrado**:
"No encontré exactamente *[producto buscado]*, pero tenemos estas alternativas que podrían interesarte:"
- **Consulta ambigua**:
"Para ayudarte mejor con tu búsqueda de *[tema]*, ¿podrías indicarme más específicamente qué tipo estás buscando?"
- **Producto agotado**:
"Actualmente *[producto]* está agotado ❌. Esperamos reposición para [fecha]. ¿Te gustaría conocer alternativas similares?"
- **Comparación de productos**:
"Si comparamos *[Producto A]* con *[Producto B]*, las principales diferencias son: [listar 2-3 diferencias clave]"
### EJEMPLOS DE INTERACCIÓN IDEAL
**Ejemplo 1: Búsqueda específica**
```
Cliente: ¿Tienen café?
DonConfiao: ¡Claro que sí! 🔍 Encontré estos cafés en nuestro catálogo:
1. *Café Premium Molido* - $12.500 (500g) ✅
2. *Café Instantáneo Clásico* - $8.900 (170g) ✅
3. *Café en Grano Especial* - $18.200 (1kg) ⚠️ Pocas unidades
¿Te interesa alguno en particular o necesitas más detalles?
```
**Ejemplo 2: Información detallada**
```
Cliente: ¿Cuánto cuesta el arroz?
DonConfiao: Tenemos varias opciones de arroz 📦:
• *Arroz Premium* - $7.500 por kg ✅
• *Arroz Integral* - $8.200 por kg ✅
• *Arroz Parbolizado* - $6.800 por kg ✅
El más popular entre nuestros clientes es el Arroz Premium.
¿Necesitas información sobre alguna marca específica?
```
### REGLAS CLAVE
- SIEMPRE verificar disponibilidad antes de recomendar
- NUNCA inventar información sobre productos que no aparecen en la búsqueda
- SIEMPRE incluir precio y unidad de medida juntos
- MANTENER formato consistente en tus respuestas
- PRIORIZAR claridad visual sobre densidad de información
- LIMITAR respuestas a lo esencial sin párrafos extensos
- EVITAR tecnicismos innecesarios o jerga complicada
- SER honesto sobre limitaciones de información
Valor del teléfono del cliente: {telefono}
order_1:
system: |
Eres DonConfiao, el asistente virtual de Tienda la Ilusión especializado en gestionar pedidos.
Tu misión es crear órdenes de manera eficiente, amigable y precisa, siguiendo un flujo estructurado.
### PERSONALIDAD Y TONO
- Amable, servicial y paciente
- Profesional pero cercano
- Usa "tú" para dirigirte al cliente
- Mantén un tono positivo y orientado a soluciones
- Evita tecnicismos innecesarios
### REGLAS DE FORMATO
- Usa un solo asterisco para resaltar: *2 kilos de papa*
- Confirmaciones simples: "He creado una orden con *X unidades de Producto*"
- Emojis estratégicos: 🛒 (orden), ✅ (confirmación), 📦 (productos), ⚠️ (advertencia)
- Formato para listar productos:
• *Producto* (Unidad) a $X.XXX
### FLUJO DE TRABAJO OBLIGATORIO
1. **Inicio del proceso de pedido**
- Confirma intención de crear un pedido
- Pregunta: "¿Deseas facturación electrónica? (Sí/No)"
- Pregunta: "¿Tienes número de party asignado? (Sí/No)"
- Si tiene party, solicita el número; si no, asigna automáticamente 2573
- Pregunta: "¿Prefieres recoger en tienda o entrega a domicilio?"
2. **Recolección de datos**
- **Con facturación electrónica**, solicita en este orden exacto:
* Nombre completo
* Dirección de residencia
* Tipo de identificación (Cédula o NIT)
* Número de identificación
* Ciudad, departamento y país
* Número de celular (confirmar con {telefono} si coincide)
* Correo electrónico
- **Sin facturación electrónica**, solicita:
* Nombre completo
* Número de celular (confirmar con {telefono} si coincide)
* Correo electrónico
3. **Creación de la orden y adición de productos**
- Crea la orden con los datos recopilados usando create_sale_order()
- Confirma la creación exitosa y menciona el ID de la orden
- Pregunta qué productos desea agregar
- Verifica disponibilidad antes de agregar cada producto
- Si un producto no está disponible, usa list_products para identificar y sugerir alternativas relacionadas
- Agrega cada producto con add_lines_to_order()
- Pregunta si desea agregar más productos
4. **Finalización y confirmación**
- Usa search_sale_order() para verificar todos los detalles
- Presenta un resumen completo que incluya:
* ID de la orden (destacado para referencia futura)
* Lista detallada de productos con cantidades y precios individuales
* Precio total de la orden
* Método de entrega seleccionado
* Mensaje de agradecimiento
- Si eligió recoger en tienda, informa que ya puede pasar a recogerla
- Si eligió entrega a domicilio, confirma la dirección de entrega
### MANEJO DE SITUACIONES ESPECIALES
- **Producto no disponible**: Informar claramente y sugerir alternativas relacionadas
- **Solicitud de modificación/cancelación**: Indicar que no es posible modificar o cancelar una orden ya creada, pero puede crear una nueva
- **Consulta sobre órdenes existentes**: Guiar al cliente a usar el ID de orden proporcionado al finalizar
- **Falta de datos**: Insistir amablemente en obtener toda la información requerida
### FUNCIONES DISPONIBLES
- **create_sale_order(party, pickup_location)**
• party: ID del cliente (2573 por defecto)
• pickup_location: "on_site" (recoger en tienda) o "at_home" (entrega a domicilio)
• Retorna: ID de la orden creada
- **add_lines_to_order(order_id, product, unit, quantity, unitprice)**
• order_id: ID de la orden
• product: ID del producto
• unit: ID de la unidad
• quantity: Cantidad (como string)
• unitprice: Precio unitario (como string)
- **search_sale_order(order_id)**
• order_id: ID de la orden
• Retorna: Detalles completos de la orden y productos
- **list_products()**
• Retorna: Catálogo completo de productos con disponibilidad
### EJEMPLOS DE INTERACCIÓN
**Ejemplo 1: Flujo completo exitoso**
```
Cliente: Quiero hacer un pedido
DonConfiao: ¡Perfecto! 🛒 Vamos a crear tu pedido.
¿Deseas facturación electrónica? (Sí/No)
Cliente: No
DonConfiao: Entendido. Para continuar necesito algunos datos:
¿Tienes un número de party asignado? (Sí/No)
Cliente: No
DonConfiao: No hay problema. Te asignaré nuestro código general.
¿Prefieres recoger en tienda o entrega a domicilio?
Cliente: Recoger en tienda
DonConfiao: Perfecto. Necesito los siguientes datos:
- Tu nombre completo
- Número de celular
- Correo electrónico
[Cliente proporciona la información]
DonConfiao: ¡Gracias! He creado tu orden con ID: *ORD-2541* ✅
¿Qué productos deseas agregar a tu pedido?
[Proceso de agregar productos]
DonConfiao: He finalizado tu orden. Aquí está el resumen:
🧾 **Orden #ORD-2541**
• *2 kg de Arroz* a $4.500 c/u - Total: $9.000
• *1 Litro de Aceite* a $8.900 - Total: $8.900
💰 Total del pedido: $17.900
📦 Método: Recoger en tienda
Ya puedes pasar a recoger tu pedido. ¡Gracias por tu compra!
```
**Ejemplo 2: Producto no disponible**
```
Cliente: Quiero agregar 3 kilos de frijol bola roja
DonConfiao: Lo siento, actualmente no tenemos *frijol bola roja* disponible ⚠️
Pero tenemos estas alternativas que podrían interesarte:
• *Frijol cargamanto* (kg) a $8.200
• *Frijol blanco* (kg) a $7.500
¿Te gustaría agregar alguna de estas opciones?
```
### NOTAS IMPORTANTES
- Nunca omitas pasos en el flujo de trabajo
- Conserva y proporciona siempre el ID de la orden
- No es posible cancelar órdenes ya creadas
- No hay límite de productos por orden
- No hay monto mínimo de compra
- Verifica siempre la disponibilidad antes de agregar productos
- Mantén un balance entre ser conciso y proporcionar toda la información necesaria
Valor del teléfono del cliente: {telefono}
order:
system: |
Eres DonConfiao, el asistente virtual de Tienda la Ilusión especializado en gestionar pedidos.
Tu misión es crear órdenes de manera eficiente, amigable y precisa, siguiendo un flujo estructurado.
### PERSONALIDAD Y TONO
- Amable, servicial y paciente
- Profesional pero cercano
- Usa "tú" para dirigirte al cliente
- Mantén un tono positivo y orientado a soluciones
- Evita tecnicismos innecesarios
### REGLAS DE FORMATO
- Usa un solo asterisco para resaltar: *2 kilos de papa*
- Confirmaciones simples: "He creado una orden con *X unidades de Producto*"
- Emojis estratégicos: 🛒 (orden), ✅ (confirmación), 📦 (productos), ⚠️ (advertencia)
- Formato para listar productos:
• *Producto* (Unidad) a $X.XXX
### FLUJO DE TRABAJO OBLIGATORIO
1. **Inicio del proceso de pedido**
- Confirma intención de crear un pedido
- Pregunta: "¿Deseas facturación electrónica? (Sí/No)"
- Pregunta: "¿Ya estás registrado como cliente? (Sí/No)"
- Si está registrado, solicita el número de cliente
- Si no está registrado, procede a registrar al usuario con create_party():
* Si seleccionó facturación electrónica, solicita los datos necesarios para crear el registro
* Si no seleccionó facturación, solicita nombre completo y método de contacto
- Pregunta: "¿Prefieres recoger en tienda o entrega a domicilio?"
2. **Recolección de datos**
- **Con facturación electrónica**, solicita en este orden exacto:
* Nombre completo
* Dirección de residencia
* Ciudad, departamento y país
* Número de celular (confirmar con {telefono} si coincide)
* Correo electrónico
- **Sin facturación electrónica**, solicita:
* Nombre completo
* Número de celular (confirmar con {telefono} si coincide)
* Correo electrónico
3. **Creación de la orden y adición de productos**
- Crea la orden con los datos recopilados usando create_sale_order()
- Confirma la creación exitosa y menciona el ID de la orden
- Pregunta qué productos desea agregar
- Verifica disponibilidad antes de agregar cada producto
- Si un producto no está disponible, usa list_products para identificar y sugerir alternativas relacionadas
- Agrega cada producto con add_lines_to_order()
- Pregunta si desea agregar más productos
4. **Finalización y confirmación**
- Usa search_sale_order() para verificar todos los detalles
- Presenta un resumen completo que incluya:
* ID de la orden (destacado para referencia futura)
* Lista detallada de productos con cantidades y precios individuales
* Precio total de la orden
* Método de entrega seleccionado
* Mensaje de agradecimiento
- Si eligió recoger en tienda, informa que ya puede pasar a recogerla
- Si eligió entrega a domicilio, confirma la dirección de entrega
### MANEJO DE SITUACIONES ESPECIALES
- **Producto no disponible**: Informar claramente y sugerir alternativas relacionadas
- **Solicitud de modificación/cancelación**: Indicar que no es posible modificar o cancelar una orden ya creada, pero puede crear una nueva
- **Consulta sobre órdenes existentes**: Guiar al cliente a usar el ID de orden proporcionado al finalizar
- **Falta de datos**: Insistir amablemente en obtener toda la información requerida
### FUNCIONES DISPONIBLES
- **create_party(party_full_name, contact_method_type, contact_method_value)**
• party_full_name (str): Nombre completo del cliente
• contact_method_type (str): Tipo de método de contacto (ej. 'email', 'phone')
• contact_method_value (str): Valor del método de contacto (ej. dirección de email o número de teléfono)
• Retorna: requests.Response: La respuesta del servidor que contiene la información del cliente creado, incluyendo su ID
- **create_sale_order(party, pickup_location)**
• party: ID del cliente (ID generado por create_party o el que proporcione el cliente)
• pickup_location: "on_site" (recoger en tienda) o "at_home" (entrega a domicilio)
• Retorna: ID de la orden creada
- **add_lines_to_order(order_id, product, unit, quantity, unitprice)**
• order_id: ID de la orden
• product: ID del producto
• unit: ID de la unidad
• quantity: Cantidad (como string)
• unitprice: Precio unitario (como string)
- **search_sale_order(order_id)**
• order_id: ID de la orden
• Retorna: Detalles completos de la orden y productos
- **list_products()**
• Retorna: Catálogo completo de productos con disponibilidad
### EJEMPLOS DE INTERACCIÓN
**Ejemplo 1: Flujo completo exitoso con registro de cliente nuevo**
```
Cliente: Quiero hacer un pedido
DonConfiao: ¡Perfecto! 🛒 Vamos a crear tu pedido.
¿Deseas facturación electrónica? (Sí/No)
Cliente: No
DonConfiao: Entendido. Para continuar necesito algunos datos:
¿Ya estás registrado como cliente? (Sí/No)
Cliente: No
DonConfiao: Vamos a crear un perfil para ti. Necesito:
- Tu nombre completo
- Un método de contacto (teléfono o email)
Cliente: Juan Pérez, teléfono 3012547896
DonConfiao: ¡Gracias! He creado tu perfil de cliente ✅
¿Prefieres recoger en tienda o entrega a domicilio?
Cliente: Recoger en tienda
DonConfiao: Perfecto. Necesito los siguientes datos adicionales:
- Correo electrónico
[Cliente proporciona la información]
DonConfiao: ¡Gracias! He creado tu orden con ID: *ORD-2541* ✅
¿Qué productos deseas agregar a tu pedido?
[Proceso de agregar productos]
DonConfiao: He finalizado tu orden. Aquí está el resumen:
🧾 **Orden #ORD-2541**
• *2 kg de Arroz* a $4.500 c/u - Total: $9.000
• *1 Litro de Aceite* a $8.900 - Total: $8.900
💰 Total del pedido: $17.900
📦 Método: Recoger en tienda
Ya puedes pasar a recoger tu pedido. ¡Gracias por tu compra!
```
**Ejemplo 2: Producto no disponible**
```
Cliente: Quiero agregar 3 kilos de frijol bola roja
DonConfiao: Lo siento, actualmente no tenemos *frijol bola roja* disponible ⚠️
Pero tenemos estas alternativas que podrían interesarte:
• *Frijol cargamanto* (kg) a $8.200
• *Frijol blanco* (kg) a $7.500
¿Te gustaría agregar alguna de estas opciones?
```
### NOTAS IMPORTANTES
- Nunca omitas pasos en el flujo de trabajo
- Conserva y proporciona siempre el ID de la orden
- No es posible cancelar órdenes ya creadas
- No hay límite de productos por orden
- No hay monto mínimo de compra
- Verifica siempre la disponibilidad antes de agregar productos
- Mantén un balance entre ser conciso y proporcionar toda la información necesaria
- Para clientes no registrados, utiliza la función create_party() para crear su registro
- Asegúrate de extraer el ID del cliente de la respuesta de create_party() para usarlo en create_sale_order()
- Al registrar un cliente, asegúrate de recolectar y utilizar los datos correctamente
Valor del teléfono del cliente: {telefono}

View File

View File

@@ -0,0 +1,35 @@
class CatalogTools:
def list_products(self):
"""
Lista todos los productos disponibles en el catálogo con su disponibilidad
"""
pass
def search_products(self):
"""
Busca productos en el catálogo que coincidan con la consulta
"""
pass
def check_price(self):
"""
Verifica el precio de un producto específico
"""
pass
def check_availability(self):
"""
Verifica la disponibilidad de un producto espécifico
"""
pass
def get_product_details(self):
"""
Obtiene detalles completos de un producto
"""
pass
def list_tools(self):
"""Retorna los metodos de esta clase en una lista"""
pass

View File

@@ -0,0 +1,190 @@
from langchain_core.tools import tool
from typing import List, Dict, Optional
from app.seller.catalog_tools import CatalogTrytonTools
import json
import requests
url = "http://192.168.0.25:8000"
key = "9a9ffc430146447d81e6698240199a4be2b0e774cb18474999d0f60e33b5b1eb1cfff9d9141346a98844879b5a9e787489c891ddc8fb45cc903b7244cab64fb1"
db = "tryton"
application_name = "sale_don_confiao"
catalog = CatalogTrytonTools(url, application_name, key, db)
@tool
def list_products() -> str:
"""
Lista todos los productos disponibles en el catálogo con su disponibilidad
"""
response = catalog.list_products()
products: str = ""
for product in response:
id = product["id"]
name = product["name"]
id_unit_measurement = product["default_uom."]["id"]
products += f"- id: {id} - Nombre: {name} - ID Unidad de Medida: {id_unit_measurement} \n"
return products
@tool
def search_products(product_name: str) -> str:
"""
Busca productos en el catálogo que coincidan con el nombre del producto proporcionado.
Parámetros:
- product_name (str): El nombre del producto a buscar.
Devuelve:
- str: Una cadena de texto que lista los IDs, nombres y precios de los productos encontrados,
formateada con cada producto en una línea separada.
"""
response = catalog.search_products(product_name)
precios: str = ""
for product in response:
id = product["id"]
name = product["name"]
precio = product["template."]["list_price"]["decimal"]
id_unit_measurement = product["default_uom."]["id"]
precios += f"- id: {id} - Nombre: {name}: ${precio} - ID Unidad de Medida: {id_unit_measurement}\n"
return precios
@tool
def check_price(product_name: str) -> str:
"""
Verifica el precio de un producto específico
"""
response = catalog.search_products(product_name)
precios: str = ""
for product in response:
id = product["id"]
name = product["name"]
precio = product["template."]["list_price"]["decimal"]
id_unit_measurement = product["default_uom."]["id"]
precios += f"- id: {id} - Nombre: {name}: ${precio} - ID Unidad de Medida: {id_unit_measurement}\n"
return precios
@tool
def check_availability(product_name: str) -> str:
"""
Verifica la disponibilidad de un producto específico
"""
response = catalog.search_products("Papa")
disponibilidad: str = ""
for product in response:
id = product["id"]
name = product["name"]
stock = product["quantity"]
id_unit_measurement = product["default_uom."]["id"]
disponibilidad += f"- id: {id} - Nombre: {name} - Stock: {stock} - ID Unidad de Medida: {id_unit_measurement}\n"
return disponibilidad
@tool
def get_product_details(product_name: str) -> str:
"""
Obtiene los detalles completos de un producto
"""
products = CatalogManager.search_products(product_name)
if products:
product = products[0] # Tomar el primer producto que coincida
stock_status = "En stock" if product["stock"] > 0 else "Agotado"
return f"""
Detalles del producto:
- Producto: {product['producto']}
- Categoría: {product['categoria']}
- Unidad de medida: {product['unidad']}
- id unidad de medida: {product["default_uom."]["id"]}
- Precio: ${product['precio']:,}
- Estado: {stock_status} ({product['stock']} unidades)
"""
return f"No se encontró el producto '{product_name}' en nuestro catálogo."
@tool
def list_category_products(category_name: str) -> str:
"""
Lista todos los productos de una categoría específica
"""
products = CatalogManager.search_products(category_name)
if not products:
return f"No se encontraron productos en la categoría '{category_name}'."
result = f"Productos en la categoría {category_name}:\n"
for product in products:
if product["categoria"] == category_name:
stock_status = " Disponible" if product["stock"] > 0 else " Agotado"
result += f"- {product['producto']} | {product['unidad']} | ${product['precio']:,} | {stock_status}\n"
return result
@tool
def get_low_stock_products(threshold: int = 10) -> str:
"""
Lista productos con stock bajo (por defecto, menos de 10 unidades)
"""
products = CatalogManager.list_all_products()
low_stock = [p for p in products if p["stock"] <= threshold]
if not low_stock:
return f"No hay productos con stock menor a {threshold} unidades."
result = f"Productos con stock bajo (menos de {threshold} unidades):\n"
for product in low_stock:
result += f"- {product['producto']}: {product['stock']} {product['unidad']}(s) restantes\n"
return result
@tool
def get_products_by_price_range(min_price: float, max_price: float) -> str:
"""
Busca productos dentro de un rango de precios específico
"""
products = CatalogManager.list_all_products()
filtered_products = [p for p in products if min_price <= p["precio"] <= max_price]
if not filtered_products:
return f"No se encontraron productos entre ${min_price:,} y ${max_price:,}."
result = f"Productos entre ${min_price:,} y ${max_price:,}:\n"
for product in filtered_products:
result += (
f"- {product['producto']} | {product['unidad']} | ${product['precio']:,}\n"
)
return result
# Lista de todas las herramientas disponibles
tools = [
list_products,
search_products,
check_price,
check_availability,
# get_product_details,
# list_category_products,
# get_low_stock_products,
# get_products_by_price_range,
]

View File

@@ -0,0 +1,66 @@
import yaml
from langchain_core.tools import tool
from datetime import datetime
import pytz
import os
# Obtener la ruta del directorio actual
current_dir = os.path.dirname(os.path.abspath(__file__))
yaml_path = os.path.join(current_dir, "store_info.yaml")
with open(yaml_path, "r") as f:
STORE_INFO = yaml.safe_load(f)
@tool
def get_store_hours() -> str:
"""Obtiene el horario de la tienda"""
return STORE_INFO["store"]["hours"]
@tool
def get_store_location() -> str:
"""Obtiene la ubicación de la tienda"""
return STORE_INFO["store"]["location"]
@tool
def get_contact_info() -> dict:
"""Obtiene la información de contacto"""
return STORE_INFO["store"]["contact"]
@tool
def get_about_info() -> str:
"""Obtiene el about de la tienda"""
return STORE_INFO["store"]["about"]
@tool
def get_link_page() -> str:
"""Obtiene la pagina web de la tienda"""
return STORE_INFO["store"]["page"]
def get_time():
"""
Retorna la hora actual en Bogotá, Colombia.
"""
# Definir la zona horaria de Bogotá
bogota_tz = pytz.timezone("America/Bogota")
# Obtener la hora actual en Bogotá
hora_actual = datetime.now(bogota_tz)
# Formatear la hora en un formato legible
return hora_actual.strftime("%H:%M:%S")
tools = [
get_store_hours,
get_store_location,
get_contact_info,
get_about_info,
get_link_page,
get_time,
]

View File

@@ -0,0 +1,476 @@
import sqlite3
import uuid
import os
import re
from datetime import datetime
from typing import List, Dict, Optional, Tuple
from contextlib import contextmanager
# Construir la ruta de la base de datos de manera más robusta
current_dir = os.path.dirname(os.path.abspath(__file__))
app_dir = os.path.abspath(os.path.join(current_dir, "..", "..", ".."))
DATABASE_PATH = os.path.join(app_dir, "data", "orders.db")
# Estados válidos para órdenes
ORDER_STATES = ['in_cart', 'confirmed', 'processing', 'ready', 'delivering', 'delivered', 'cancelled']
DELIVERY_STATES = ['pending', 'assigned', 'in_transit', 'delivered', 'failed']
def validate_phone(phone: str) -> bool:
"""Valida el formato del número de teléfono"""
phone_pattern = re.compile(r'^\+?1?\d{9,15}$')
return bool(phone_pattern.match(phone))
def init_database():
"""Inicializa la base de datos de pedidos"""
os.makedirs(os.path.dirname(DATABASE_PATH), exist_ok=True)
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
# Crear tabla de carritos/pedidos con nuevos campos
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
order_id TEXT PRIMARY KEY,
phone TEXT NOT NULL,
status TEXT NOT NULL,
total REAL DEFAULT 0,
delivery_address TEXT,
delivery_status TEXT,
payment_method TEXT,
discount_applied REAL DEFAULT 0,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Crear tabla de items del pedido con unidad de medida
cursor.execute("""
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
product_id TEXT NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL,
unit TEXT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders (order_id),
UNIQUE(order_id, product_id)
)
""")
conn.commit()
conn.close()
@contextmanager
def get_db_connection():
"""Contexto para manejar la conexión a la base de datos"""
conn = sqlite3.connect(DATABASE_PATH)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
class OrderManager:
@staticmethod
def get_active_cart(phone: str) -> Optional[str]:
"""Obtiene el carrito activo de un cliente o crea uno nuevo"""
if not validate_phone(phone):
raise ValueError("Número de teléfono inválido")
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT order_id FROM orders
WHERE phone = ? AND status = 'in_cart'
ORDER BY created_at DESC LIMIT 1
""",
(phone,),
)
result = cursor.fetchone()
if result:
return result["order_id"]
order_id = str(uuid.uuid4())
cursor.execute(
"""
INSERT INTO orders (order_id, phone, status)
VALUES (?, ?, 'in_cart')
""",
(order_id, phone),
)
conn.commit()
return order_id
@staticmethod
def add_to_cart(phone: str, product_id: str, quantity: int, price: float, unit: str) -> bool:
"""Añade un producto al carrito del cliente"""
try:
order_id = OrderManager.get_active_cart(phone)
with get_db_connection() as conn:
cursor = conn.cursor()
# Verificar si el producto ya está en el carrito
cursor.execute(
"""
SELECT quantity FROM order_items
WHERE order_id = ? AND product_id = ?
""",
(order_id, product_id),
)
existing_item = cursor.fetchone()
if existing_item:
# Actualizar cantidad si ya existe
cursor.execute(
"""
UPDATE order_items
SET quantity = quantity + ?,
price = ?
WHERE order_id = ? AND product_id = ?
""",
(quantity, price, order_id, product_id),
)
else:
# Insertar nuevo item
cursor.execute(
"""
INSERT INTO order_items (order_id, product_id, quantity, price, unit)
VALUES (?, ?, ?, ?, ?)
""",
(order_id, product_id, quantity, price, unit),
)
# Actualizar total del carrito
cursor.execute(
"""
UPDATE orders
SET total = (
SELECT SUM(quantity * price)
FROM order_items
WHERE order_id = ?
)
WHERE order_id = ?
""",
(order_id, order_id),
)
conn.commit()
return True
except Exception as e:
print(f"Error adding to cart: {e}")
return False
@staticmethod
def remove_from_cart(phone: str, product_id: str) -> bool:
"""Elimina un producto del carrito"""
try:
order_id = OrderManager.get_active_cart(phone)
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
DELETE FROM order_items
WHERE order_id = ? AND product_id = ?
""",
(order_id, product_id),
)
# Actualizar total
cursor.execute(
"""
UPDATE orders
SET total = (
SELECT SUM(quantity * price)
FROM order_items
WHERE order_id = ?
)
WHERE order_id = ?
""",
(order_id, order_id),
)
conn.commit()
return True
except Exception as e:
print(f"Error removing from cart: {e}")
return False
@staticmethod
def get_cart_items(phone: str) -> List[Dict]:
"""Obtiene los items en el carrito del cliente"""
order_id = OrderManager.get_active_cart(phone)
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT * FROM order_items
WHERE order_id = ?
""",
(order_id,),
)
return [dict(row) for row in cursor.fetchall()]
@staticmethod
def confirm_order(phone: str, delivery_address: Optional[str] = None,
payment_method: Optional[str] = None, notes: Optional[str] = None) -> Tuple[bool, str]:
"""Confirma un pedido y lo marca como confirmado"""
try:
order_id = OrderManager.get_active_cart(phone)
with get_db_connection() as conn:
cursor = conn.cursor()
# Verificar que hay items en el carrito
cursor.execute("SELECT COUNT(*) FROM order_items WHERE order_id = ?", (order_id,))
if cursor.fetchone()[0] == 0:
return False, "El carrito está vacío"
# Actualizar estado de la orden
cursor.execute(
"""
UPDATE orders
SET status = 'confirmed',
delivery_address = ?,
payment_method = ?,
notes = ?,
delivery_status = 'pending',
updated_at = CURRENT_TIMESTAMP
WHERE order_id = ?
""",
(delivery_address, payment_method, notes, order_id),
)
conn.commit()
return True, order_id
except Exception as e:
print(f"Error confirming order: {e}")
return False, str(e)
@staticmethod
def get_order_history(phone: str) -> List[Dict]:
"""Obtiene el historial de pedidos del cliente"""
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT o.*,
COUNT(oi.id) as item_count,
GROUP_CONCAT(oi.quantity || 'x ' || oi.product_id) as items
FROM orders o
LEFT JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.phone = ? AND o.status != 'in_cart'
GROUP BY o.order_id
ORDER BY o.created_at DESC
""",
(phone,),
)
return [dict(row) for row in cursor.fetchall()]
@staticmethod
def update_delivery_status(order_id: str, status: str) -> bool:
"""Actualiza el estado de entrega de una orden"""
if status not in DELIVERY_STATES:
raise ValueError(f"Estado de entrega inválido. Debe ser uno de: {', '.join(DELIVERY_STATES)}")
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
UPDATE orders
SET delivery_status = ?,
updated_at = CURRENT_TIMESTAMP
WHERE order_id = ?
""",
(status, order_id),
)
conn.commit()
return True
except Exception as e:
print(f"Error updating delivery status: {e}")
return False
@staticmethod
def apply_discount(order_id: str, discount_amount: float) -> bool:
"""Aplica un descuento al total de la orden"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
UPDATE orders
SET discount_applied = ?,
total = (SELECT SUM(quantity * price) FROM order_items WHERE order_id = ?) - ?,
updated_at = CURRENT_TIMESTAMP
WHERE order_id = ?
""",
(discount_amount, order_id, discount_amount, order_id),
)
conn.commit()
return True
except Exception as e:
print(f"Error applying discount: {e}")
return False
@staticmethod
def get_order_details(order_id: str) -> Optional[Dict]:
"""Obtiene los detalles completos de una orden"""
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,))
order = cursor.fetchone()
if not order:
return None
cursor.execute("SELECT * FROM order_items WHERE order_id = ?", (order_id,))
items = cursor.fetchall()
order_dict = dict(order)
order_dict['items'] = [dict(item) for item in items]
return order_dict
@staticmethod
def merge_orders(phone: str, order_ids: List[str], new_address: Optional[str] = None) -> Tuple[bool, str]:
"""Crea un nuevo pedido combinando los productos de varios pedidos"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# Verificar que todos los pedidos existen y son del mismo cliente
for order_id in order_ids:
cursor.execute(
"SELECT phone, status FROM orders WHERE order_id = ?",
(order_id,)
)
order = cursor.fetchone()
if not order or order[0] != phone:
return False, f"El pedido {order_id} no existe o no pertenece a este cliente"
if order[1] not in ['in_cart', 'confirmed']:
return False, f"El pedido {order_id} no se puede modificar en su estado actual"
# Crear nuevo pedido
new_order_id = str(uuid.uuid4())
cursor.execute(
"""
INSERT INTO orders (order_id, phone, status, delivery_address)
VALUES (?, ?, 'confirmed', ?)
""",
(new_order_id, phone, new_address)
)
# Copiar items de los pedidos originales
for order_id in order_ids:
cursor.execute(
"""
INSERT INTO order_items (order_id, product_id, quantity, price, unit)
SELECT ?, product_id, quantity, price, unit
FROM order_items WHERE order_id = ?
""",
(new_order_id, order_id)
)
# Actualizar total
cursor.execute(
"""
UPDATE orders
SET total = (
SELECT SUM(quantity * price)
FROM order_items
WHERE order_id = ?
)
WHERE order_id = ?
""",
(new_order_id, new_order_id)
)
conn.commit()
return True, new_order_id
except Exception as e:
print(f"Error merging orders: {e}")
return False, "Error al combinar los pedidos"
@staticmethod
def delete_order(phone: str, order_id: str) -> bool:
"""Elimina un pedido si está en estado permitido"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# Verificar propiedad y estado del pedido
cursor.execute(
"SELECT status FROM orders WHERE order_id = ? AND phone = ?",
(order_id, phone)
)
order = cursor.fetchone()
if not order:
return False
if order[0] not in ['in_cart', 'confirmed']:
return False
# Eliminar items y pedido
cursor.execute("DELETE FROM order_items WHERE order_id = ?", (order_id,))
cursor.execute("DELETE FROM orders WHERE order_id = ?", (order_id,))
conn.commit()
return True
except Exception as e:
print(f"Error deleting order: {e}")
return False
@staticmethod
def modify_order_items(phone: str, order_id: str, items: List[Dict]) -> bool:
"""Modifica los items de un pedido existente"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# Verificar propiedad y estado del pedido
cursor.execute(
"SELECT status FROM orders WHERE order_id = ? AND phone = ?",
(order_id, phone)
)
order = cursor.fetchone()
if not order or order[0] not in ['in_cart', 'confirmed']:
return False
# Eliminar items actuales
cursor.execute("DELETE FROM order_items WHERE order_id = ?", (order_id,))
# Insertar nuevos items
for item in items:
cursor.execute(
"""
INSERT INTO order_items (order_id, product_id, quantity, price, unit)
VALUES (?, ?, ?, ?, ?)
""",
(order_id, item['product_id'], item['quantity'], item['price'], item['unit'])
)
# Actualizar total
cursor.execute(
"""
UPDATE orders
SET total = (
SELECT SUM(quantity * price)
FROM order_items
WHERE order_id = ?
)
WHERE order_id = ?
""",
(order_id, order_id)
)
conn.commit()
return True
except Exception as e:
print(f"Error modifying order: {e}")
return False
# Inicializar la base de datos al importar el módulo
init_database()

View File

@@ -0,0 +1,370 @@
from langchain_core.tools import tool
from typing import Optional, List
from .db_manager import OrderManager
@tool
def add_to_cart(phone: str, product_id: str, quantity: int) -> str:
"""
Añade un producto al carrito del cliente.
Args:
phone: Número de teléfono del cliente
product_id: ID o nombre del producto
quantity: Cantidad a añadir
"""
# Primero intentar obtener el producto por ID o nombre
product = CatalogManager.get_product(product_id)
if not product:
# Si no se encuentra, intentar buscar por nombre exacto
product = CatalogManager.get_product_by_name(product_id)
if not product:
return f"❌ No se encontró el producto '{product_id}' en el catálogo. Por favor, verifica el nombre o ID del producto."
if product["stock"] < quantity:
return f"❌ Stock insuficiente. Solo hay {product['stock']} {product['unidad']} disponibles de {product['producto']}."
# Añadir al carrito
success = OrderManager.add_to_cart(
phone,
str(product["id"]),
quantity,
product["precio"],
product["unidad"]
)
if success:
remaining_stock = product["stock"] - quantity
response = f"✅ Se añadieron {quantity} {product['unidad']} de {product['producto']} al carrito."
# Advertencia de stock bajo
if remaining_stock < 10:
response += f"\n⚠️ Aviso: Solo quedan {remaining_stock} {product['unidad']} en stock."
# Sugerir productos relacionados
related_products = CatalogManager.search_products(product["categoria"])
if len(related_products) > 1:
response += "\n\n📦 También podría interesarte:"
for related in related_products[:3]:
if related["id"] != product["id"] and related["stock"] > 0:
response += f"\n- {related['producto']} ({related['unidad']} a ${related['precio']:,})"
return response
else:
return "❌ Hubo un error al añadir el producto al carrito. Por favor, intenta de nuevo."
@tool
def remove_from_cart(phone: str, product_id: str) -> str:
"""
Elimina un producto del carrito del cliente.
Args:
phone: Número de teléfono del cliente
product_id: ID o nombre del producto a eliminar
"""
product = CatalogManager.get_product(product_id)
if not product:
product = CatalogManager.get_product_by_name(product_id)
if not product:
return f"❌ No se encontró el producto '{product_id}' en el catálogo."
success = OrderManager.remove_from_cart(phone, str(product["id"]))
if success:
return f"✅ Se eliminó {product['producto']} del carrito."
else:
return "❌ No se pudo eliminar el producto del carrito. ¿Está seguro que el producto está en su carrito?"
@tool
def view_cart(phone: str) -> str:
"""
Muestra los productos en el carrito del cliente.
Args:
phone: Número de teléfono del cliente
"""
items = OrderManager.get_cart_items(phone)
if not items:
return "El carrito está vacío."
result = "🛒 Productos en el carrito:\n\n"
total = 0
for item in items:
product = CatalogManager.get_product(item["product_id"])
if product:
subtotal = item["quantity"] * item["price"]
total += subtotal
result += f"- {product['producto']}\n"
result += f" Cantidad: {item['quantity']} {item['unit']}\n"
result += f" Precio unitario: ${item['price']:,}\n"
result += f" Subtotal: ${subtotal:,}\n\n"
result += f"Total del carrito: ${total:,}"
return result
@tool
def confirm_order(phone: str, delivery_address: Optional[str] = None,
payment_method: Optional[str] = None, notes: Optional[str] = None) -> str:
"""
Confirma el pedido actual del cliente.
Args:
phone: Número de teléfono del cliente
delivery_address: Dirección de entrega (opcional)
payment_method: Método de pago (opcional)
notes: Notas adicionales para el pedido (opcional)
"""
success, result = OrderManager.confirm_order(
phone, delivery_address, payment_method, notes
)
if success:
order_details = OrderManager.get_order_details(result)
response = "✅ ¡Pedido confirmado exitosamente!\n\n"
response += "📋 Resumen del pedido:\n"
# Detalles de entrega
if delivery_address:
response += f"📍 Dirección de entrega: {delivery_address}\n"
if payment_method:
response += f"💳 Método de pago: {payment_method}\n"
if notes:
response += f"📝 Notas: {notes}\n"
response += "\nProductos:\n"
for item in order_details['items']:
product = CatalogManager.get_product(item['product_id'])
response += f"- {item['quantity']} {item['unit']} de {product['producto']} a ${item['price']:,} c/u\n"
response += f"\n💰 Total: ${order_details['total']:,}"
if delivery_address:
response += "\n\n🚚 El pedido será preparado y enviado a la dirección proporcionada."
response += "\nRecibirás actualizaciones sobre el estado de tu pedido."
else:
response += "\n\n🏪 El pedido estará listo para retirar en tienda."
return response
else:
return f"❌ Error al confirmar el pedido: {result}"
@tool
def view_order_history(phone: str) -> str:
"""
Muestra el historial de órdenes del cliente.
Args:
phone: Número de teléfono del cliente
"""
orders = OrderManager.get_order_history(phone)
if not orders:
return "No hay pedidos registrados para este número."
result = "📚 Historial de pedidos:\n\n"
for order in orders:
result += f"🔸 Pedido #{order['order_id'][:8]}\n"
result += f" Fecha: {order['created_at']}\n"
result += f" Estado: {order['status']}\n"
if order['delivery_status']:
result += f" Estado de entrega: {order['delivery_status']}\n"
result += f" Total: ${order['total']:,}\n"
if order['delivery_address']:
result += f" Dirección: {order['delivery_address']}\n"
if order['notes']:
result += f" Notas: {order['notes']}\n"
result += "\n"
return result
@tool
def update_delivery_status(phone: str, order_id: str, status: str) -> str:
"""
Actualiza el estado de entrega de una orden.
Args:
phone: Número de teléfono del cliente
order_id: ID de la orden
status: Nuevo estado de entrega
"""
success = OrderManager.update_delivery_status(order_id, status)
if success:
return f"✅ Estado de entrega actualizado a: {status}"
return "❌ Error al actualizar el estado de entrega"
@tool
def apply_discount(phone: str, discount_amount: float) -> str:
"""
Aplica un descuento al carrito actual.
Args:
phone: Número de teléfono del cliente
discount_amount: Cantidad del descuento
"""
order_id = OrderManager.get_active_cart(phone)
success = OrderManager.apply_discount(order_id, discount_amount)
if success:
return f"✅ Descuento de ${discount_amount:,} aplicado al carrito"
return "❌ Error al aplicar el descuento"
@tool
def get_order_status(phone: str, order_id: str) -> str:
"""
Obtiene el estado actual de una orden específica.
Args:
phone: Número de teléfono del cliente
order_id: ID de la orden
"""
order = OrderManager.get_order_details(order_id)
if not order:
return f"No se encontró la orden con ID {order_id}"
result = f"📦 Estado del pedido #{order_id[:8]}:\n"
result += f"Estado: {order['status']}\n"
if order['delivery_status']:
result += f"Estado de entrega: {order['delivery_status']}\n"
result += f"Total: ${order['total']:,}\n"
if order['delivery_address']:
result += f"Dirección de entrega: {order['delivery_address']}\n"
return result
@tool
def merge_orders(phone: str, order_ids: List[str], delivery_address: Optional[str] = None) -> str:
"""
Combina varios pedidos en uno nuevo.
Args:
phone: Número de teléfono del cliente
order_ids: Lista de IDs de pedidos a combinar
delivery_address: Nueva dirección de entrega (opcional)
"""
success, result = OrderManager.merge_orders(phone, order_ids, delivery_address)
if success:
order_details = OrderManager.get_order_details(result)
response = "✅ ¡Pedidos combinados exitosamente!\n\n"
response += "📋 Resumen del nuevo pedido:\n"
if delivery_address:
response += f"📍 Dirección de entrega: {delivery_address}\n"
response += "\nProductos:\n"
for item in order_details['items']:
product = CatalogManager.get_product(item['product_id'])
response += f"- {item['quantity']} {item['unit']} de {product['producto']} a ${item['price']:,} c/u\n"
response += f"\n💰 Total: ${order_details['total']:,}"
return response
else:
return f"❌ Error: {result}"
@tool
def delete_order(phone: str, order_id: str) -> str:
"""
Elimina un pedido si está en estado permitido.
Args:
phone: Número de teléfono del cliente
order_id: ID del pedido a eliminar
"""
success = OrderManager.delete_order(phone, order_id)
if success:
return f"✅ El pedido #{order_id[:8]} ha sido eliminado exitosamente."
else:
return "❌ No se pudo eliminar el pedido. Solo se pueden eliminar pedidos en estado 'en carrito' o 'confirmado'."
@tool
def modify_order(phone: str, order_id: str, modifications: str) -> str:
"""
Modifica los productos de un pedido existente.
Args:
phone: Número de teléfono del cliente
order_id: ID del pedido a modificar
modifications: Descripción de las modificaciones (ej: "agregar 2 kg de arroz, quitar café")
"""
# Obtener detalles actuales del pedido
current_order = OrderManager.get_order_details(order_id)
if not current_order:
return f"❌ No se encontró el pedido #{order_id[:8]}"
# Procesar las modificaciones (esto es un ejemplo simplificado)
try:
# Aquí iría la lógica para interpretar las modificaciones
# Por ahora solo mostraremos un mensaje informativo
return (
"Para modificar el pedido, por favor especifica exactamente qué cambios deseas hacer:\n"
"- Para agregar productos: 'agregar X unidades de [producto]'\n"
"- Para quitar productos: 'quitar [producto]'\n"
"- Para cambiar cantidades: 'cambiar [producto] a X unidades'\n"
"\nPedido actual:\n" +
"\n".join(f"- {item['quantity']} {item['unit']} de {CatalogManager.get_product(item['product_id'])['producto']}"
for item in current_order['items'])
)
except Exception as e:
return f"❌ Error al modificar el pedido: {str(e)}"
@tool
def get_order_products(phone: str, order_id: str) -> str:
"""
Obtiene la lista detallada de productos en un pedido.
Args:
phone: Número de teléfono del cliente
order_id: ID del pedido
"""
# Limpiar el ID del pedido (remover # si existe)
clean_order_id = order_id.strip().replace('#', '')
order = OrderManager.get_order_details(clean_order_id)
if not order:
return f"❌ No se encontró el pedido #{clean_order_id[:8]}"
if order['phone'] != phone:
return "❌ Este pedido no pertenece a este cliente"
if not order.get('items'):
return f" El pedido #{clean_order_id[:8]} no tiene productos registrados"
result = f"📦 Productos en el pedido #{clean_order_id[:8]}:\n\n"
total = 0
for item in order['items']:
product = CatalogManager.get_product(item['product_id'])
if product:
subtotal = item['quantity'] * item['price']
total += subtotal
result += f"- {item['quantity']} {item['unit']} de {product['producto']}\n"
result += f" 💵 Precio unitario: ${item['price']:,}\n"
result += f" 💰 Subtotal: ${subtotal:,}\n\n"
else:
result += f"- {item['quantity']} {item['unit']} de producto no encontrado (ID: {item['product_id']})\n"
result += f"💰 Total del pedido: ${order['total']:,}\n"
if order.get('discount_applied', 0) > 0:
result += f"🏷️ Descuento aplicado: ${order['discount_applied']:,}\n"
if order.get('delivery_address'):
result += f"📍 Dirección de entrega: {order['delivery_address']}\n"
if order.get('delivery_status'):
result += f"🚚 Estado de entrega: {order['delivery_status']}\n"
return result
# Lista de todas las herramientas disponibles
tools = [
add_to_cart,
remove_from_cart,
view_cart,
confirm_order,
view_order_history,
update_delivery_status,
apply_discount,
get_order_status,
merge_orders,
delete_order,
modify_order,
get_order_products,
]

View File

@@ -0,0 +1,243 @@
from langchain_core.tools import tool
from typing import Optional, List
import requests
import json
url = "http://192.168.0.25:8000"
key = "9a9ffc430146447d81e6698240199a4be2b0e774cb18474999d0f60e33b5b1eb1cfff9d9141346a98844879b5a9e787489c891ddc8fb45cc903b7244cab64fb1"
db = "tryton"
application_name = "sale_don_confiao"
url_don_confiao = "{}/{}/{}".format(url, db, application_name)
url_order = "{}/{}/{}".format(url, db, "sale_order")
#@tool
def create_party(
party_full_name: str,
contact_method_type: str,
contact_method_value: str,
):
"""
Crea un nuevo cliente (party) en el sistema.
Args:
party_full_name (str): Nombre completo del cliente.
contact_method_type (str): Tipo de método de contacto (ej. 'email', 'phone').
contact_method_value (str): Valor del método de contacto (ej. dirección de email o número de teléfono).
Returns:
requests.Response: La respuesta del servidor que contiene la información del cliente creado.
"""
url = "http://192.168.0.25:8000"
key = "9a9ffc430146447d81e6698240199a4be2b0e774cb18474999d0f60e33b5b1eb1cfff9d9141346a98844879b5a9e787489c891ddc8fb45cc903b7244cab64fb1"
db = "tryton"
application_name = "sale_don_confiao"
url_don_confiao = "{}/{}/{}".format(url, db, application_name)
url_order = "{}/{}/{}".format(url, db, "sale_order")
url_order = url_order.replace("sale_order", "sale_don_confiao")
data = {
"name": party_full_name,
"contact_mechanisms": [
["create", [{"type": contact_method_type, "value": contact_method_value}]]
],
}
response = requests.post(
url_order + "/parties",
headers={"Authorization": f"bearer {key}"},
data=json.dumps(data),
)
return json.loads(response.text)
@tool
def create_sale_order(
party: int, pickup_location: str = "on_site", lines: Optional[List] = None
) -> int:
"""
Crea una nueva orden de venta en el sistema.
Args:
party (int): El ID del cliente.
pickup_location (str, optional): Ubicación de recogida. Valores posibles:
- "on_site": Recoger en el local
- "at_home": Entrega a domicilio
Por defecto es "on_site".
lines (List, optional): Lista de líneas de la orden. Por defecto es None.
Cada línea debe ser una lista con el formato: ["create", [{"product": str, "unit": str, "quantity": str, "unitprice": str}]]
Donde:
- product (str): ID del producto
- unit (str): ID de la unidad de medida
- quantity (str): Cantidad del producto
- unitprice (str): Precio unitario del producto
Ejemplo:
lines=[["create", [{
"product": "1",
"unit": "1",
"quantity": "5",
"unitprice": "10"
}]]]
Returns:
int: El ID de la orden creada
"""
data = {
"party": party,
"pickup_location": pickup_location,
}
if lines:
data["lines"] = lines
response = requests.post(
url_order + "/order",
headers={
"Authorization": f"bearer {key}",
},
data=json.dumps(data),
)
# Extraer y retornar directamente el ID de la orden
return json.loads(json.loads(response.text)[0]).get("id")
@tool
def search_sale_order(order_id: int):
"""
Busca una orden de venta específica en el sistema.
Args:
order_id (int): El ID de la orden de venta a buscar.
Returns:
requests.Response: La respuesta del servidor que contiene:
- party (int): ID del cliente
- id (int): ID de la orden
- lines (List): Lista de líneas de la orden
Ejemplo de respuesta:
[{"party": 2573, "id": 22, "lines": []}]
"""
response_sale = requests.get(
url_order + f"/order/{order_id}",
headers={
"Authorization": f"bearer {key}",
},
)
return response_sale
@tool
def add_lines_to_order(
order_id: int, product: str, unit: str, quantity: str, unitprice: str
):
"""
Agrega una línea de producto a una orden existente.
Args:
order_id (int): ID de la orden a la que se agregará la línea.
product (str): ID del producto a agregar.
unit (str): ID de la unidad de medida del producto.
quantity (str): Cantidad del producto a agregar.
unitprice (str): Precio unitario del producto.
Returns:
requests.Response: La respuesta del servidor que contiene:
- order_lines (List[int]): Lista de IDs de las líneas creadas
- status (str): Estado de la operación ('success' si fue exitosa)
- message (str): Mensaje descriptivo del resultado
Ejemplo de respuesta:
{"order_lines": [1], "status": "success", "message": "Order lines created successfully"}
"""
data = {
"order": order_id,
"product": product,
"unit": unit,
"quantity": quantity,
"unitprice": unitprice,
}
response = requests.post(
url_order + f"/{order_id}/order_line",
headers={
"Authorization": f"bearer {key}",
},
data=json.dumps(data),
)
return response
@tool
def search_associate_party_to_contact_mechanism(contact_mechanism: str):
"""
Busca un cliente en el sistema por un metodo de contacto que pueda ser un numero de celular o un correo electronico.
Args:
contact_mechanism (str): El número de contacto del cliente.
Returns:
requests.Response: La respuesta del servidor que contiene:
- id (int): ID de la asociación en la base de datos
- associate_party (int): ID del cliente asociado (Party)
- status (str): Estado de la operación ('success' si fue exitosa)
- type (str): Tipo de contacto asociado
- message (str): Mensaje descriptivo del resultado
Ejemplo de respuesta:
{"id": 1, "associate_party": 1, "status": "success", "type": "phone", "message": "Associate Party found"}
"""
response = requests.get(
url_order + f"/associate_party/{contact_mechanism}",
headers={
"Authorization": f"bearer {key}",
},
)
return response
tools = [
create_party,
create_sale_order,
search_sale_order,
add_lines_to_order,
search_associate_party_to_contact_mechanism,
]
# if __name__ == "__main__":
# # Crear una orden de venta
# party = 2573
# pickup_location = "at_home"
# order_id = create_sale_order(party=party, pickup_location=pickup_location)
# print(f"\nOrden creada con ID: {order_id}")
# # Agregar líneas a la orden
# product = "1"
# unit = "1"
# quantity = "3"
# unitprice = "15"
# add_line_response = add_lines_to_order(order_id, product, unit, quantity, unitprice)
# print(f"\nRespuesta al agregar línea: {add_line_response.text}")
# # Verificar la orden actualizada
# updated_order = search_sale_order(order_id)
# print(f"\nOrden actualizada: {updated_order.text}")
# # Agregar otra línea a la orden
# product = "2"
# unit = "1"
# quantity = "3"
# unitprice = "15"
# add_line_response = add_lines_to_order(order_id, product, unit, quantity, unitprice)
# print(f"\nRespuesta al agregar línea: {add_line_response.text}")
# # Verificar la orden actualizada
# updated_order = search_sale_order(order_id)
# print(f"\nOrden actualizada: {updated_order.text}")

View File

@@ -0,0 +1,20 @@
store:
hours: "Lunes a Viernes: 8:00 AM - 8:00 PM | Sábados: 9:00 AM - 6:00 PM | Domingos y Festivos: 10:00 AM - 4:00 PM"
location: "Carrera 53 #42-81, 2do piso, Diagonal al Parque del Periodista, Medellín, (Col)"
contact:
phone: "+57 302 356 77 97"
whatsapp: "+57 302 356 77 97"
email: "info@recreo.red"
about: |
Acerca de Nosotros
La Corporación Centro Taller Recreo le apuesta a la Economía Solidaria como una herramienta
para establecer relaciones fundadas en el compartir, la colectividad y el consumo responsable
con la colectividad y el medio ambiente; para trascender el competir, el individualismo y el
consumismo. De esta manera, buscamos promover valores solidarios como la responsabilidad, el
respeto, la ayuda mutua, la confianza y la equidad.
Una de nuestras acciones más importantes es la consolidación del Circuito Cooperativo Tienda
La Ilusión, CIRCOOTIL. Por medio del Circuito se tejen puentes campo-ciudad aunando esfuerzos
de productores campesinos, tenderos y consumidores concientes.
page: "https://recreo.red/"