feat: add agents/app

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

1
.gitignore vendored
View File

@ -225,4 +225,3 @@ pyvenv.cfg
pip-selfcheck.json
.tdd_cache
app

5
agents/app/__init__.py Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

BIN
agents/app/data/orders.db Normal file

Binary file not shown.

288
agents/app/documentation.md Normal file
View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

View File

View 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

View 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)

View File

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View 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
View 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)

View File

View 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
View 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

View 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)

View 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})

View 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

View 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

View 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
View 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"}