diff --git a/.gitignore b/.gitignore index 2b48346..959d4c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ __pycache__/ data/ *.log .DS_Store +picoclaw/ +pymesbot/picoclaw/workspace/ diff --git a/docker-compose.yml b/docker-compose.yml index d559836..cfc9781 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,10 +14,39 @@ services: environment: - OPENCLAW_TOKEN=wlillidan1-demo-token-12345 - OPENCLAW_WS_URL=ws://openclaw_gateway:18789 + - PICOLAW_URL=http://picoclaw:8080 + - USE_PICOLAW=true networks: - default - caddy - openclaw_net + depends_on: + - picoclaw + + picoclaw: + build: + context: ./picoclaw + dockerfile: Dockerfile + container_name: pymesbot_picoclaw + restart: unless-stopped + volumes: + - ./pymesbot/picoclaw/config.json:/root/.picoclaw/config.json:ro + - ./pymesbot/picoclaw/workspace:/root/.picoclaw/workspace + environment: + # Configuración Anthropic (Z.ai) + - ANTHROPIC_API_KEY=6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS + - ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic + - ANTHROPIC_MODEL=glm-4.7 + # Variables PicoClaw + - PICOCLAW_LOG_LEVEL=info + - PICOCLAW_AGENTS_DEFAULTS_MODEL=claude-3-5-sonnet-20241022 + - PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS=4096 + - PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE=0.3 + networks: + - default + - caddy + ports: + - "8202:8080" networks: default: diff --git a/pymesbot/backend/main.py b/pymesbot/backend/main.py index 894b4a7..1c89979 100644 --- a/pymesbot/backend/main.py +++ b/pymesbot/backend/main.py @@ -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(""" @@ -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 diff --git a/pymesbot/picoclaw/config.json b/pymesbot/picoclaw/config.json index aa07c2d..1dd58f9 100644 --- a/pymesbot/picoclaw/config.json +++ b/pymesbot/picoclaw/config.json @@ -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 } }