feat: add agents/app
This commit is contained in:
parent
cb63404b50
commit
b6b94ac2b5
1
.gitignore
vendored
1
.gitignore
vendored
@ -225,4 +225,3 @@ pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
|
||||
.tdd_cache
|
||||
app
|
||||
|
5
agents/app/__init__.py
Normal file
5
agents/app/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
OC Assistant Don Confiao - A chatbot for managing orders and product catalog
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
69
agents/app/chat.py
Normal file
69
agents/app/chat.py
Normal file
@ -0,0 +1,69 @@
|
||||
from dotenv import load_dotenv
|
||||
from langgraph_tools.nodes import ChatBotState
|
||||
from langgraph_tools.graph import create_chat_graph
|
||||
from langchain_core.messages import HumanMessage
|
||||
import re
|
||||
|
||||
|
||||
def validate_phone(phone: str) -> bool:
|
||||
"""Valida que el número de teléfono tenga el formato correcto"""
|
||||
phone_pattern = re.compile(r"^\d{10}$") # Formato: 10 dígitos
|
||||
return bool(phone_pattern.match(phone))
|
||||
|
||||
|
||||
def get_valid_phone() -> str:
|
||||
"""Solicita y valida el número de teléfono del usuario"""
|
||||
while True:
|
||||
phone = input("Por favor, ingresa tu número de teléfono (10 dígitos): ").strip()
|
||||
if validate_phone(phone):
|
||||
return phone
|
||||
print("Número inválido. Debe contener exactamente 10 dígitos.")
|
||||
|
||||
|
||||
def run_simple_chat():
|
||||
# Cargar variables de entorno y crear el grafo
|
||||
load_dotenv()
|
||||
graph = create_chat_graph()
|
||||
|
||||
print("\nBienvenido a DonConfiao - Asistente Virtual")
|
||||
|
||||
# Solicitar número de teléfono al inicio
|
||||
phone = get_valid_phone()
|
||||
print("\nGracias! Ya puedes empezar a chatear.")
|
||||
print("Escribe 'salir' para terminar\n")
|
||||
|
||||
# Inicializar el estado con el teléfono
|
||||
current_state = ChatBotState(
|
||||
messages=[], query="", category="", response="", phone=phone
|
||||
)
|
||||
|
||||
while True:
|
||||
user_input = input("Tú: ").strip()
|
||||
|
||||
if user_input.lower() == "salir":
|
||||
print("\n¡Hasta pronto!")
|
||||
break
|
||||
|
||||
try:
|
||||
# Actualizar el estado con la nueva query
|
||||
current_state["query"] = user_input
|
||||
|
||||
# Invocar el grafo con el estado actualizado
|
||||
result = graph.invoke(current_state)
|
||||
|
||||
# Actualizar el estado actual (manteniendo el teléfono)
|
||||
current_state = result
|
||||
if "phone" not in current_state:
|
||||
current_state["phone"] = phone
|
||||
|
||||
# Mostrar la respuesta
|
||||
print(f"Categoría: {result['category']}")
|
||||
print(f"\nDonConfiao: {result['response']}\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {str(e)}")
|
||||
print("Por favor, intenta de nuevo.\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_simple_chat()
|
80
agents/app/chat_api.py
Normal file
80
agents/app/chat_api.py
Normal file
@ -0,0 +1,80 @@
|
||||
import requests
|
||||
import sys
|
||||
|
||||
class ChatClient:
|
||||
def __init__(self, base_url="http://127.0.0.1:8000"):
|
||||
self.base_url = base_url
|
||||
# Verificar conexión
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/status")
|
||||
if response.status_code != 200:
|
||||
raise ConnectionError("No se pudo conectar al servidor")
|
||||
print(f"✓ Conectado al servidor exitosamente")
|
||||
except requests.RequestException as e:
|
||||
print(f"Error: No se pudo conectar al servidor. {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
def send_message(self, phone: str, message: str) -> str:
|
||||
"""Envía un mensaje al servidor y retorna la respuesta"""
|
||||
try:
|
||||
print(f"Enviando mensaje al servidor...")
|
||||
response = requests.post(
|
||||
f"{self.base_url}/chat",
|
||||
json={
|
||||
"phone": phone,
|
||||
"message": message,
|
||||
"source": {
|
||||
"channel": "test",
|
||||
"channel_data": {}
|
||||
}
|
||||
},
|
||||
timeout=60 # Aumentado a 60 segundos
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["response"]
|
||||
except requests.RequestException as e:
|
||||
print(f"Error detallado: {e.__class__.__name__}: {str(e)}")
|
||||
return f"Error de comunicación: {str(e)}"
|
||||
|
||||
def main():
|
||||
print("¡Bienvenido a DonConfiao - Asistente Virtual de Tienda La Ilusión!")
|
||||
print("(Escribe 'salir' para terminar)")
|
||||
|
||||
# Inicializar cliente
|
||||
try:
|
||||
client = ChatClient()
|
||||
except Exception as e:
|
||||
print(f"Error al iniciar el cliente: {e}")
|
||||
return
|
||||
|
||||
# Solicitar número de teléfono
|
||||
while True:
|
||||
phone = input("\nIngresa tu número de teléfono: ").strip()
|
||||
if len(phone) >= 8:
|
||||
break
|
||||
print("Por favor ingresa un número de teléfono válido (mínimo 8 dígitos)")
|
||||
|
||||
print("\n¡Hola! ¿En qué puedo ayudarte hoy?")
|
||||
|
||||
# Bucle principal de chat
|
||||
while True:
|
||||
try:
|
||||
message = input("\nTú: ").strip()
|
||||
|
||||
if message.lower() == 'salir':
|
||||
print("¡Hasta luego! ¡Que tengas un excelente día!")
|
||||
break
|
||||
|
||||
if message:
|
||||
response = client.send_message(phone, message)
|
||||
print(f"\nDonConfiao: {response}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n¡Hasta luego! ¡Que tengas un excelente día!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"\nError: {str(e)}")
|
||||
print("Intenta de nuevo o escribe 'salir' para terminar")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
103
agents/app/chat_dc.py
Normal file
103
agents/app/chat_dc.py
Normal file
@ -0,0 +1,103 @@
|
||||
from dotenv import load_dotenv
|
||||
from langgraph_tools.nodes import ChatBotState
|
||||
from langgraph_tools.graph import create_chat_graph
|
||||
from langchain_core.messages import HumanMessage
|
||||
import streamlit as st
|
||||
import re
|
||||
|
||||
def validate_phone(phone: str) -> bool:
|
||||
"""Valida que el número de teléfono tenga el formato correcto"""
|
||||
phone_pattern = re.compile(r"^\d{10}$")
|
||||
return bool(phone_pattern.match(phone))
|
||||
|
||||
def initialize_session_state():
|
||||
"""Inicializa el estado de la sesión si no existe"""
|
||||
if "messages" not in st.session_state:
|
||||
st.session_state.messages = []
|
||||
if "phone" not in st.session_state:
|
||||
st.session_state.phone = None
|
||||
if "current_state" not in st.session_state:
|
||||
st.session_state.current_state = None
|
||||
if "graph" not in st.session_state:
|
||||
load_dotenv()
|
||||
st.session_state.graph = create_chat_graph()
|
||||
if "waiting_for_response" not in st.session_state:
|
||||
st.session_state.waiting_for_response = False
|
||||
|
||||
def main():
|
||||
st.markdown("<h1 style='text-align: center;'>DonConfiao - Asistente Virtual</h1>",
|
||||
unsafe_allow_html=True)
|
||||
|
||||
# Inicializar estado de sesión
|
||||
initialize_session_state()
|
||||
|
||||
# Solicitar número de teléfono en el sidebar
|
||||
phone = st.sidebar.text_input(
|
||||
"Ingresa tu número de teléfono (10 dígitos):",
|
||||
value=st.session_state.phone if st.session_state.phone else "",
|
||||
key="phone_input"
|
||||
)
|
||||
|
||||
# Validar teléfono
|
||||
if phone:
|
||||
if validate_phone(phone):
|
||||
st.session_state.phone = phone
|
||||
if st.session_state.current_state is None:
|
||||
st.session_state.current_state = ChatBotState(
|
||||
messages=[],
|
||||
query="",
|
||||
category="",
|
||||
response="",
|
||||
phone=phone
|
||||
)
|
||||
else:
|
||||
st.sidebar.error("Número inválido. Debe contener exactamente 10 dígitos.")
|
||||
return
|
||||
|
||||
# Si no hay teléfono válido, no mostrar el chat
|
||||
if not st.session_state.phone:
|
||||
st.info("Por favor, ingresa un número de teléfono válido para comenzar.")
|
||||
return
|
||||
|
||||
# Mostrar historial de mensajes
|
||||
for message in st.session_state.messages:
|
||||
with st.chat_message(message["role"]):
|
||||
st.markdown(message["content"])
|
||||
|
||||
# Input del usuario
|
||||
user_input = st.chat_input("¿En qué puedo ayudarte hoy?")
|
||||
|
||||
if user_input and st.session_state.current_state:
|
||||
# Agregar mensaje del usuario al historial inmediatamente
|
||||
st.session_state.messages.append({"role": "user", "content": user_input})
|
||||
st.session_state.waiting_for_response = True
|
||||
|
||||
# Mostrar el mensaje del usuario inmediatamente
|
||||
with st.chat_message("user"):
|
||||
st.markdown(user_input)
|
||||
|
||||
# Mostrar indicador de carga mientras se procesa la respuesta
|
||||
with st.chat_message("assistant"):
|
||||
with st.spinner("Generando respuesta..."):
|
||||
# Actualizar estado y obtener respuesta
|
||||
st.session_state.current_state["query"] = user_input
|
||||
try:
|
||||
result = st.session_state.graph.invoke(st.session_state.current_state)
|
||||
|
||||
# Actualizar estado
|
||||
st.session_state.current_state = result
|
||||
if "phone" not in st.session_state.current_state:
|
||||
st.session_state.current_state["phone"] = st.session_state.phone
|
||||
|
||||
# Agregar respuesta al historial
|
||||
response_text = f"**Categoría:** {result['category']}\n\n{result['response']}"
|
||||
st.session_state.messages.append({"role": "assistant", "content": response_text})
|
||||
st.markdown(response_text)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error: {str(e)}")
|
||||
|
||||
st.session_state.waiting_for_response = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
BIN
agents/app/data/catalog.db
Normal file
BIN
agents/app/data/catalog.db
Normal file
Binary file not shown.
BIN
agents/app/data/orders.db
Normal file
BIN
agents/app/data/orders.db
Normal file
Binary file not shown.
288
agents/app/documentation.md
Normal file
288
agents/app/documentation.md
Normal file
@ -0,0 +1,288 @@
|
||||
# DonConfiao - Asistente Virtual de Tienda La Ilusión
|
||||
|
||||
## Índice
|
||||
1. [Descripción General](#descripción-general)
|
||||
2. [Arquitectura](#arquitectura)
|
||||
3. [Componentes Principales](#componentes-principales)
|
||||
4. [Gestión de Pedidos](#gestión-de-pedidos)
|
||||
5. [Catálogo de Productos](#catálogo-de-productos)
|
||||
6. [Base de Datos](#base-de-datos)
|
||||
7. [API y Endpoints](#api-y-endpoints)
|
||||
8. [Guía de Uso](#guía-de-uso)
|
||||
9. [Configuración y Despliegue](#configuración-y-despliegue)
|
||||
|
||||
## Descripción General
|
||||
DonConfiao es un asistente virtual diseñado para Tienda La Ilusión que facilita la gestión de pedidos y la atención al cliente. Utiliza procesamiento de lenguaje natural para entender y responder a las solicitudes de los clientes de manera eficiente y amigable.
|
||||
|
||||
## Arquitectura
|
||||
El sistema está construido utilizando una arquitectura modular basada en agentes, implementada con LangGraph y LangChain. Los principales componentes son:
|
||||
|
||||
- **Classifier Agent**: Clasifica las intenciones del usuario
|
||||
- **Catalog Agent**: Maneja consultas relacionadas con productos
|
||||
- **Order Agent**: Gestiona todo lo relacionado con pedidos
|
||||
- **RAG System**: Proporciona respuestas basadas en conocimiento contextual
|
||||
|
||||
## Componentes Principales
|
||||
|
||||
### Classifier Agent
|
||||
- Analiza el contexto completo de la conversación
|
||||
- Categoriza las consultas en:
|
||||
- `general_info`: Información general
|
||||
- `catalog`: Consultas sobre productos
|
||||
- `order`: Gestión de pedidos
|
||||
|
||||
### Catalog Manager
|
||||
- Gestiona el catálogo de productos
|
||||
- Funcionalidades:
|
||||
- Búsqueda de productos
|
||||
- Verificación de disponibilidad
|
||||
- Actualización de precios
|
||||
- Gestión de inventario
|
||||
|
||||
### Order Manager
|
||||
- Sistema completo de gestión de pedidos
|
||||
- Estados de Pedido:
|
||||
- `in_cart`: En carrito
|
||||
- `confirmed`: Confirmado
|
||||
- `processing`: En proceso
|
||||
- `ready`: Listo
|
||||
- `delivering`: En entrega
|
||||
- `delivered`: Entregado
|
||||
- `cancelled`: Cancelado
|
||||
|
||||
- Estados de Entrega:
|
||||
- `pending`: Pendiente
|
||||
- `assigned`: Asignado
|
||||
- `in_transit`: En tránsito
|
||||
- `delivered`: Entregado
|
||||
- `failed`: Fallido
|
||||
|
||||
## Gestión de Pedidos
|
||||
|
||||
### Funcionalidades Principales
|
||||
1. **Gestión de Carrito**
|
||||
- Agregar productos
|
||||
- Remover productos
|
||||
- Ver contenido
|
||||
- Modificar cantidades
|
||||
|
||||
2. **Gestión de Pedidos**
|
||||
- Confirmar pedidos
|
||||
- Modificar pedidos existentes
|
||||
- Combinar múltiples pedidos
|
||||
- Eliminar pedidos
|
||||
- Consultar estado
|
||||
- Aplicar descuentos
|
||||
|
||||
3. **Seguimiento de Entregas**
|
||||
- Actualización de estado
|
||||
- Gestión de direcciones
|
||||
- Notificaciones de cambios
|
||||
|
||||
### Herramientas Disponibles
|
||||
```python
|
||||
- add_to_cart
|
||||
- remove_from_cart
|
||||
- view_cart
|
||||
- confirm_order
|
||||
- view_order_history
|
||||
- get_order_status
|
||||
- merge_orders
|
||||
- delete_order
|
||||
- modify_order
|
||||
- get_order_products
|
||||
- update_delivery_status
|
||||
- apply_discount
|
||||
```
|
||||
|
||||
## Catálogo de Productos
|
||||
|
||||
### Estructura de Producto
|
||||
```python
|
||||
{
|
||||
'producto_id': str,
|
||||
'producto': str,
|
||||
'precio': float,
|
||||
'unidad': str,
|
||||
'categoria': str,
|
||||
'stock': int
|
||||
}
|
||||
```
|
||||
|
||||
### Funcionalidades
|
||||
- Búsqueda por nombre
|
||||
- Filtrado por categoría
|
||||
- Verificación de stock
|
||||
- Actualización de precios
|
||||
- Gestión de inventario
|
||||
|
||||
## Base de Datos
|
||||
|
||||
### Tablas Principales
|
||||
|
||||
#### Orders
|
||||
```sql
|
||||
CREATE TABLE 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,
|
||||
updated_at TIMESTAMP
|
||||
)
|
||||
```
|
||||
|
||||
#### Order Items
|
||||
```sql
|
||||
CREATE TABLE 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)
|
||||
)
|
||||
```
|
||||
|
||||
## API y Endpoints
|
||||
|
||||
### Chat API
|
||||
- `POST /chat`: Procesa mensajes de chat
|
||||
```python
|
||||
{
|
||||
"message": str,
|
||||
"phone": str,
|
||||
"context": dict
|
||||
}
|
||||
```
|
||||
- `GET /history`: Obtiene historial de conversación
|
||||
```python
|
||||
{
|
||||
"phone": str
|
||||
}
|
||||
```
|
||||
|
||||
### Server API
|
||||
- `GET /products`: Lista de productos
|
||||
- `GET /orders`: Historial de pedidos
|
||||
- `POST /orders`: Crear nuevo pedido
|
||||
- `PUT /orders/{id}`: Actualizar pedido
|
||||
- `DELETE /orders/{id}`: Eliminar pedido
|
||||
|
||||
## Guía de Uso
|
||||
|
||||
### Ejemplos de Interacción
|
||||
|
||||
1. **Consultar Productos**
|
||||
```
|
||||
Usuario: "¿Qué productos tienen disponibles?"
|
||||
DonConfiao: [Lista productos con precios y disponibilidad]
|
||||
```
|
||||
|
||||
2. **Crear Pedido**
|
||||
```
|
||||
Usuario: "Quiero 2 kilos de arroz"
|
||||
DonConfiao: [Agrega al carrito y muestra confirmación]
|
||||
```
|
||||
|
||||
3. **Modificar Pedido**
|
||||
```
|
||||
Usuario: "Modifica mi pedido #123"
|
||||
DonConfiao: [Muestra opciones de modificación]
|
||||
```
|
||||
|
||||
4. **Combinar Pedidos**
|
||||
```
|
||||
Usuario: "Combina mis pedidos #123 y #456"
|
||||
DonConfiao: [Verifica y combina los pedidos]
|
||||
```
|
||||
|
||||
### Mejores Prácticas
|
||||
1. Usar número de teléfono para identificación
|
||||
2. Verificar disponibilidad antes de confirmar
|
||||
3. Confirmar cambios importantes
|
||||
4. Mantener actualizadas las direcciones de entrega
|
||||
|
||||
## Configuración y Despliegue
|
||||
|
||||
### Requisitos
|
||||
- Python 3.8+
|
||||
- SQLite3
|
||||
- Dependencias en `requirements.txt`
|
||||
|
||||
### Variables de Entorno
|
||||
```env
|
||||
OPENAI_API_KEY=your_api_key
|
||||
DATABASE_PATH=path/to/database
|
||||
```
|
||||
|
||||
### Estructura del Proyecto
|
||||
```
|
||||
app/
|
||||
├── data/
|
||||
│ ├── orders.db
|
||||
│ └── catalog.db
|
||||
├── langgraph_tools/
|
||||
│ ├── tools/
|
||||
│ │ ├── orders/
|
||||
│ │ └── catalog/
|
||||
│ ├── nodes.py
|
||||
│ └── prompts.yaml
|
||||
├── rag/
|
||||
│ └── knowledge_base/
|
||||
├── server.py
|
||||
├── chat.py
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
### Instalación
|
||||
```bash
|
||||
# Clonar repositorio
|
||||
git clone [repository_url]
|
||||
|
||||
# Instalar dependencias
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Inicializar base de datos
|
||||
python init_db.py
|
||||
|
||||
# Iniciar servidor
|
||||
python server.py
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Ejecutar todos los tests
|
||||
python -m pytest test/
|
||||
|
||||
# Ejecutar tests específicos
|
||||
python -m pytest test/test_catalog_db.py
|
||||
python -m pytest test/test_db.py
|
||||
```
|
||||
|
||||
## Mantenimiento y Soporte
|
||||
|
||||
### Logs y Monitoreo
|
||||
- Los logs se guardan en `data/logs/`
|
||||
- Monitoreo de errores y excepciones
|
||||
- Seguimiento de uso y rendimiento
|
||||
|
||||
### Backup y Recuperación
|
||||
- Backups automáticos diarios
|
||||
- Procedimiento de recuperación documentado
|
||||
- Gestión de versiones de la base de datos
|
||||
|
||||
### Contacto y Soporte
|
||||
- Equipo de desarrollo: dev@tiendailusion.com
|
||||
- Soporte técnico: support@tiendailusion.com
|
||||
- Documentación adicional: [wiki_url]
|
||||
|
||||
---
|
||||
|
||||
Esta documentación está en constante evolución. Para sugerencias o correcciones, por favor contactar al equipo de desarrollo.
|
BIN
agents/app/graph_DonConfia.png
Normal file
BIN
agents/app/graph_DonConfia.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
0
agents/app/langchain_tools/__init__.py
Normal file
0
agents/app/langchain_tools/__init__.py
Normal file
0
agents/app/langchain_tools/chains.py
Normal file
0
agents/app/langchain_tools/chains.py
Normal file
55
agents/app/langchain_tools/cost.py
Normal file
55
agents/app/langchain_tools/cost.py
Normal file
@ -0,0 +1,55 @@
|
||||
import tiktoken
|
||||
|
||||
|
||||
# Model costs
|
||||
LLM_COSTS = {
|
||||
"o1-mini": {
|
||||
"input": 3.00 / 1_000_000, # $3.00 per 1M input tokens
|
||||
"output": 12.00 / 1_000_000, # $12.00 per 1M output tokens
|
||||
},
|
||||
"gpt-4o-mini": {
|
||||
"input": 0.150 / 1_000_000, # $0.150 per 1M input tokens
|
||||
"output": 0.600 / 1_000_000, # $0.600 per 1M output tokens
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def calculate_total_cost(model, input_tokens, output_tokens):
|
||||
"""
|
||||
Calculate the total cost of using a language model based on token usage.
|
||||
|
||||
Parameters:
|
||||
model (str): The model's name (e.g., "gpt-4o-mini").
|
||||
input_tokens (int): The number of input tokens used.
|
||||
output_tokens (int): The number of output tokens generated.
|
||||
|
||||
Returns:
|
||||
float: The total cost in USD.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model name is not found in LLM_COSTS.
|
||||
"""
|
||||
if model not in LLM_COSTS:
|
||||
raise ValueError(f"Model '{model}' not found in LLM_COSTS.")
|
||||
|
||||
# Get per-token costs for the specified model
|
||||
input_cost_per_token = LLM_COSTS[model]["input"]
|
||||
output_cost_per_token = LLM_COSTS[model]["output"]
|
||||
|
||||
# Calculate total cost for input and output tokens
|
||||
total_input_cost = input_tokens * input_cost_per_token
|
||||
total_output_cost = output_tokens * output_cost_per_token
|
||||
|
||||
# Combine the costs
|
||||
total_cost = total_input_cost + total_output_cost
|
||||
|
||||
return total_cost
|
||||
|
||||
|
||||
def calculate_tokens(text):
|
||||
encoding = tiktoken.get_encoding("o200k_base")
|
||||
|
||||
# Calculate the number of tokens
|
||||
num_tokens = len(encoding.encode(text))
|
||||
|
||||
return num_tokens
|
56
agents/app/langchain_tools/llm.py
Normal file
56
agents/app/langchain_tools/llm.py
Normal file
@ -0,0 +1,56 @@
|
||||
from openai import OpenAI
|
||||
from dotenv import load_dotenv
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
# from .cost import calculate_tokens
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def load_llm_openai(
|
||||
temperature: float = 0.1, max_tokens: int = 2000, model: str = "gpt-4o-mini"
|
||||
) -> ChatOpenAI:
|
||||
llm = ChatOpenAI(temperature=temperature, max_tokens=max_tokens, model=model)
|
||||
return llm
|
||||
|
||||
|
||||
def load_llm_o1(
|
||||
prompt: str,
|
||||
model: str = "o1-mini",
|
||||
max_tokens: int = 2000,
|
||||
temperature: float = 0.1,
|
||||
) -> dict:
|
||||
client = OpenAI()
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
max_completion_tokens=max_tokens,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
|
||||
result = {
|
||||
"text": response.choices[0].message.content,
|
||||
"token_input": calculate_tokens(prompt),
|
||||
"token_output": calculate_tokens(response.choices[0].message.content),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
# Please install OpenAI SDK first: `pip3 install openai`
|
||||
def load_llm_deepseek():
|
||||
api_key = os.getenv("DEEPSEEK_API_KEY")
|
||||
|
||||
llm = ChatOpenAI(
|
||||
model="deepseek-chat",
|
||||
# model = "deepseek-reasoner",
|
||||
openai_api_base="https://api.deepseek.com",
|
||||
openai_api_key=api_key,
|
||||
)
|
||||
return llm
|
||||
|
||||
# probar el llm de deepseek
|
||||
|
||||
# llm = load_llm_deepseek()
|
||||
# response = llm.invoke("Hello, how are you?")
|
||||
|
||||
# print(response)
|
||||
|
0
agents/app/langgraph_tools/__init__.py
Normal file
0
agents/app/langgraph_tools/__init__.py
Normal file
44
agents/app/langgraph_tools/graph.py
Normal file
44
agents/app/langgraph_tools/graph.py
Normal 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()
|
199
agents/app/langgraph_tools/nodes.py
Normal file
199
agents/app/langgraph_tools/nodes.py
Normal 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,
|
||||
}
|
615
agents/app/langgraph_tools/prompts.yaml
Normal file
615
agents/app/langgraph_tools/prompts.yaml
Normal 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}
|
0
agents/app/langgraph_tools/state.py
Normal file
0
agents/app/langgraph_tools/state.py
Normal file
35
agents/app/langgraph_tools/tools/catalog/catalog_class.py
Normal file
35
agents/app/langgraph_tools/tools/catalog/catalog_class.py
Normal 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
|
190
agents/app/langgraph_tools/tools/catalog/catalog_tools.py
Normal file
190
agents/app/langgraph_tools/tools/catalog/catalog_tools.py
Normal 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,
|
||||
]
|
66
agents/app/langgraph_tools/tools/general_info.py
Normal file
66
agents/app/langgraph_tools/tools/general_info.py
Normal 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,
|
||||
]
|
476
agents/app/langgraph_tools/tools/orders/db_manager.py
Normal file
476
agents/app/langgraph_tools/tools/orders/db_manager.py
Normal 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()
|
370
agents/app/langgraph_tools/tools/orders/order_tools.py
Normal file
370
agents/app/langgraph_tools/tools/orders/order_tools.py
Normal 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,
|
||||
]
|
243
agents/app/langgraph_tools/tools/orders/order_tools_2.py
Normal file
243
agents/app/langgraph_tools/tools/orders/order_tools_2.py
Normal 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}")
|
20
agents/app/langgraph_tools/tools/store_info.yaml
Normal file
20
agents/app/langgraph_tools/tools/store_info.yaml
Normal 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/"
|
BIN
agents/app/productos_tienda.xlsx
Normal file
BIN
agents/app/productos_tienda.xlsx
Normal file
Binary file not shown.
82
agents/app/prueba_catalog.py
Normal file
82
agents/app/prueba_catalog.py
Normal file
@ -0,0 +1,82 @@
|
||||
from seller.catalog_tools import CatalogTrytonTools
|
||||
import json
|
||||
|
||||
# result = json.dumps(response, indent=4)
|
||||
|
||||
url = "http://192.168.0.25:8000"
|
||||
key = "9a9ffc430146447d81e6698240199a4be2b0e774cb18474999d0f60e33b5b1eb1cfff9d9141346a98844879b5a9e787489c891ddc8fb45cc903b7244cab64fb1"
|
||||
db = "tryton"
|
||||
application_name = "sale_don_confiao"
|
||||
|
||||
|
||||
catalog = CatalogTrytonTools(url, application_name, key, db)
|
||||
|
||||
# Lista de los productos
|
||||
print("=== Productos ===")
|
||||
|
||||
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"
|
||||
|
||||
print(products)
|
||||
|
||||
# Precio del los productos
|
||||
print("=== Precios de los productos ===")
|
||||
|
||||
response = catalog.search_products("Arroz")
|
||||
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"
|
||||
|
||||
print(precios)
|
||||
|
||||
# Revisar disponibilidad de los productos
|
||||
# print("=== Disponibilidad de los productos ===")
|
||||
|
||||
# response = catalog.search_products("Papa")
|
||||
# disponibilidad: str = ""
|
||||
|
||||
# for product in response:
|
||||
# id = product["id"]
|
||||
# name = product["name"]
|
||||
# stock = product["quantity"]
|
||||
|
||||
# disponibilidad += f"- id: {id} - Nombre: {name} - Stock: {stock}\n"
|
||||
|
||||
# print(disponibilidad)
|
||||
|
||||
|
||||
|
||||
# from langgraph_tools.tools.catalog.catalog_tools import (
|
||||
# list_products,
|
||||
# search_products,
|
||||
# check_price,
|
||||
# )
|
||||
|
||||
# def test_catalog_functions():
|
||||
# print("=== Probando lista de productos ===")
|
||||
# print(list_products())
|
||||
# print("\n")
|
||||
|
||||
# print("=== Probando búsqueda de productos ===")
|
||||
# print(search_products("Arroz"))
|
||||
# print("\n")
|
||||
|
||||
# print("=== Probando verificación de precio ===")
|
||||
# print(check_price("Arroz"))
|
||||
# print("\n")
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# test_catalog_functions()
|
20
agents/app/prueba_llm.py
Normal file
20
agents/app/prueba_llm.py
Normal file
@ -0,0 +1,20 @@
|
||||
# from langchain_tools.llm import load_llm_openai
|
||||
# import yaml
|
||||
#
|
||||
# with open("langgraph_tools/prompts.yaml", "r") as f:
|
||||
# PROMPTS = yaml.safe_load(f)
|
||||
#
|
||||
#
|
||||
# llm = load_llm_openai()
|
||||
# query = "Necesito informacion sobre la tienda."
|
||||
# prompt = PROMPTS["classifier"]["system"].format(query=query)
|
||||
#
|
||||
# response: dict = llm.invoke(prompt)
|
||||
#
|
||||
# print(response.content)
|
||||
|
||||
from langgraph_tools.tools.general_info import get_link_page
|
||||
|
||||
|
||||
link = get_link_page()
|
||||
print(link)
|
0
agents/app/rag/__init__.py
Normal file
0
agents/app/rag/__init__.py
Normal file
10
agents/app/rag/embeddings.py
Normal file
10
agents/app/rag/embeddings.py
Normal file
@ -0,0 +1,10 @@
|
||||
from dotenv import load_dotenv
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
|
||||
|
||||
def load_embeddins():
|
||||
load_dotenv()
|
||||
# model = "text-embedding-ada-002"
|
||||
model = "text-embedding-3-small"
|
||||
|
||||
return OpenAIEmbeddings(model=model)
|
17
agents/app/rag/llm.py
Normal file
17
agents/app/rag/llm.py
Normal file
@ -0,0 +1,17 @@
|
||||
from dotenv import load_dotenv
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
|
||||
def load_llm_openai():
|
||||
load_dotenv()
|
||||
# model = "gpt-3.5-turbo-0125"
|
||||
# model = "gpt-4o"
|
||||
model = "gpt-4o-mini"
|
||||
|
||||
llm = ChatOpenAI(
|
||||
model=model,
|
||||
temperature=0.1,
|
||||
max_tokens=2000,
|
||||
)
|
||||
|
||||
return llm
|
46
agents/app/rag/rag_chain.py
Normal file
46
agents/app/rag/rag_chain.py
Normal file
@ -0,0 +1,46 @@
|
||||
from langchain.chains import create_history_aware_retriever
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain.chains import create_retrieval_chain
|
||||
from langchain.chains.combine_documents import create_stuff_documents_chain
|
||||
|
||||
|
||||
def create_rag_chain(llm, retriever):
|
||||
contextualize_q_system_prompt = """
|
||||
Given a chat history and the latest user question \
|
||||
which might reference context in the chat history,
|
||||
formulate a standalone question \
|
||||
which can be understood without the chat history.
|
||||
Do NOT answer the question, \
|
||||
just reformulate it if needed and otherwise return it as is.
|
||||
"""
|
||||
contextualize_q_prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", contextualize_q_system_prompt),
|
||||
MessagesPlaceholder("chat_history"),
|
||||
("human", "{input}"),
|
||||
]
|
||||
)
|
||||
history_aware_retriever = create_history_aware_retriever(
|
||||
llm, retriever, contextualize_q_prompt
|
||||
)
|
||||
|
||||
# ___________________Chain con el chat history_______________________-
|
||||
qa_system_prompt = """
|
||||
You are an assistant for question-answering tasks. \
|
||||
Use the following pieces of retrieved context to answer the question. \
|
||||
If you don't know the answer, just say that you don't know. \
|
||||
The length of the answer should be sufficient to address
|
||||
what is being asked, \
|
||||
meaning don't limit yourself in length.\
|
||||
{context}"""
|
||||
qa_prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", qa_system_prompt),
|
||||
MessagesPlaceholder("chat_history"),
|
||||
("human", "{input}"),
|
||||
]
|
||||
)
|
||||
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
|
||||
|
||||
return create_retrieval_chain(
|
||||
history_aware_retriever, question_answer_chain)
|
18
agents/app/rag/retriever.py
Normal file
18
agents/app/rag/retriever.py
Normal file
@ -0,0 +1,18 @@
|
||||
from langchain_chroma import Chroma
|
||||
|
||||
|
||||
def create_retriever(embeddings, persist_directory: str):
|
||||
# Cargamos la vectorstore
|
||||
# vectordb = Chroma.from_documents(
|
||||
# persist_directory=st.session_state.persist_directory,
|
||||
# Este es el directorio del la vs del docuemnto del usuario
|
||||
# que se encuentra cargado en la session_state.
|
||||
# embedding_function=embeddings,
|
||||
# )
|
||||
vectordb = Chroma(
|
||||
persist_directory=persist_directory,
|
||||
embedding_function=embeddings,
|
||||
)
|
||||
|
||||
# Creamos el retriver para que retorne los fragmentos mas relevantes.
|
||||
return vectordb.as_retriever(search_kwargs={"k": 6})
|
42
agents/app/rag/split_docs.py
Normal file
42
agents/app/rag/split_docs.py
Normal file
@ -0,0 +1,42 @@
|
||||
from langchain_community.document_loaders.pdf import PyPDFLoader
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
import os
|
||||
|
||||
|
||||
# def load_split_docs(file_name: str) -> list:
|
||||
# file_path: str = os.path.join("documents", "pdfs", file_name)
|
||||
# loader = PyPDFLoader(file_path)
|
||||
# docs: list = loader.load()
|
||||
# chunk_size: int = 2000
|
||||
# chunk_overlap: int = 300
|
||||
#
|
||||
# splitter = RecursiveCharacterTextSplitter(
|
||||
# chunk_size=chunk_size, chunk_overlap=chunk_overlap
|
||||
# )
|
||||
# docs_split: list = splitter.split_documents(docs)
|
||||
#
|
||||
# return docs_split
|
||||
|
||||
|
||||
def load_split_docs(file_name: str) -> list:
|
||||
# Obtener el directorio base del proyecto
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Construir la ruta absoluta al PDF
|
||||
file_path = os.path.join(base_dir, "documents", "pdfs", file_name)
|
||||
|
||||
# Verificar si el archivo existe
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Archivo no encontrado en: {file_path}")
|
||||
raise FileNotFoundError(f"No se encontró el archivo en: {file_path}")
|
||||
|
||||
loader = PyPDFLoader(file_path)
|
||||
docs: list = loader.load()
|
||||
|
||||
chunk_size: int = 2000
|
||||
chunk_overlap: int = 300
|
||||
splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=chunk_size, chunk_overlap=chunk_overlap
|
||||
)
|
||||
docs_split: list = splitter.split_documents(docs)
|
||||
return docs_split
|
48
agents/app/rag/vectorstore.py
Normal file
48
agents/app/rag/vectorstore.py
Normal file
@ -0,0 +1,48 @@
|
||||
from langchain_chroma import Chroma
|
||||
import os
|
||||
|
||||
#
|
||||
# def create_vectorstore(docs_split: list, embeddings, file_name: str):
|
||||
# db_name: str = file_name.replace(".pdf", "").replace(" ", "_").lower()
|
||||
# persist_directory: str = f"embeddings/{db_name}"
|
||||
#
|
||||
# # Crear el directorio si no existe
|
||||
# os.makedirs(persist_directory, exist_ok=True)
|
||||
#
|
||||
# # Siempre crear/actualizar el vectorstore
|
||||
# vectordb = Chroma.from_documents(
|
||||
# persist_directory=persist_directory,
|
||||
# documents=docs_split,
|
||||
# embedding=embeddings,
|
||||
# )
|
||||
#
|
||||
# return vectordb
|
||||
|
||||
|
||||
def create_vectorstore(docs_split: list, embeddings, file_name: str):
|
||||
# Obtener el directorio base del proyecto
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Crear el nombre de la base de datos
|
||||
db_name: str = file_name.replace(".pdf", "").replace(" ", "_").lower()
|
||||
|
||||
# Construir la ruta absoluta para los embeddings
|
||||
persist_directory: str = os.path.join(base_dir, "embeddings", db_name)
|
||||
|
||||
# Crear el directorio si no existe
|
||||
os.makedirs(persist_directory, exist_ok=True)
|
||||
|
||||
# Debug log
|
||||
print(f"Creando vectorstore en: {persist_directory}")
|
||||
|
||||
try:
|
||||
# Crear/actualizar el vectorstore
|
||||
vectordb = Chroma.from_documents(
|
||||
persist_directory=persist_directory,
|
||||
documents=docs_split,
|
||||
embedding=embeddings,
|
||||
)
|
||||
return vectordb
|
||||
except Exception as e:
|
||||
print(f"Error al crear vectorstore: {e}")
|
||||
raise
|
128
agents/app/seller/catalog_tools.py
Normal file
128
agents/app/seller/catalog_tools.py
Normal file
@ -0,0 +1,128 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import requests
|
||||
import json
|
||||
|
||||
|
||||
class CatalogAbstract(ABC):
|
||||
@abstractmethod
|
||||
def list_products(self):
|
||||
"""
|
||||
Lista todos los productos disponibles en el catálogo con su disponibilidad
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search_products(self):
|
||||
"""
|
||||
Busca productos en el catálogo que coincidan con la consulta
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def check_price(self):
|
||||
"""
|
||||
Verifica el precio de un producto específico
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def check_availability(self):
|
||||
"""
|
||||
Verifica la disponibilidad de un producto espécifico
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_product_details(self):
|
||||
"""
|
||||
Obtiene detalles completos de un producto
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def list_tools(self):
|
||||
"""Retorna los metodos de esta clase en una lista"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CatalogTrytonTools(CatalogAbstract):
|
||||
def __init__(self, url, application_name, key, db):
|
||||
self.tryton_url = url
|
||||
self.application_name = application_name
|
||||
self.tryton_db = db
|
||||
self.tryton_key = key
|
||||
self.base_url = "{}/{}/{}".format(
|
||||
self.tryton_url,
|
||||
self.tryton_db,
|
||||
self.application_name,
|
||||
)
|
||||
|
||||
def list_products(self) -> list:
|
||||
"""
|
||||
Lista todos los productos disponibles en el catálogo con su disponibilidad
|
||||
"""
|
||||
endpoint = f"{self.base_url}/products"
|
||||
products = requests.get(
|
||||
endpoint,
|
||||
headers={
|
||||
"Authorization": f"bearer {self.tryton_key}",
|
||||
},
|
||||
)
|
||||
|
||||
return products.json()
|
||||
|
||||
def search_products(self, product_name) -> list:
|
||||
"""
|
||||
Busca productos en el catálogo que coincidan con la consulta
|
||||
"""
|
||||
|
||||
endpoint = f"{self.base_url}/search_products/{product_name}"
|
||||
products = requests.get(
|
||||
endpoint,
|
||||
headers={
|
||||
"Authorization": f"bearer {self.tryton_key}",
|
||||
},
|
||||
)
|
||||
|
||||
return products.json()
|
||||
|
||||
def check_price(self, product_name):
|
||||
"""
|
||||
Verifica el precio de un producto específico
|
||||
"""
|
||||
products = self.search_products(product_name)
|
||||
|
||||
return products.json()
|
||||
|
||||
def check_availability(self, product_name):
|
||||
"""
|
||||
Verifica la disponibilidad de un producto espécifico
|
||||
"""
|
||||
keys = ["id", "name", "template"]
|
||||
products = self.clean_keys_from_records(
|
||||
self.search_products(product_name), keys
|
||||
)
|
||||
|
||||
return products.json()
|
||||
|
||||
def get_product_details(self, product_name):
|
||||
"""
|
||||
Obtiene detalles completos de un producto
|
||||
"""
|
||||
keys = ["id", "name", "description"]
|
||||
products = self.search_products(product_name)
|
||||
|
||||
return products.json()
|
||||
|
||||
def list_tools(self):
|
||||
"""Retorna los metodos de esta clase en una lista"""
|
||||
raise NotImplementedError
|
||||
|
||||
def clean_keys_from_records(self, keys, records):
|
||||
# Iterar sobre cada registro y eliminar las claves no deseadas
|
||||
for record in records:
|
||||
for clave in keys:
|
||||
record.pop(clave, None) # Elimina la clave si existe
|
||||
|
||||
# Mostrar el resultado
|
||||
return json.dumps(records, indent=4, ensure_ascii=False)
|
71
agents/app/server.py
Normal file
71
agents/app/server.py
Normal file
@ -0,0 +1,71 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from app.langgraph_tools.graph import create_chat_graph
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="ChatBot API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
graph = create_chat_graph()
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
user_id: str
|
||||
query: str
|
||||
|
||||
|
||||
chat_states = {}
|
||||
|
||||
|
||||
@app.post("/chat/")
|
||||
async def chat(request: ChatRequest):
|
||||
try:
|
||||
logger.info(
|
||||
f"Recibido mensaje: user_id={request.user_id}, query={request.query}"
|
||||
)
|
||||
|
||||
if request.user_id not in chat_states:
|
||||
chat_states[request.user_id] = {
|
||||
"messages": [],
|
||||
"query": "",
|
||||
"response": "",
|
||||
"phone": request.user_id,
|
||||
}
|
||||
|
||||
state = chat_states[request.user_id]
|
||||
state["query"] = request.query
|
||||
|
||||
try:
|
||||
result = graph.invoke(state)
|
||||
chat_states[request.user_id] = result
|
||||
|
||||
# Asegurar que siempre devolvemos una respuesta válida
|
||||
if isinstance(result, dict) and "response" in result:
|
||||
return {"response": result["response"]}
|
||||
else:
|
||||
logger.warning(f"Resultado inesperado del graph: {result}")
|
||||
return {"response": str(result)}
|
||||
|
||||
except Exception as graph_error:
|
||||
logger.error(f"Error en el graph: {str(graph_error)}", exc_info=True)
|
||||
return {"response": "Lo siento, hubo un error al procesar tu solicitud."}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error general: {str(e)}", exc_info=True)
|
||||
return {"response": "Lo siento, ocurrió un error inesperado."}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
Loading…
Reference in New Issue
Block a user