Integra AI con tools (Anthropic/Z.ai) y elimina código obsoleto

- Reemplaza respuestas programadas por AI con tools
- Implementa buscar_productos y confirmar_venta como tools
- Configura API Anthropic (Z.ai) con glm-4.7
- Elimina código legacy de búsqueda manual
- Limpia variables obsoletas (PicoClaw, OpenClaw)
- Actualiza docker-compose para nuevo flujo
This commit is contained in:
Renato
2026-02-15 22:17:36 +01:00
parent b5772e159e
commit 14435cd3b2
4 changed files with 534 additions and 173 deletions

View File

@@ -1,97 +1,324 @@
import asyncio
import json
import sqlite3
import os
import sqlite3
import logging
import uuid
from contextlib import asynccontextmanager
import unicodedata
from datetime import datetime
from pathlib import Path
from typing import Optional
from typing import Optional, List
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def normalize_text(text: str) -> str:
"""Remove accents and normalize text for searching"""
# Normalize to NFKD and remove combining characters (accents)
normalized = unicodedata.normalize("NFKD", text)
return "".join(c for c in normalized if not unicodedata.combining(c))
from fastapi import (
FastAPI,
WebSocket,
WebSocketDisconnect,
HTTPException,
UploadFile,
File,
Depends,
Request,
)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel
import httpx
OPENCLAW_WS_URL = os.getenv("OPENCLAW_WS_URL", "ws://openclaw_gateway:18789")
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN", "wlillidan1-demo-token-12345")
# Anthropic API Configuration (Z.ai)
ANTHROPIC_API_KEY = "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS"
ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic"
# System prompt for the AI sales assistant
SYSTEM_PROMPT = """Sos el asistente de ventas de Demo Librería, una librería escolar en Argentina.
Tu trabajo es ayudar al vendedor a atender clientes de forma rápida y con información exacta del inventario.
## HERRAMIENTAS DISPONIBLES
Tenés acceso a estas herramientas. SIEMPRE usarlas antes de responder sobre stock o precios:
1. `buscar_productos` - Busca productos en el inventario por nombre o descripción
Input: query (string) - nombre o descripción del producto
2. `confirmar_venta` - Registra una venta y descuenta el stock
Input: producto_nombre (string), cantidad (number)
## REGLAS OBLIGATORIAS (nunca violarlas)
1. **NUNCA des precios aproximados.** Siempre usá `buscar_productos` para obtener el precio exacto.
Si la herramienta no devuelve el producto, decí que no tenés ese dato, no inventes.
2. **Si no hay stock de un producto**, ofrecé SIEMPRE la alternativa más cercana que sí haya stock.
Ej: "No tenemos Bic azul, pero sí tenemos Faber roja a $750."
3. **Al final de cada consulta exitosa**, preguntá: "¿Se concretó la venta?"
Si dicen sí, preguntá cuántas unidades y usá `confirmar_venta`.
4. **Respondé SIEMPRE en español argentino coloquial pero profesional.**
Usá "vos", "tenés", "querés". No uses "usted" ni español neutro.
5. **Sé conciso.** Una respuesta de chat, no un ensayo. Máximo 3-4 líneas por respuesta.
6. **Cuando el usuario pida múltiples productos** (ej: "10 cuadernos y 5 lápices"),
usá `buscar_productos` para CADA producto por separado y respondé con la info de todos.
## EJEMPLOS DE RESPUESTAS CORRECTAS
Vendedor: "tenés birome bic azul?"
Bot: [usa buscar_productos con "birome bic azul"]
Bot: "Sí, tenemos Bic Cristal azul a $850. Quedan 23. ¿Se vendió?"
Vendedor: "no hay regla 30cm"
Bot: [usa buscar_productos con "regla 30cm"]
Bot: "No tenemos regla de 30cm por ahora 😕 Pero sí hay de 20cm (Maped, $650, stock 8). ¿Te sirve esa?"
Vendedor: "10 cuadernos y 5 lápices"
Bot: [usa buscar_productos con "cuaderno"]
Bot: [usa buscar_productos con "lápiz"]
Bot: "Encontré:
• Cuaderno Rivadavia 48 Hojas - $2500 (stock: 20)
• Lápiz Faber Castell 2B - $450 (stock: 100)
¿Se concretó la venta? ¿Cuántas unidades de cada uno?"""
def db_buscar_productos(query: str) -> list:
"""Busca productos en la base de datos"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query_normalized = normalize_text(query.lower())
search_term = f"%{query_normalized}%"
cursor.execute(
"""
SELECT id, nombre, marca, categoria, precio, stock
FROM productos
WHERE activo = 1 AND (
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(nombre), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ? OR
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(marca), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ? OR
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(categoria), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ?
)
ORDER BY stock DESC
LIMIT 10
""",
(search_term, search_term, search_term),
)
productos = cursor.fetchall()
conn.close()
return [
{
"id": p["id"],
"nombre": p["nombre"],
"marca": p["marca"] or "",
"categoria": p["categoria"],
"precio": p["precio"],
"stock": p["stock"],
}
for p in productos
]
def db_confirmar_venta(producto_nombre: str, cantidad: int) -> dict:
"""Registra una venta en la base de datos"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query_normalized = normalize_text(producto_nombre.lower())
search_term = f"%{query_normalized}%"
cursor.execute(
"""
SELECT * FROM productos
WHERE activo = 1 AND (
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(nombre), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ?
)
LIMIT 1
""",
(search_term,),
)
producto = cursor.fetchone()
if not producto:
conn.close()
return {"error": f"Producto no encontrado: {producto_nombre}"}
if producto["stock"] < cantidad:
conn.close()
return {
"error": f"Stock insuficiente. Stock actual: {producto['stock']}, solicitado: {cantidad}",
"producto": producto["nombre"],
"stock_disponible": producto["stock"],
}
cursor.execute(
"""
INSERT INTO ventas (producto_id, cantidad, precio_vendido, vendedor)
VALUES (?, ?, ?, ?)
""",
(producto["id"], cantidad, producto["precio"], "AI Bot"),
)
cursor.execute(
"""
UPDATE productos SET stock = stock - ?, updated_at = datetime('now')
WHERE id = ?
""",
(cantidad, producto["id"]),
)
conn.commit()
cursor.execute("SELECT stock FROM productos WHERE id = ?", (producto["id"],))
nuevo_stock = cursor.fetchone()["stock"]
conn.close()
return {
"success": True,
"producto": producto["nombre"],
"cantidad": cantidad,
"precio_unitario": producto["precio"],
"total": cantidad * producto["precio"],
"stock_nuevo": nuevo_stock,
}
async def chat_with_ai(message: str, session_id: str = "pymesbot") -> Optional[str]:
"""Send message to OpenClaw CLI"""
import subprocess
"""Send message to Anthropic API with tools"""
# Forzar al agente a dar respuesta completa
message_full = (
message
+ " Dame una respuesta completa con TODOS los productos del inventario, no resumas."
)
# Definir las herramientas disponibles
tools = [
{
"name": "buscar_productos",
"description": "Busca productos en el inventario por nombre o descripción. Usar SIEMPRE antes de dar precios.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Nombre o descripción del producto a buscar",
}
},
"required": ["query"],
},
},
{
"name": "confirmar_venta",
"description": "Registra una venta y descuenta el stock del inventario.",
"input_schema": {
"type": "object",
"properties": {
"producto_nombre": {
"type": "string",
"description": "Nombre del producto vendido",
},
"cantidad": {
"type": "integer",
"description": "Cantidad de unidades vendidas",
},
},
"required": ["producto_nombre", "cantidad"],
},
},
]
try:
# Usar OpenClaw CLI directamente
result = subprocess.run(
["openclaw", "agent", "--agent", "main", "-m", message_full],
capture_output=True,
text=True,
timeout=60,
cwd="/home/ren/openclaw",
)
messages = [{"role": "user", "content": message}]
max_iterations = 5
# Buscar la respuesta real (ignorar logs de gateway)
output = result.stdout + result.stderr
async with httpx.AsyncClient(timeout=60.0) as client:
for iteration in range(max_iterations):
try:
response = await client.post(
f"{ANTHROPIC_BASE_URL}/v1/messages",
headers={
"x-api-key": ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
json={
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"system": SYSTEM_PROMPT,
"messages": messages,
"tools": tools,
},
)
# Tomar todo después de la última línea de logs
lines = output.split("\n")
response_lines = []
capture = False
if response.status_code != 200:
logger.error(f"API error: {response.status_code} - {response.text}")
return (
f"Error al procesar la consulta. Por favor, intentá de nuevo."
)
for line in lines:
# Empezar a capturar después de estos patrones
if "Bind:" in line or "Gateway agent" in line:
capture = True
continue
if capture:
# Ignorar líneas de errores
if "error:" in line.lower() and "Error:" in line:
continue
response_lines.append(line)
result = response.json()
content = result.get("content", [])
# Limpiar response
response = "\n".join(response_lines).strip()
# Verificar si hay tool calls
tool_calls = [
block for block in content if block.get("type") == "tool_use"
]
text_response = [
block for block in content if block.get("type") == "text"
]
# Si está vacío, usar todo el output
if not response:
response = output
if not tool_calls:
# No hay tool calls, devolver la respuesta de texto
if text_response:
return text_response[0].get(
"text", "No pude generar una respuesta."
)
return "No pude generar una respuesta."
# Limpiar caracteres especiales
response = response.encode().decode("utf-8", errors="ignore")
# Procesar tool calls
tool_results = []
for tool_call in tool_calls:
tool_name = tool_call.get("name")
tool_input = tool_call.get("input", {})
# Limitar a ~2000 caracteres para mostrar más productos
if len(response) > 2000:
response = response[:2000] + "..."
if tool_name == "buscar_productos":
query = tool_input.get("query", "")
productos = db_buscar_productos(query)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": tool_call.get("id"),
"content": json.dumps({"productos": productos}),
}
)
elif tool_name == "confirmar_venta":
producto_nombre = tool_input.get("producto_nombre", "")
cantidad = tool_input.get("cantidad", 1)
resultado = db_confirmar_venta(producto_nombre, cantidad)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": tool_call.get("id"),
"content": json.dumps(resultado),
}
)
return response if response else "Disculpa, no pude obtener una respuesta."
# Agregar el tool use y los resultados a los mensajes
messages.append({"role": "assistant", "content": json.dumps(content)})
messages.append({"role": "user", "content": json.dumps(tool_results)})
except subprocess.TimeoutExpired:
logger.error("[OpenClaw] Timeout")
return "La consulta tardó demasiado. ¿Podés esperar un momento e intentarlo de nuevo?"
except Exception as e:
logger.error(f"[OpenClaw] Error: {e}")
return "Disculpa, tuve un problema al procesar tu mensaje."
except Exception as e:
logger.error(f"Error in chat_with_ai: {e}")
return f"Error al procesar la consulta: {str(e)}"
# Si llegamos aquí, se agotaron las iteraciones
return "Lo siento, no pude completar la consulta en el tiempo permitido."
# Alias para compatibilidad
@@ -400,6 +627,182 @@ async def confirmar_venta(req: ConfirmarVentaRequest):
}
# ============================================================================
# TOOLS PARA PICOCLAW
# ============================================================================
class BuscarProductosTool(BaseModel):
query: str
class ConfirmarVentaTool(BaseModel):
producto_nombre: str
cantidad: int
@app.get("/api/tools/listar_productos")
async def tool_listar_productos():
"""Tool para PicoClaw: Lista todos los productos disponibles"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT id, nombre, marca, categoria, precio, stock
FROM productos
WHERE activo = 1 AND stock > 0
ORDER BY categoria, nombre
""")
productos = cursor.fetchall()
conn.close()
result = []
for p in productos:
result.append(
{
"id": p["id"],
"nombre": p["nombre"],
"marca": p["marca"] or "",
"categoria": p["categoria"],
"precio": p["precio"],
"stock": p["stock"],
}
)
return {"productos": result, "total": len(result)}
@app.post("/api/tools/buscar_productos")
async def tool_buscar_productos(req: BuscarProductosTool):
"""Tool para PicoClaw: Busca productos por nombre o descripción"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# Normalizar búsqueda (quitar tildes)
query_normalized = normalize_text(req.query.lower())
search_term = f"%{query_normalized}%"
cursor.execute(
"""
SELECT id, nombre, marca, categoria, precio, stock
FROM productos
WHERE activo = 1 AND (
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(nombre), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ? OR
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(marca), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ? OR
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(categoria), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ?
)
ORDER BY stock DESC
LIMIT 10
""",
(search_term, search_term, search_term),
)
productos = cursor.fetchall()
conn.close()
if not productos:
return {
"productos": [],
"mensaje": f"No se encontraron productos para: {req.query}",
}
result = []
for p in productos:
result.append(
{
"id": p["id"],
"nombre": p["nombre"],
"marca": p["marca"] or "",
"categoria": p["categoria"],
"precio": p["precio"],
"stock": p["stock"],
}
)
return {"productos": result, "query": req.query}
@app.post("/api/tools/confirmar_venta")
async def tool_confirmar_venta(req: ConfirmarVentaTool):
"""Tool para PicoClaw: Registra una venta y descuenta stock"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# Buscar producto por nombre
query_normalized = normalize_text(req.producto_nombre.lower())
search_term = f"%{query_normalized}%"
cursor.execute(
"""
SELECT * FROM productos
WHERE activo = 1 AND (
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER(nombre), 'á', 'a'), 'é', 'e'), 'í', 'i'), 'ó', 'o'), 'ú', 'u') LIKE ?
)
LIMIT 1
""",
(search_term,),
)
producto = cursor.fetchone()
if not producto:
conn.close()
return {"error": f"Producto no encontrado: {req.producto_nombre}"}
if producto["stock"] < req.cantidad:
conn.close()
return {
"error": f"Stock insuficiente. Stock actual: {producto['stock']}, solicitado: {req.cantidad}",
"producto": producto["nombre"],
"stock_disponible": producto["stock"],
}
# Registrar venta
cursor.execute(
"""
INSERT INTO ventas (producto_id, cantidad, precio_vendido, vendedor)
VALUES (?, ?, ?, ?)
""",
(producto["id"], req.cantidad, producto["precio"], "PicoClaw Bot"),
)
# Actualizar stock
cursor.execute(
"""
UPDATE productos SET stock = stock - ?, updated_at = datetime('now')
WHERE id = ?
""",
(req.cantidad, producto["id"]),
)
conn.commit()
# Obtener stock actualizado
cursor.execute("SELECT stock FROM productos WHERE id = ?", (producto["id"],))
nuevo_stock = cursor.fetchone()["stock"]
conn.close()
return {
"success": True,
"producto": producto["nombre"],
"cantidad": req.cantidad,
"precio_unitario": producto["precio"],
"total": req.cantidad * producto["precio"],
"stock_anterior": producto["stock"],
"stock_nuevo": nuevo_stock,
}
# ============================================================================
# FIN TOOLS PICOCLAW
# ============================================================================
@app.get("/stats/ventas")
async def ventas_stats(periodo: str = "hoy"):
conn = sqlite3.connect(DB_PATH)
@@ -436,10 +839,10 @@ async def ventas_stats(periodo: str = "hoy"):
@app.get("/")
async def root():
async def root(request: Request):
if templates:
return templates.TemplateResponse(
"chat.html", {"request": {}, "nombre_negocio": "Demo Librería"}
"chat.html", {"request": request, "nombre_negocio": "Demo Librería"}
)
return HTMLResponse("""
<html>
@@ -512,7 +915,8 @@ async def root():
const sessionId = Math.random().toString(36).substring(7);
function connect() {
ws = new WebSocket(`wss://${window.location.host}/chat/ws/${sessionId}`);
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/chat/ws/${sessionId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.msg) {
@@ -582,107 +986,10 @@ async def websocket_chat(websocket: WebSocket, session_id: str):
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'}"
)
# Usar AI con tools para respuestas inteligentes
logger.info(f"[WS] Processing: {mensaje}")
respuesta = await chat_with_ai(mensaje, session_id)
chat_sessions[session_id].append({"role": "user", "content": mensaje})
chat_sessions[session_id].append(
@@ -692,10 +999,13 @@ async def websocket_chat(websocket: WebSocket, session_id: str):
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 Exception as e:
logger.error(f"[WS] Error: {e}")
await websocket.send_json({"typing": False})
await websocket.send_json(
{"msg": "Ups, algo salió mal. ¿Podés repetir la consulta?"}
)
except WebSocketDisconnect:
pass

View File

@@ -2,17 +2,37 @@
"agents": {
"defaults": {
"workspace": "/root/.picoclaw/workspace",
"model": "gpt-4o",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20,
"system": "Sos el asistente de ventas de Demo PymesBot, una librería en Argentina.\n\nREGLAS:\n1. Respondé en español argentino.\n2. Sé conciso (máximo 3 líneas).\n3. Siempre preguntá si se concretó la venta."
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 4096,
"temperature": 0.3,
"max_tool_iterations": 10,
"system": "Sos el asistente de ventas de Demo Librería, una librería escolar en Argentina.\n\nTu trabajo es ayudar al vendedor a atender clientes de forma rápida y con información exacta del inventario.\n\n## HERRAMIENTAS DISPONIBLES\nTenés acceso a estas herramientas. SIEMPRE usarlas antes de responder sobre stock o precios:\n- `buscar_productos`: busca productos en el inventario por nombre o descripción\n- `confirmar_venta`: registra una venta y descuenta el stock\n- `listar_productos`: muestra todos los productos disponibles\n\n## REGLAS OBLIGATORIAS (nunca violarlas)\n\n1. **NUNCA des precios aproximados.** Siempre usá `buscar_productos` para obtener el precio exacto. \n Si la herramienta no devuelve el producto, decí que no tenés ese dato, no inventes.\n\n2. **Si no hay stock de un producto**, ofrecé SIEMPRE la alternativa más cercana que sí haya stock.\n Ej: \"No tenemos Bic azul, pero sí tenemos Faber roja a $750.\"\n\n3. **Al final de cada consulta exitosa**, preguntá: \"¿Se concretó la venta?\"\n Si dicen sí, preguntá cuántas unidades y llamá a `confirmar_venta`.\n\n4. **Respondé SIEMPRE en español argentino coloquial pero profesional.**\n Usá \"vos\", \"tenés\", \"querés\". No uses \"usted\" ni español neutro.\n\n5. **Sé conciso.** Una respuesta de chat, no un ensayo. Máximo 3-4 líneas por respuesta.\n\n6. **Cuando el usuario pida múltiples productos** (ej: \"10 cuadernos y 5 lápices\"),\n buscá CADA producto por separado y respondé con la info de todos.\n\n## EJEMPLOS DE RESPUESTAS CORRECTAS\n\nVendedor: \"tenés birome bic azul?\"\nBot: [llama buscar_productos con \"birome bic azul\"]\nBot: \"Sí, tenemos Bic Cristal azul a $850. Quedan 23. ¿Se vendió?\"\n\nVendedor: \"no hay regla 30cm\"\nBot: [llama buscar_productos con \"regla 30cm\"]\nBot: \"No tenemos regla de 30cm por ahora 😕 Pero sí hay de 20cm (Maped, $650, stock 8). ¿Te sirve esa?\"\n\nVendedor: \"10 cuadernos y 5 lápices\"\nBot: [llama buscar_productos con \"cuaderno\"]\nBot: [llama buscar_productos con \"lápiz\"]\nBot: \"Encontré:\n• Cuaderno Rivadavia 48 Hojas - $2500 (stock: 20)\n• Lápiz Faber Castell 2B - $450 (stock: 100)\n\n¿Se concretó la venta? ¿Cuántas unidades de cada uno?\""
}
},
"providers": {
"openai": {
"anthropic": {
"api_key": "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS",
"api_base": "https://api.z.ai/v1"
"api_base": "https://api.z.ai/api/anthropic"
}
},
"tools": {
"custom": {
"buscar_productos": {
"command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"query\": \"{{query}}\"}' http://pymesbot_backend:8000/api/tools/buscar_productos",
"description": "Busca productos en el inventario por nombre o descripción. Usar SIEMPRE antes de dar precios. Input: query (string) - nombre o descripción del producto a buscar. Devuelve lista de productos con nombre, marca, precio y stock."
},
"confirmar_venta": {
"command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"producto_nombre\": \"{{producto_nombre}}\", \"cantidad\": {{cantidad}}}' http://pymesbot_backend:8000/api/tools/confirmar_venta",
"description": "Registra una venta y descuenta el stock del inventario. Input: producto_nombre (string), cantidad (number). Devuelve confirmación con detalles de la venta."
},
"listar_productos": {
"command": "curl -s http://pymesbot_backend:8000/api/tools/listar_productos",
"description": "Muestra todos los productos disponibles en el inventario. No requiere input. Devuelve lista completa de productos."
}
}
},
"gateway": {
"host": "0.0.0.0",
"port": 8080
}
}