Initial commit: PymesBot Demo with IA integration
- FastAPI backend with WebSocket chat - SQLite database for products - Z.AI (GLM-4.7) integration for AI responses - Docker deployment ready - Caddy proxy configuration
This commit is contained in:
688
pymesbot/backend/main.py
Normal file
688
pymesbot/backend/main.py
Normal file
@@ -0,0 +1,688 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
import os
|
||||
import logging
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
HTTPException,
|
||||
UploadFile,
|
||||
File,
|
||||
Depends,
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
OPENCLAW_WS_URL = os.getenv("OPENCLAW_WS_URL", "ws://openclaw_gateway:18789")
|
||||
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN", "wlillidan1-demo-token-12345")
|
||||
|
||||
|
||||
async def chat_with_ai(message: str, session_id: str = "pymesbot") -> Optional[str]:
|
||||
"""Send message to Z.AI API directly"""
|
||||
import httpx
|
||||
|
||||
ZAI_API_KEY = "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS"
|
||||
ZAI_API_URL = "https://api.z.ai/api/anthropic/v1/messages"
|
||||
|
||||
system_prompt = """Sos el asistente de ventas de Demo Librería en Argentina.
|
||||
Productos: biromes, lápices, cuadernos, colores, reglas, etc.
|
||||
Precios en pesos argentinos.
|
||||
Respondé de forma útil, breve y siempre preguntá si se concretó la venta."""
|
||||
|
||||
try:
|
||||
response = httpx.post(
|
||||
ZAI_API_URL,
|
||||
headers={
|
||||
"Authorization": f"Bearer {ZAI_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": "glm-4.7",
|
||||
"max_tokens": 200,
|
||||
"system": system_prompt,
|
||||
"messages": [{"role": "user", "content": message}],
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
content = data.get("content", [])
|
||||
if content and len(content) > 0:
|
||||
return content[0].get("text", "")
|
||||
else:
|
||||
logger.error(f"[Z.AI] API error: {response.status_code} - {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Z.AI] Error: {e}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Alias para compatibilidad
|
||||
chat_with_openclaw = chat_with_ai
|
||||
|
||||
|
||||
DATA_DIR = Path(__file__).parent / "data"
|
||||
DB_PATH = DATA_DIR / "stock.db"
|
||||
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
|
||||
|
||||
def get_db():
|
||||
if not DB_PATH.exists():
|
||||
init_db()
|
||||
return DB_PATH
|
||||
|
||||
|
||||
def init_db():
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS productos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nombre TEXT NOT NULL,
|
||||
marca TEXT,
|
||||
categoria TEXT NOT NULL DEFAULT 'general',
|
||||
precio REAL NOT NULL,
|
||||
stock INTEGER NOT NULL DEFAULT 0,
|
||||
variantes TEXT,
|
||||
codigo TEXT,
|
||||
activo INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ventas (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
producto_id INTEGER NOT NULL,
|
||||
cantidad INTEGER NOT NULL,
|
||||
precio_vendido REAL NOT NULL,
|
||||
vendedor TEXT,
|
||||
notas TEXT,
|
||||
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (producto_id) REFERENCES productos(id)
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
clave TEXT PRIMARY KEY,
|
||||
valor TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
default_config = [
|
||||
("nombre_negocio", "Demo Librería"),
|
||||
("moneda", "ARS"),
|
||||
("moneda_simbolo", "$"),
|
||||
("alerta_stock_minimo", "5"),
|
||||
("vendedor_pin", "1234"),
|
||||
("admin_password", "admin123"),
|
||||
("rubro", "libreria"),
|
||||
]
|
||||
|
||||
for clave, valor in default_config:
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO config (clave, valor) VALUES (?, ?)", (clave, valor)
|
||||
)
|
||||
|
||||
sample_products = [
|
||||
(
|
||||
"Birome Bic Cristal Azul",
|
||||
"Bic",
|
||||
"escritura",
|
||||
850,
|
||||
50,
|
||||
'{"color": ["azul"]}',
|
||||
"7501031311309",
|
||||
),
|
||||
(
|
||||
"Birome Bic Cristal Rojo",
|
||||
"Bic",
|
||||
"escritura",
|
||||
850,
|
||||
30,
|
||||
'{"color": ["rojo"]}',
|
||||
"7501031311316",
|
||||
),
|
||||
(
|
||||
"Birome Bic Cristal Negro",
|
||||
"Bic",
|
||||
"escritura",
|
||||
850,
|
||||
45,
|
||||
'{"color": ["negro"]}',
|
||||
"7501031311323",
|
||||
),
|
||||
(
|
||||
"Cuaderno Rivadavia 48 Hojas",
|
||||
"Rivadavia",
|
||||
"cuadernos",
|
||||
2500,
|
||||
20,
|
||||
'{"tipo": ["rayado", "blanco"]}',
|
||||
None,
|
||||
),
|
||||
("Lápiz Faber Castell 2B", "Faber Castell", "escritura", 450, 100, None, None),
|
||||
("Goma de borrar Staedtler", "Staedtler", "escritura", 320, 25, None, None),
|
||||
("Regla 30cm", "Maped", "geometria", 650, 15, None, None),
|
||||
("Compás Prisma", "Prisma", "geometria", 2500, 8, None, None),
|
||||
("Caja de colores 12", "Maped", "colores", 3200, 18, None, None),
|
||||
("Papel glasé x 20", "Laprida", "colores", 850, 40, None, None),
|
||||
]
|
||||
|
||||
for nombre, marca, categoria, precio, stock, variantes, codigo in sample_products:
|
||||
v = variantes if variantes else None
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(nombre, marca, categoria, precio, stock, v, codigo),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
class BuscarStockRequest(BaseModel):
|
||||
query: str
|
||||
limit: int = 5
|
||||
|
||||
|
||||
class ConfirmarVentaRequest(BaseModel):
|
||||
producto_id: int
|
||||
cantidad: int
|
||||
precio_vendido: float
|
||||
vendedor: str = "vendedor"
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
pin: str
|
||||
|
||||
|
||||
app = FastAPI(title="PymesBot Backend")
|
||||
|
||||
if TEMPLATES_DIR.exists():
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
else:
|
||||
templates = None
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
init_db()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT valor FROM config WHERE clave = 'nombre_negocio'")
|
||||
nombre = cursor.fetchone()
|
||||
conn.close()
|
||||
return {"status": "ok", "negocio": nombre[0] if nombre else "Demo"}
|
||||
|
||||
|
||||
@app.get("/stock/search")
|
||||
async def buscar_stock(q: str, limit: int = 5):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
q_lower = q.lower()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT * FROM productos
|
||||
WHERE activo = 1 AND (
|
||||
LOWER(nombre) LIKE ? OR
|
||||
LOWER(marca) LIKE ? OR
|
||||
LOWER(categoria) LIKE ?
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN LOWER(nombre) LIKE ? THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
stock DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(f"%{q_lower}%", f"%{q_lower}%", f"%{q_lower}%", f"%{q_lower}%", limit),
|
||||
)
|
||||
|
||||
rows = cursor.fetchall()
|
||||
resultados = []
|
||||
|
||||
for row in rows:
|
||||
resultados.append(
|
||||
{
|
||||
"id": row["id"],
|
||||
"nombre": row["nombre"],
|
||||
"marca": row["marca"],
|
||||
"categoria": row["categoria"],
|
||||
"precio": row["precio"],
|
||||
"precio_formateado": f"${row['precio']:.0f}",
|
||||
"stock": row["stock"],
|
||||
"variantes": json.loads(row["variantes"]) if row["variantes"] else {},
|
||||
"hay_stock": row["stock"] > 0,
|
||||
"promo_activa": None,
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
|
||||
return {"resultados": resultados, "total": len(resultados), "query": q}
|
||||
|
||||
|
||||
@app.get("/stock/producto/{producto_id}")
|
||||
async def get_producto(producto_id: int):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM productos WHERE id = ?", (producto_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Producto con id {producto_id} no encontrado"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": row["id"],
|
||||
"nombre": row["nombre"],
|
||||
"marca": row["marca"],
|
||||
"categoria": row["categoria"],
|
||||
"precio": row["precio"],
|
||||
"stock": row["stock"],
|
||||
"variantes": json.loads(row["variantes"]) if row["variantes"] else {},
|
||||
"codigo": row["codigo"],
|
||||
"activo": bool(row["activo"]),
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/venta/confirmar")
|
||||
async def confirmar_venta(req: ConfirmarVentaRequest):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"SELECT * FROM productos WHERE id = ? AND activo = 1", (req.producto_id,)
|
||||
)
|
||||
producto = cursor.fetchone()
|
||||
|
||||
if not producto:
|
||||
conn.close()
|
||||
raise HTTPException(status_code=404, detail="Producto no encontrado")
|
||||
|
||||
if producto[4] < req.cantidad:
|
||||
conn.close()
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Stock insuficiente. Stock actual: {producto[4]}, solicitado: {req.cantidad}",
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ventas (producto_id, cantidad, precio_vendido, vendedor)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(req.producto_id, req.cantidad, req.precio_vendido, req.vendedor),
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE productos SET stock = stock - ?, updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
""",
|
||||
(req.cantidad, req.producto_id),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT stock FROM productos WHERE id = ?", (req.producto_id,))
|
||||
nuevo_stock = cursor.fetchone()[0]
|
||||
|
||||
venta_id = cursor.lastrowid
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"venta_id": venta_id,
|
||||
"producto": producto[1],
|
||||
"cantidad": req.cantidad,
|
||||
"precio_vendido": req.precio_vendido,
|
||||
"total": req.cantidad * req.precio_vendido,
|
||||
"stock_anterior": producto[4],
|
||||
"stock_nuevo": nuevo_stock,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/stats/ventas")
|
||||
async def ventas_stats(periodo: str = "hoy"):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
if periodo == "hoy":
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
|
||||
FROM ventas
|
||||
WHERE date(timestamp) = date('now')
|
||||
""")
|
||||
elif periodo == "semana":
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
|
||||
FROM ventas
|
||||
WHERE timestamp >= datetime('now', '-7 days')
|
||||
""")
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
|
||||
FROM ventas
|
||||
WHERE timestamp >= datetime('now', '-30 days')
|
||||
""")
|
||||
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"periodo": periodo,
|
||||
"total_ventas": row[0] or 0,
|
||||
"total_pesos": row[1] or 0,
|
||||
"ticket_promedio": row[2] or 0,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
if templates:
|
||||
return templates.TemplateResponse(
|
||||
"chat.html", {"request": {}, "nombre_negocio": "Demo Librería"}
|
||||
)
|
||||
return HTMLResponse("""
|
||||
<html>
|
||||
<head>
|
||||
<title>PymesBot - Demo</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
.chat-box { background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); height: 70vh; display: flex; flex-direction: column; }
|
||||
.chat-header { padding: 16px 20px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 12px; }
|
||||
.chat-header h1 { font-size: 18px; color: #333; }
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; }
|
||||
.message { margin-bottom: 16px; max-width: 80%; }
|
||||
.message.bot { margin-right: auto; }
|
||||
.message.user { margin-left: auto; text-align: right; }
|
||||
.message .bubble { display: inline-block; padding: 12px 16px; border-radius: 18px; }
|
||||
.message.bot .bubble { background: #f0f0f0; color: #333; }
|
||||
.message.user .bubble { background: #007bff; color: white; }
|
||||
.typing { color: #888; font-style: italic; padding: 8px; }
|
||||
.chat-input { padding: 16px 20px; border-top: 1px solid #eee; display: flex; gap: 12px; }
|
||||
.chat-input input { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 24px; outline: none; font-size: 16px; }
|
||||
.chat-input button { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 24px; cursor: pointer; font-size: 16px; }
|
||||
.chat-input button:hover { background: #0056b3; }
|
||||
.sidebar { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.sidebar h2 { font-size: 16px; margin-bottom: 12px; color: #333; }
|
||||
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||
.stat-value { font-weight: 600; color: #007bff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||
<div class="chat-box">
|
||||
<div class="chat-header">
|
||||
<h1>🛒 Demo Librería</h1>
|
||||
</div>
|
||||
<div class="chat-messages" id="messages">
|
||||
<div class="message bot">
|
||||
<div class="bubble">¡Hola! Soy el asistente de ventas de Demo Librería. ¿Qué estás buscando?</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<input type="text" id="msgInput" placeholder="Escribí tu consulta..." autocomplete="off">
|
||||
<button onclick="sendMessage()">Enviar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sidebar">
|
||||
<h2>📊 Hoy</h2>
|
||||
<div class="stat">
|
||||
<span>Ventas</span>
|
||||
<span class="stat-value" id="ventas-hoy">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Total</span>
|
||||
<span class="stat-value" id="total-hoy">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<h2>⚠️ Stock bajo</h2>
|
||||
<div id="alertas-stock"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let ws = null;
|
||||
const sessionId = Math.random().toString(36).substring(7);
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(`wss://${window.location.host}/chat/ws/${sessionId}`);
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.msg) {
|
||||
addMessage(data.msg, 'bot');
|
||||
}
|
||||
if (data.stats) {
|
||||
document.getElementById('ventas-hoy').textContent = data.stats.total_ventas;
|
||||
document.getElementById('total-hoy').textContent = '$' + data.stats.total_pesos.toLocaleString();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function addMessage(text, type) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `message ${type}`;
|
||||
div.innerHTML = `<div class="bubble">${text}</div>`;
|
||||
document.getElementById('messages').appendChild(div);
|
||||
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('msgInput');
|
||||
const text = input.value.trim();
|
||||
if (!text || !ws) return;
|
||||
addMessage(text, 'user');
|
||||
ws.send(JSON.stringify({ msg: text, session_id: sessionId }));
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
document.getElementById('msgInput').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
});
|
||||
|
||||
connect();
|
||||
|
||||
fetch('/stats/ventas?periodo=hoy')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('ventas-hoy').textContent = data.total_ventas;
|
||||
document.getElementById('total-hoy').textContent = '$' + data.total_pesos.toLocaleString();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
|
||||
chat_sessions = {}
|
||||
|
||||
|
||||
@app.websocket("/chat/ws/{session_id}")
|
||||
async def websocket_chat(websocket: WebSocket, session_id: str):
|
||||
await websocket.accept()
|
||||
logger.info(f"[WS] Accepted connection for session: {session_id}")
|
||||
|
||||
if session_id not in chat_sessions:
|
||||
chat_sessions[session_id] = []
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
logger.info(f"[WS] Received: {data}")
|
||||
mensaje = data.get("msg", "")
|
||||
session_id = data.get("session_id", session_id)
|
||||
|
||||
await websocket.send_json({"typing": True})
|
||||
|
||||
logger.info(f"[WS] Processing message: {mensaje}")
|
||||
|
||||
# Mejorar búsqueda: manejar plurales y buscar en nombre, marca y categoría
|
||||
mensaje_lower = mensaje.lower()
|
||||
|
||||
# Mapeo de plurales a singular
|
||||
plurales = {
|
||||
"lapices": "lapiz",
|
||||
"lápices": "lápiz",
|
||||
"lápices": "lápiz",
|
||||
"cuadernos": "cuaderno",
|
||||
"biromes": "birome",
|
||||
"gomas": "goma",
|
||||
"marcadores": "marcador",
|
||||
"colores": "colores",
|
||||
"fibras": "fibras",
|
||||
"reglas": "regla",
|
||||
"tijeras": "tijera",
|
||||
}
|
||||
|
||||
for plural, singular in plurales.items():
|
||||
mensaje_lower = mensaje_lower.replace(plural, singular)
|
||||
|
||||
# Extraer palabras clave de búsqueda (más de 3 letras)
|
||||
palabras = mensaje_lower.split()
|
||||
stopwords = {
|
||||
"hola",
|
||||
"buenos",
|
||||
"buenas",
|
||||
"que",
|
||||
"tiene",
|
||||
"tenes",
|
||||
"busco",
|
||||
"quiero",
|
||||
"necesito",
|
||||
"para",
|
||||
"una",
|
||||
"clienta",
|
||||
"vino",
|
||||
"buscar",
|
||||
"necesita",
|
||||
"quanto",
|
||||
"cuanto",
|
||||
"cuántos",
|
||||
"cuántas",
|
||||
}
|
||||
terminos = [p for p in palabras if len(p) > 3 and p not in stopwords]
|
||||
|
||||
if not terminos:
|
||||
# Si no hay términos claros, usar el mensaje completo para buscar
|
||||
terminos = [mensaje_lower]
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Buscar todos los términos juntos
|
||||
all_results = []
|
||||
for term in terminos:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT * FROM productos
|
||||
WHERE activo = 1 AND (
|
||||
LOWER(nombre) LIKE ? OR
|
||||
LOWER(marca) LIKE ? OR
|
||||
LOWER(categoria) LIKE ?
|
||||
)
|
||||
ORDER BY stock DESC
|
||||
LIMIT 10
|
||||
""",
|
||||
(f"%{term}%", f"%{term}%", f"%{term}%"),
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
if row["id"] not in [r["id"] for r in all_results]:
|
||||
all_results.append(dict(row))
|
||||
|
||||
rows = all_results[:5]
|
||||
|
||||
logger.info(f"[WS] Found {len(rows)} rows")
|
||||
|
||||
try:
|
||||
if rows:
|
||||
resultados = []
|
||||
for row in rows:
|
||||
resultados.append(
|
||||
f"• {row['nombre']} - ${row['precio']:.0f} (stock: {row['stock']})"
|
||||
)
|
||||
|
||||
respuesta = f"Encontre estos productos:\n" + "\n".join(resultados)
|
||||
respuesta += "\n\n¿Se concretó la venta? ¿Cuántas unidades?"
|
||||
logger.info(f"[WS] Respuesta: {respuesta[:50]}")
|
||||
else:
|
||||
# Usar OpenClaw cuando no hay match en DB
|
||||
logger.info(
|
||||
f"[WS] No products found, trying OpenClaw for: {mensaje}"
|
||||
)
|
||||
respuesta = await chat_with_openclaw(mensaje, session_id)
|
||||
if not respuesta:
|
||||
respuesta = "No encontré productos con esa descripción. ¿Podés ser más específico?"
|
||||
logger.info(
|
||||
f"[WS] OpenClaw response: {respuesta[:50] if respuesta else 'None'}"
|
||||
)
|
||||
|
||||
chat_sessions[session_id].append({"role": "user", "content": mensaje})
|
||||
chat_sessions[session_id].append(
|
||||
{"role": "assistant", "content": respuesta}
|
||||
)
|
||||
|
||||
await websocket.send_json({"typing": False})
|
||||
await websocket.send_json({"msg": respuesta})
|
||||
logger.info("[WS] Response sent")
|
||||
except Exception as e:
|
||||
logger.error(f"[WS] Error sending response: {e}")
|
||||
|
||||
conn.close()
|
||||
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user