Implementa sistema de skills para búsqueda inteligente

- Carga skills desde archivos .md en workspace/skills/
- Agrega skill vendedor_libreria con contexto completo
- Agrega skill busqueda_productos con manejo de sinónimos
- Agrega skill gestion_ventas para registrar ventas
- Nueva tool listar_todo_el_stock para ver inventario completo
- Mejora system prompt con reglas claras de uso de tools
- Agrega mapeo de sinónimos (fibras→marcadores, bloc→papel)
- Procesa múltiples productos en una sola consulta
- Ofrece alternativas inteligentes cuando no hay stock
This commit is contained in:
Renato
2026-02-15 22:25:24 +01:00
parent 14435cd3b2
commit 0d4e281829

View File

@@ -35,57 +35,72 @@ import httpx
ANTHROPIC_API_KEY = "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS" ANTHROPIC_API_KEY = "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS"
ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic" ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic"
# System prompt for the AI sales assistant # Skills directory
SYSTEM_PROMPT = """Sos el asistente de ventas de Demo Librería, una librería escolar en Argentina. SKILLS_DIR = Path(__file__).parent.parent / "picoclaw" / "workspace" / "skills"
Tu trabajo es ayudar al vendedor a atender clientes de forma rápida y con información exacta del inventario.
def load_skills() -> str:
"""Carga todos los skills disponibles y los combina en un string"""
skills_content = []
if SKILLS_DIR.exists():
for skill_file in sorted(SKILLS_DIR.glob("*.md")):
try:
with open(skill_file, "r", encoding="utf-8") as f:
content = f.read()
skills_content.append(
f"\n{'=' * 60}\nSKILL: {skill_file.stem}\n{'=' * 60}\n{content}"
)
except Exception as e:
logger.warning(f"Error cargando skill {skill_file}: {e}")
return "\n\n".join(skills_content) if skills_content else ""
# Base system prompt
BASE_SYSTEM_PROMPT = """Sos el asistente de ventas de Demo Librería, una librería escolar en Argentina.
## REGLA CRÍTICA - SIEMPRE USAR HERRAMIENTAS
**NUNCA respondas de memoria.** Antes de dar cualquier información sobre productos, precios o stock, DEBÉS usar una herramienta.
## HERRAMIENTAS DISPONIBLES ## 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 1. `buscar_productos` - Busca productos específicos por nombre
Input: query (string) - nombre o descripción del producto Úsala cuando el usuario mencione un producto específico: "cuadernos", "lápices", "biromes", etc.
2. `confirmar_venta` - Registra una venta y descuenta el stock 2. `listar_todo_el_stock` - Muestra TODO el inventario completo
Input: producto_nombre (string), cantidad (number) Úsala cuando el usuario quiera ver todo: "mostrame todo", "qué tienen", "stock"
## REGLAS OBLIGATORIAS (nunca violarlas) 3. `confirmar_venta` - Registra una venta
Úsala solo cuando el usuario confirme que se vendió algo
1. **NUNCA des precios aproximados.** Siempre usá `buscar_productos` para obtener el precio exacto. ## INSTRUCCIONES IMPORTANTES
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. 1. **NUNCA inventar precios.** Siempre usar herramientas primero.
Ej: "No tenemos Bic azul, pero sí tenemos Faber roja a $750." 2. **MÚLTIPLES PRODUCTOS**: Cuando pidan varios productos ("bloc, lápices y fibras"), buscar CADA UNO por separado.
3. Si no hay stock, ofrecer alternativas.
4. Ser conciso (máximo 3-4 líneas).
5. Usar español argentino: "vos", "tenés", "querés".
6. Preguntar siempre al final: "¿Se concretó la venta?"
3. **Al final de cada consulta exitosa**, preguntá: "¿Se concretó la venta?" ## MAPEO DE SINÓNIMOS
Si dicen sí, preguntá cuántas unidades y usá `confirmar_venta`. - "fibras" → buscar "marcadores" o "colores"
- "bloc" → buscar "papel"
- "lapices de colores" → buscar "caja colores"
- "biromes" → buscar "birome"
4. **Respondé SIEMPRE en español argentino coloquial pero profesional.** ## EJEMPLOS
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. Usuario: "bloc de hojas, lápices de 12 y fibras"
Acción:
1. buscar_productos("bloc")
2. buscar_productos("caja colores")
3. buscar_productos("fibras")
Respuesta: Resultados de los 3 productos"""
6. **Cuando el usuario pida múltiples productos** (ej: "10 cuadernos y 5 lápices"), # Combinar base prompt con skills
usá `buscar_productos` para CADA producto por separado y respondé con la info de todos. SKILLS_CONTENT = load_skills()
SYSTEM_PROMPT = f"{BASE_SYSTEM_PROMPT}\n\n{SKILLS_CONTENT}"
## 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: def db_buscar_productos(query: str) -> list:
@@ -128,6 +143,40 @@ def db_buscar_productos(query: str) -> list:
] ]
def db_listar_todo_el_stock() -> dict:
"""Lista todo el inventario organizado por categorías"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT DISTINCT nombre, marca, categoria, precio, stock
FROM productos
WHERE activo = 1 AND stock > 0
ORDER BY categoria, nombre
""")
productos = cursor.fetchall()
conn.close()
# Organizar por categorías
categorias = {}
for p in productos:
cat = p["categoria"]
if cat not in categorias:
categorias[cat] = []
categorias[cat].append(
{
"nombre": p["nombre"],
"marca": p["marca"] or "",
"precio": p["precio"],
"stock": p["stock"],
}
)
return {"categorias": categorias, "total_productos": len(productos)}
def db_confirmar_venta(producto_nombre: str, cantidad: int) -> dict: def db_confirmar_venta(producto_nombre: str, cantidad: int) -> dict:
"""Registra una venta en la base de datos""" """Registra una venta en la base de datos"""
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
@@ -202,27 +251,35 @@ async def chat_with_ai(message: str, session_id: str = "pymesbot") -> Optional[s
tools = [ tools = [
{ {
"name": "buscar_productos", "name": "buscar_productos",
"description": "Busca productos en el inventario por nombre o descripción. Usar SIEMPRE antes de dar precios.", "description": "Busca productos específicos en el inventario por nombre o descripción. Usar SIEMPRE antes de dar información sobre productos.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"query": { "query": {
"type": "string", "type": "string",
"description": "Nombre o descripción del producto a buscar", "description": "Nombre o descripción del producto a buscar (ej: 'cuaderno', 'birome', 'lápices')",
} }
}, },
"required": ["query"], "required": ["query"],
}, },
}, },
{
"name": "listar_todo_el_stock",
"description": "Muestra TODO el inventario completo de la librería organizado por categorías. Usar cuando el usuario quiera ver todo el stock disponible.",
"input_schema": {
"type": "object",
"properties": {},
},
},
{ {
"name": "confirmar_venta", "name": "confirmar_venta",
"description": "Registra una venta y descuenta el stock del inventario.", "description": "Registra una venta y descuenta el stock del inventario. Usar SOLO cuando el usuario confirme explícitamente que se vendió algo.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"producto_nombre": { "producto_nombre": {
"type": "string", "type": "string",
"description": "Nombre del producto vendido", "description": "Nombre exacto o similar del producto vendido",
}, },
"cantidad": { "cantidad": {
"type": "integer", "type": "integer",
@@ -297,6 +354,15 @@ async def chat_with_ai(message: str, session_id: str = "pymesbot") -> Optional[s
"content": json.dumps({"productos": productos}), "content": json.dumps({"productos": productos}),
} }
) )
elif tool_name == "listar_todo_el_stock":
inventario = db_listar_todo_el_stock()
tool_results.append(
{
"type": "tool_result",
"tool_use_id": tool_call.get("id"),
"content": json.dumps(inventario),
}
)
elif tool_name == "confirmar_venta": elif tool_name == "confirmar_venta":
producto_nombre = tool_input.get("producto_nombre", "") producto_nombre = tool_input.get("producto_nombre", "")
cantidad = tool_input.get("cantidad", 1) cantidad = tool_input.get("cantidad", 1)