43 KiB
PymesBot — Especificación Técnica Completa del Proyecto
Documento de referencia para desarrollo con IA Versión 1.0 · Febrero 2026 · RVConsultas Este documento contiene TODO lo necesario para construir PymesBot desde cero. No asumas nada que no esté escrito acá. Si algo no está en este documento, preguntá antes de inventar.
ÍNDICE
- Qué es PymesBot
- Problema que resuelve
- Cómo funciona — visión general
- Stack tecnológico completo
- Arquitectura multi-tenant
- Estructura de carpetas en el servidor
- Base de datos — esquema completo
- Backend FastAPI — todos los endpoints
- El agente IA — PicoClaw + Claude
- Dashboard web — especificación UI
- Docker Compose por cliente
- Templates de configuración
- Flujo completo de una venta
- Modelo de negocio y precios
- Roadmap de implementación
- Reglas de desarrollo obligatorias
1. Qué es PymesBot
PymesBot es una plataforma SaaS multi-tenant que da a negocios minoristas (librerías, kioscos, papelerías, bazares) un asistente de ventas con inteligencia artificial accesible desde el navegador web.
El producto final es una web app donde el vendedor escribe en lenguaje natural y el bot le responde con información exacta del stock, precios, alternativas y combos. Al confirmar una venta, el stock se descuenta automáticamente.
Quién lo usa:
- Vendedor (en el mostrador): chat con el bot para atender clientes rápido
- Dueño del negocio (en la misma web): dashboard con estadísticas, gestión de stock, promociones
Quién lo opera:
- RVConsultas (Guillermo): da de alta clientes nuevos, cobra mensualidad, administra el servidor
2. Problema que resuelve
La mayoría de negocios PyME argentinos manejan el inventario en papel o Excel sin conexión. Esto genera:
- Precios aproximados ("creo que está como $800...") → desconfianza del cliente
- Tiempo perdido buscando productos en carpetas o cuadernos → el cliente se va
- Cero datos de ventas → el dueño no sabe qué se vende, cuándo ni cuánto
PymesBot resuelve los tres problemas sin pedirle al negocio que aprenda un sistema nuevo. El dueño sigue usando su Excel; el sistema lo importa.
3. Cómo funciona — visión general
[Vendedor escribe en el chat]
↓
[Web app (browser)] ←→ WebSocket ←→ [Backend FastAPI]
↓
[PicoClaw agent]
↓
[Claude API (Anthropic)]
↓
[Herramientas: buscar stock, registrar venta, armar combo]
↓
[SQLite del cliente]
Regla de oro: el vendedor completa una consulta en menos de 2 minutos.
Flujo de ejemplo:
- Vendedor escribe:
"tenés biromes bic azul?" - El bot busca en el stock → encuentra: precio $850, stock 23 unidades, variantes azul/rojo/negro
- Bot responde:
"Sí, Bic azul a $850 (quedan 23). También hay roja y negra al mismo precio." - Bot pregunta:
"¿Se concretó la venta? ¿Cuántas unidades?" - Vendedor:
"2" - Stock se descuenta de 23 a 21, venta se registra con precio y timestamp
4. Stack tecnológico completo
Lenguajes y frameworks
| Capa | Tecnología | Versión mínima | Por qué |
|---|---|---|---|
| Backend API | Python + FastAPI | Python 3.11, FastAPI 0.110 | Asíncrono, rápido, soporte nativo WebSocket |
| Servidor ASGI | Uvicorn | 0.29 | Standard para FastAPI en producción |
| Agente IA | PicoClaw (Go) | v0.0.1 | <10MB RAM, arranca en <1s, gateway nativo |
| Base de datos | SQLite | 3.x (incluido en Python) | Zero config, un archivo, backup trivial |
| Templates HTML | Jinja2 | 3.x | Incluido en FastAPI |
| Frontend | HTML + CSS + JS vanilla | ES2020 | Sin build step, carga instantánea |
| Reverse proxy | Nginx | 1.24+ | Wildcard SSL, reverse proxy, performance |
| SSL | Certbot + Let's Encrypt | Último | Gratis, auto-renovación |
| Contenedores | Docker + Docker Compose | Docker 24+, Compose v2 | Aislamiento por cliente |
Servicios externos
| Servicio | Uso | Quién paga |
|---|---|---|
| Anthropic API (Claude) | El cerebro del bot | RVConsultas (costo absorbido en precio mensual) |
| Modelo a usar | claude-sonnet-4-5-20250929 |
— |
| VPS | Servidor de producción | RVConsultas |
VPS recomendado
- RAM: 16 GB
- CPU: 6 núcleos
- Disco: 100 GB SSD
- OS: Ubuntu 24.04 LTS
- Capacidad: ~150 clientes simultáneos cómodos
5. Arquitectura multi-tenant
Concepto fundamental
Cada cliente (negocio) tiene su propio subdominio bajo rvconsultas.com:
castillo.rvconsultas.com → Librería Castillo
reyes.rvconsultas.com → Librería Reyes
nuevocliente.rvconsultas.com → cualquier negocio nuevo
Cada subdominio es un entorno completamente aislado:
- Su propio contenedor Docker (backend FastAPI)
- Su propio agente PicoClaw
- Su propia base de datos SQLite
- Su propio inventario y configuración
Un cliente NUNCA puede ver datos de otro cliente. Jamás. Este es un requisito no negociable.
Diagrama de arquitectura
Internet
│
▼
┌─────────────────────────────────────────────────┐
│ Nginx (host) │
│ *.rvconsultas.com → Let's Encrypt wildcard SSL │
│ Reverse proxy → puerto interno por cliente │
└────────┬────────────────────┬───────────────────┘
│ │
▼ ▼
castillo.rvconsultas.com reyes.rvconsultas.com
Puerto 8201 Puerto 8202
│ │
┌────────┴──────┐ ┌────────┴──────┐
│ Docker │ │ Docker │
│ Container A │ │ Container B │
│ │ │ │
│ FastAPI │ │ FastAPI │
│ PicoClaw │ │ PicoClaw │
│ SQLite DB │ │ SQLite DB │
└───────────────┘ └───────────────┘
│ │
└────────┬───────────┘
▼
Anthropic API
(claude-sonnet-4-5-20250929)
Una sola API key de RVConsultas
Nginx — configuración de subdominio por cliente
Cada cliente tiene su archivo /etc/nginx/conf.d/{slug}.conf:
server {
listen 80;
server_name castillo.rvconsultas.com;
# Certbot agrega el bloque HTTPS automáticamente
location / {
proxy_pass http://127.0.0.1:8201;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
6. Estructura de carpetas en el servidor
/opt/pymesbot/ ← raíz de toda la plataforma
│
├── .env.global ← API key de Anthropic (una sola, compartida)
│ # Contenido:
│ # ANTHROPIC_API_KEY=sk-ant-...
│
├── nginx/
│ └── conf.d/
│ ├── castillo.conf ← bloque Nginx generado por el instalador
│ └── reyes.conf
│
├── scripts/
│ ├── nuevo_cliente.sh ← script de onboarding
│ └── backup.sh ← backup diario de todas las DBs
│
├── castillo/ ← carpeta completa de UN cliente
│ ├── docker-compose.yml ← levanta los contenedores de este cliente
│ ├── .env ← variables de entorno (generadas por instalador)
│ │
│ ├── picoclaw/
│ │ └── config.json ← config de PicoClaw: API key + system prompt
│ │
│ ├── backend/
│ │ ├── Dockerfile ← imagen del backend FastAPI
│ │ ├── requirements.txt
│ │ ├── main.py ← FastAPI app principal
│ │ ├── db.py ← helpers de SQLite
│ │ ├── tools.py ← endpoints que llama PicoClaw como herramientas
│ │ ├── dashboard.py ← rutas del dashboard web
│ │ ├── auth.py ← autenticación simple (PIN vendedor / admin)
│ │ ├── models.py ← Pydantic models
│ │ └── templates/
│ │ ├── base.html ← layout base (navbar, CSS global)
│ │ ├── chat.html ← vista del vendedor (mostrador)
│ │ └── admin.html ← vista del dueño (dashboard)
│ │
│ └── data/
│ ├── stock.db ← base de datos SQLite del cliente
│ └── uploads/ ← Excels subidos por el dueño
│
└── reyes/ ← ídem para otro cliente
└── ...
7. Base de datos — esquema completo
Cada cliente tiene su propio archivo stock.db. El esquema se crea con este SQL al inicializar:
-- ═══════════════════════════════════════════════════════
-- TABLA: productos
-- ═══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS productos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL, -- "Birome Bic"
marca TEXT, -- "Bic"
categoria TEXT NOT NULL, -- "escritura"
precio REAL NOT NULL, -- 850.00
stock INTEGER NOT NULL DEFAULT 0, -- 23
variantes TEXT, -- JSON: {"color":["azul","rojo"],"talle":["M","L"]}
codigo TEXT, -- código interno o EAN (opcional)
activo INTEGER NOT NULL DEFAULT 1, -- 1=activo, 0=deshabilitado (no aparece en búsquedas)
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Índices para búsqueda rápida
CREATE INDEX IF NOT EXISTS idx_productos_nombre ON productos(nombre);
CREATE INDEX IF NOT EXISTS idx_productos_categoria ON productos(categoria);
CREATE INDEX IF NOT EXISTS idx_productos_activo ON productos(activo);
-- ═══════════════════════════════════════════════════════
-- TABLA: ventas
-- ═══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS ventas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
producto_id INTEGER NOT NULL REFERENCES productos(id),
cantidad INTEGER NOT NULL,
precio_vendido REAL NOT NULL, -- precio al momento de la venta (puede diferir del actual)
vendedor TEXT, -- nombre del vendedor (optativo)
notas TEXT, -- notas adicionales (optativo)
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_ventas_producto_id ON ventas(producto_id);
CREATE INDEX IF NOT EXISTS idx_ventas_timestamp ON ventas(timestamp);
-- ═══════════════════════════════════════════════════════
-- TABLA: promociones
-- ═══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS promociones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL, -- "Vuelta al cole 20% off"
tipo TEXT NOT NULL, -- "descuento_pct" | "descuento_fijo" | "precio_especial"
valor REAL NOT NULL, -- 20 (para pct) | 500 (para fijo) | 1500 (para precio_especial)
productos TEXT, -- JSON array de product IDs: [1,2,3] o NULL = todos
categorias TEXT, -- JSON array de categorías: ["escritura"] o NULL = todas
activa INTEGER NOT NULL DEFAULT 1,
fecha_inicio TEXT, -- NULL = siempre activa
fecha_fin TEXT, -- NULL = sin vencimiento
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ═══════════════════════════════════════════════════════
-- TABLA: config
-- Configuración del negocio como key-value
-- ═══════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS config (
clave TEXT PRIMARY KEY,
valor TEXT NOT NULL
);
-- Valores por defecto que se insertan al crear el negocio
INSERT OR IGNORE INTO config (clave, valor) VALUES
('nombre_negocio', 'Mi Negocio'), -- se sobreescribe con el nombre real
('moneda', 'ARS'),
('moneda_simbolo', '$'),
('combo_categorias', '["escritura","cuadernos","geometria","colores"]'),
('alerta_stock_minimo', '5'), -- notificar cuando stock < este número
('vendedor_pin', '1234'), -- PIN para acceso rápido al chat
('admin_password', ''), -- se genera aleatoriamente al instalar
('rubro', 'libreria'); -- libreria | kiosco | bazar | otro
Notas importantes sobre la DB
- SQLite es suficiente para este caso de uso. Un negocio PyME rara vez tiene más de 5.000 productos y algunas docenas de ventas por hora. SQLite maneja esto sin problema.
- Nunca usar ORM pesados como SQLAlchemy para esto. Usar
sqlite3de la stdlib de Python directamente oaiosqlitepara las queries async. Simple y sin magia. - El campo
varianteses un JSON string. Ejemplo:'{"color": ["azul", "rojo", "negro"], "talle": ["M", "L", "XL"]}'. El backend lo parsea conjson.loads(). - El campo
precio_vendidoen ventas es importante: el precio puede cambiar con el tiempo. Hay que guardar el precio al momento de la venta, no una referencia al precio actual del producto.
8. Backend FastAPI — todos los endpoints
Archivo: main.py
# Estructura básica de main.py
from fastapi import FastAPI, WebSocket, UploadFile, Depends
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI(title="PymesBot Backend")
# Montar rutas
app.include_router(stock_router, prefix="/stock", tags=["stock"])
app.include_router(ventas_router, prefix="/venta", tags=["ventas"])
app.include_router(combo_router, prefix="/combo", tags=["combos"])
app.include_router(promo_router, prefix="/promo", tags=["promociones"])
app.include_router(stats_router, prefix="/stats", tags=["estadísticas"])
app.include_router(dashboard_router, prefix="", tags=["dashboard"])
@app.get("/health")
async def health():
return {"status": "ok", "negocio": get_config("nombre_negocio")}
Endpoints de Stock (/stock)
GET /stock/search
Búsqueda de productos en lenguaje natural. Este es el endpoint más importante, lo llama PicoClaw.
Parámetros query:
q(string, requerido): query de búsqueda libre. Ej:"birome bic azul","cuaderno rayado chico"limit(int, opcional, default: 5): máximo de resultados
Lógica de búsqueda (implementar en este orden de prioridad):
- Búsqueda exacta por nombre (LIKE
%q%) - Búsqueda por palabras clave individuales (split por espacios, AND de todos los términos)
- Si hay menos de 3 resultados: búsqueda OR (al menos una palabra coincide)
- Solo devolver productos con
activo = 1
Respuesta exitosa (200):
{
"resultados": [
{
"id": 42,
"nombre": "Birome Bic Cristal",
"marca": "Bic",
"categoria": "escritura",
"precio": 850.00,
"precio_formateado": "$850",
"stock": 23,
"variantes": {"color": ["azul", "rojo", "negro"]},
"hay_stock": true,
"promo_activa": null
}
],
"total": 1,
"query": "birome bic azul"
}
Si no hay resultados:
{
"resultados": [],
"total": 0,
"query": "birome bic azul",
"sugerencias": ["Birome Bic Cristal", "Birome Faber Castell"]
}
Las sugerencias son productos de la misma categoría estimada con stock disponible.
GET /stock/producto/{id}
Detalle completo de un producto por ID.
Respuesta (200):
{
"id": 42,
"nombre": "Birome Bic Cristal",
"marca": "Bic",
"categoria": "escritura",
"precio": 850.00,
"stock": 23,
"variantes": {"color": ["azul", "rojo", "negro"]},
"codigo": "7501031311309",
"activo": true,
"created_at": "2026-01-15T10:30:00",
"updated_at": "2026-02-10T14:22:00"
}
Error 404:
{"detail": "Producto con id 42 no encontrado"}
PUT /stock/producto/{id}
Actualiza precio o stock de un producto existente. Solo el dueño puede llamar esto (auth requerida).
Body (todos opcionales, solo se actualiza lo que se envía):
{
"precio": 950.00,
"stock": 30,
"activo": true
}
Respuesta (200): el producto actualizado completo.
POST /stock/importar
Importa o reemplaza el inventario completo desde un archivo Excel o CSV. Solo el dueño puede llamar esto.
Form data:
file: archivo.xlsxo.csvmodo:"reemplazar"(borra todo y reimporta) |"actualizar"(solo actualiza precios/stock de los que ya existen, agrega los nuevos)
Columnas esperadas en el Excel/CSV (detección automática, case-insensitive):
nombreoproductoodescripcion→ camponombre(REQUERIDO)preciooprecio_ventaoprice→ campoprecio(REQUERIDO)stockocantidadoqty→ campostock(default: 0 si no está)categoriaorubroocategory→ campocategoria(default: "general")marcaobrand→ campomarca(opcional)codigooeanobarcode→ campocodigo(opcional)
Respuesta (200):
{
"importados": 245,
"actualizados": 12,
"errores": 0,
"errores_detalle": []
}
Respuesta con errores (200, pero con detalle de errores):
{
"importados": 240,
"actualizados": 0,
"errores": 5,
"errores_detalle": [
{"fila": 12, "razon": "precio inválido: 'consultar'"},
{"fila": 34, "razon": "nombre vacío"}
]
}
GET /stock/alertas
Devuelve productos con stock por debajo del mínimo configurado.
Respuesta (200):
{
"alertas": [
{"id": 12, "nombre": "Goma Staedtler", "stock": 3, "minimo": 5},
{"id": 44, "nombre": "Regla 30cm", "stock": 1, "minimo": 5}
],
"total": 2
}
Endpoints de Ventas (/venta)
POST /venta/confirmar
Registra una venta y descuenta el stock. Lo llama PicoClaw después de confirmar con el vendedor.
Body:
{
"producto_id": 42,
"cantidad": 2,
"precio_vendido": 850.00,
"vendedor": "María"
}
Validaciones antes de ejecutar:
- El producto existe y está activo
cantidad > 0stock actual >= cantidad(si el stock quedaría negativo, devolver error)- Si hay promo activa aplicable, usar ese precio (pero respetar el precio_vendido enviado si difiere)
Respuesta (200):
{
"venta_id": 891,
"producto": "Birome Bic Cristal",
"cantidad": 2,
"precio_vendido": 850.00,
"total": 1700.00,
"stock_anterior": 23,
"stock_nuevo": 21,
"timestamp": "2026-02-14T15:42:00"
}
Error 400 (stock insuficiente):
{
"detail": "Stock insuficiente. Stock actual: 1, solicitado: 2."
}
GET /venta/historial
Lista de ventas con filtros opcionales.
Parámetros query (todos opcionales):
fecha_desde: ISO date string. Default: hoy a las 00:00fecha_hasta: ISO date string. Default: ahoraproducto_id: filtrar por productovendedor: filtrar por vendedorlimit: default 100, max 1000offset: para paginación
Respuesta (200):
{
"ventas": [
{
"id": 891,
"producto_id": 42,
"producto_nombre": "Birome Bic Cristal",
"cantidad": 2,
"precio_vendido": 850.00,
"total": 1700.00,
"vendedor": "María",
"timestamp": "2026-02-14T15:42:00"
}
],
"total": 1,
"suma_total": 1700.00
}
Endpoints de Estadísticas (/stats)
GET /stats/ventas
Resumen de ventas para el dashboard del dueño.
Parámetros query:
periodo:"hoy"|"semana"|"mes"(default:"hoy")
Respuesta (200):
{
"periodo": "hoy",
"total_ventas": 47,
"total_pesos": 38500.00,
"ticket_promedio": 819.15,
"productos_mas_vendidos": [
{"nombre": "Birome Bic Cristal", "cantidad": 12, "total": 10200.00},
{"nombre": "Cuaderno Rivadavia", "cantidad": 8, "total": 12000.00}
],
"comparacion_periodo_anterior": {
"total_pesos_anterior": 31200.00,
"variacion_pct": 23.4
}
}
Endpoints de Combos (/combo)
POST /combo/armar
Genera una lista de productos sugeridos para un presupuesto dado. Lo llama PicoClaw.
Body:
{
"presupuesto": 300000,
"edad": 7,
"genero": "nena",
"categorias": ["escritura", "cuadernos", "geometria", "colores"],
"incluir_solo_con_stock": true
}
Notas sobre genero:
"nena": priorizar colores llamativos (rosa, lila, rojo), evitar nombres genéricamente masculinos en nombres de productos si hubiera"neno": priorizar azul, verde, colores neutrosnullo ausente: sin preferencia de género
Notas sobre edad:
<= 6 años: primaria inicial → lápices de colores grandes, plastilina, sin tijeras de punta7-10 años: primaria general → juego de geometría, cuadernos doble raya, colores largos11-13 años: primaria superior → cuadernos cuadriculados, compás, colores profesionales>= 14 años: secundaria → calculadora, carpeta, birome, cuaderno universitario
Lógica del armado de combo:
- Filtrar productos por categorías solicitadas y con stock > 0
- Ordenar por "esencialidad" (el backend tiene una lista hardcodeada de productos esenciales por edad)
- Ir sumando productos desde los más esenciales hasta llegar al 95% del presupuesto
- Si el presupuesto no alcanza para un producto esencial, marcarlo como "no incluido" pero mostrarlo igual
Respuesta (200):
{
"presupuesto": 300000,
"total_combo": 287500,
"vuelto": 12500,
"productos": [
{
"id": 42,
"nombre": "Birome Bic Cristal (azul)",
"precio": 850,
"cantidad": 2,
"subtotal": 1700,
"incluido": true,
"esencial": true
},
{
"id": 78,
"nombre": "Cuaderno Rivadavia A4",
"precio": 2500,
"cantidad": 3,
"subtotal": 7500,
"incluido": true,
"esencial": true
}
],
"no_incluidos": []
}
Endpoints de Promociones (/promo)
GET /promo/activas
Lista de promociones vigentes hoy.
Respuesta (200):
{
"promociones": [
{
"id": 3,
"nombre": "Vuelta al cole 20% off escritura",
"tipo": "descuento_pct",
"valor": 20,
"categorias": ["escritura"],
"productos": null,
"fecha_fin": "2026-03-15"
}
]
}
POST /promo/crear
Crea una nueva promoción. Solo el dueño puede llamar esto.
Body:
{
"nombre": "Vuelta al cole 20% off escritura",
"tipo": "descuento_pct",
"valor": 20,
"categorias": ["escritura"],
"productos": null,
"fecha_inicio": null,
"fecha_fin": "2026-03-15"
}
WebSocket de Chat (/chat)
WebSocket /chat/ws/{session_id}
El canal de comunicación entre el frontend del vendedor y el agente PicoClaw.
Flujo del WebSocket:
Frontend Backend (FastAPI) PicoClaw
| | |
|-- connect ws/session_abc ---> | |
| | |
|-- { "msg": "tenés bic?" } --> | |
| |-- llamar PicoClaw -------> |
| | |-- GET /stock/search?q=bic
| |<-- respuesta parcial ----- |
|<-- { "typing": true } ------- | |
| |<-- respuesta final ------- |
|<-- { "msg": "Sí, Bic...", "typing": false } ------------- |
Formato de mensajes del frontend al backend:
{ "msg": "texto del vendedor", "session_id": "abc123" }
Formato de mensajes del backend al frontend:
{ "msg": "texto de respuesta del bot", "typing": false }
{ "typing": true }
{ "error": "descripción del error" }
Historial de conversación:
- El backend mantiene el historial de la sesión en memoria (dict con
session_idcomo clave) - El historial se descarta después de 30 minutos de inactividad
- Máximo 20 turnos de historial por sesión (para no explotar el context window)
9. El agente IA — PicoClaw + Claude
Qué es PicoClaw
PicoClaw (github.com/sipeed/picoclaw) es un agente IA escrito en Go. Es extremadamente liviano:
- RAM: < 10 MB por instancia
- Startup: < 1 segundo
- Arquitectura: binario Go auto-contenido, sin dependencias externas
- Se conecta a la API de Claude (Anthropic) o cualquier API compatible con OpenAI
En nuestra arquitectura, PicoClaw corre como un proceso dentro del contenedor del cliente. El backend FastAPI lo llama internamente cuando llega un mensaje de chat.
Config de PicoClaw (picoclaw/config.json)
{
"agents": {
"defaults": {
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 1000,
"temperature": 0.3,
"max_tool_iterations": 5,
"system": "SYSTEM PROMPT AQUÍ (ver abajo)"
}
},
"providers": {
"anthropic": {
"api_key": "${ANTHROPIC_API_KEY}"
}
}
}
System Prompt completo
El system prompt se personaliza por negocio. Este es el template:
Sos el asistente de ventas de {NOMBRE_NEGOCIO}, una {RUBRO} ubicada en Argentina.
Tu trabajo es ayudar al vendedor a atender clientes de forma rápida y con información exacta.
## HERRAMIENTAS DISPONIBLES
Tenés acceso a estas herramientas. SIEMPRE usarlas antes de responder sobre stock o precios:
- `buscar_stock`: busca productos en el inventario por nombre o descripción
- `confirmar_venta`: registra una venta y descuenta el stock
- `armar_combo`: genera una lista de productos para un presupuesto dado
- `ver_promociones`: muestra las promociones activas de hoy
## REGLAS OBLIGATORIAS (nunca violarlas)
1. **NUNCA des precios aproximados.** Siempre usá `buscar_stock` para obtener el precio exacto.
Si el tool no devuelve precio, 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 armar combos**, SIEMPRE preguntá antes:
- ¿Cuánto es el presupuesto? (número exacto)
- ¿Para qué edad es?
- ¿Es para nene o nena? (optativo, decile que puede no responder)
4. **Al final de cada consulta exitosa**, preguntá: "¿Se concretó la venta?"
Si dicen sí, preguntá cuántas unidades y llamá a `confirmar_venta`.
5. **Respondé SIEMPRE en español argentino coloquial pero profesional.**
Usá "vos", "tenés", "querés". No uses "usted" ni español neutro.
6. **Sé conciso.** Una respuesta de chat, no un ensayo. Máximo 3-4 líneas por respuesta.
7. **Nunca menciones información de otros negocios, competidores ni otras sucursales.**
## EJEMPLOS DE RESPUESTAS CORRECTAS
Vendedor: "tenés birome bic azul?"
Bot: [llama buscar_stock con "birome bic azul"]
Bot: "Sí, tenemos Bic Cristal azul a $850. Quedan 23. ¿Se vendió?"
Vendedor: "no hay regla 30cm"
Bot: [llama buscar_stock con "regla 30cm"]
Bot: "No tenemos regla de 30cm por ahora 😕 Pero sí hay de 20cm (Maped, $650, stock 8). ¿Te sirve esa?"
Vendedor: "una nena me pregunta para útiles, tiene $300000"
Bot: "¡Perfecto! ¿Cuántos años tiene? Y si querés podés decirme si tiene algún color preferido."
Herramientas que PicoClaw puede llamar
PicoClaw llama los endpoints del backend FastAPI como "tools". La configuración de tools se pasa en el context:
{
"tools": [
{
"name": "buscar_stock",
"description": "Busca productos en el inventario. Úsala siempre antes de dar precios o hablar de stock.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Nombre o descripción del producto" }
},
"required": ["query"]
}
},
{
"name": "confirmar_venta",
"description": "Registra una venta y descuenta el stock. Solo llamar cuando el vendedor confirme.",
"input_schema": {
"type": "object",
"properties": {
"producto_id": { "type": "integer" },
"cantidad": { "type": "integer" },
"precio_vendido": { "type": "number" }
},
"required": ["producto_id", "cantidad", "precio_vendido"]
}
},
{
"name": "armar_combo",
"description": "Genera una lista de productos para un presupuesto dado.",
"input_schema": {
"type": "object",
"properties": {
"presupuesto": { "type": "number" },
"edad": { "type": "integer" },
"genero": { "type": "string", "enum": ["nena", "neno", null] }
},
"required": ["presupuesto"]
}
},
{
"name": "ver_promociones",
"description": "Muestra las promociones activas de hoy.",
"input_schema": { "type": "object", "properties": {} }
}
]
}
10. Dashboard web — especificación UI
Vista Vendedor (chat del mostrador)
URL: https://castillo.rvconsultas.com/
Acceso: PIN de 4 dígitos (configurable por el dueño)
Layout:
┌──────────────────────────────────────────┐
│ 🛒 Librería Castillo [vendedor: María]│ ← header
├─────────────────────┬────────────────────┤
│ │ 📊 Hoy │
│ CHAT │ Ventas: 47 │
│ │ Total: $38.500 │
│ Bot: Hola! ¿Qué │ │
│ necesitás buscar? │ ⚠️ Stock bajo (2) │
│ │ - Goma Staedtler │
│ Vend: tenés bic? │ - Regla 20cm │
│ │ │
│ Bot: Sí, Bic azul │ │
│ $850. Quedan 23. │ │
│ ¿Se vendió? │ │
│ │ │
│ [_______________] │ │
│ [ Enviar ] │ │
└─────────────────────┴────────────────────┘
Características del chat:
- El historial de la sesión actual se muestra en pantalla
- Los mensajes del bot tienen fondo diferente a los del vendedor
- Indicador de "escribiendo..." mientras el bot procesa
- Botones de acción rápida cuando corresponde:
[✓ Confirmar venta][Ver alternativas][Nuevo combo] - En móvil: el panel de resumen del día se colapsa. Solo el chat visible.
- Responsive: debe funcionar bien en tablet 10" y en PC de escritorio
Autenticación:
- Al entrar, pide el PIN de 4 dígitos
- El PIN correcto abre el chat
- No hay "logout", el PIN simplemente da acceso
Vista Dueño / Admin
URL: https://castillo.rvconsultas.com/admin
Acceso: usuario admin + contraseña generada al instalar
Secciones del admin:
📊 Dashboard principal
- Total vendido hoy / esta semana / este mes (switcher de período)
- Gráfico de barras de ventas por día (últimos 7 días)
- Top 5 productos más vendidos (tabla)
- Comparación con período anterior (% de variación)
📦 Gestión de stock
- Tabla con todos los productos: nombre, categoría, precio, stock, activo
- Edición inline: hacer click en precio o stock para editar
- Filtros: por categoría, por nombre (búsqueda), por "stock bajo"
- Botón "Importar Excel": abre modal para subir archivo
🎯 Promociones
- Lista de promociones activas/inactivas
- Formulario para crear nueva promo:
- Nombre
- Tipo:
% descuento/monto fijo/precio especial - Valor
- Aplicable a: todas las categorías / categorías específicas / productos específicos
- Fecha de vencimiento (opcional)
- Toggle para activar/desactivar
📋 Historial de ventas
- Tabla filtrable por fecha y producto
- Filtro de rango de fechas (date picker)
- Exportar a CSV (botón)
⚙️ Configuración
- Nombre del negocio
- Moneda
- Umbral de alerta de stock mínimo
- Cambiar PIN del vendedor
- Cambiar contraseña del admin
11. Docker Compose por cliente
Cada cliente tiene su propio docker-compose.yml. Se genera automáticamente con el instalador.
version: '3.8'
services:
backend_castillo:
build:
context: ./backend
dockerfile: Dockerfile
container_name: pymesbot_castillo
restart: always
ports:
- "${PUERTO}:8000" # PUERTO viene del .env, ej: 8201
volumes:
- ./data:/app/data # la DB SQLite vive acá
- ./backend:/app # para desarrollo (en prod, solo copiar)
env_file:
- .env
networks:
- pymesbot_net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
picoclaw_castillo:
image: ghcr.io/sipeed/picoclaw:latest
# Alternativa si la imagen no está disponible: build local del binario Go
container_name: picoclaw_castillo
restart: always
volumes:
- ./picoclaw:/root/.picoclaw # config.json de PicoClaw
- ./data:/data:ro # acceso READ-ONLY a los datos (solo el backend escribe)
depends_on:
backend_castillo:
condition: service_healthy
networks:
- pymesbot_net
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
networks:
pymesbot_net:
external: true # creada una vez por el instalador, compartida entre todos los clientes
Dockerfile del backend (backend/Dockerfile)
FROM python:3.11-slim
WORKDIR /app
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copiar e instalar dependencias Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar código
COPY . .
# Crear directorio de datos
RUN mkdir -p /app/data/uploads
# Exponer puerto
EXPOSE 8000
# Comando de inicio
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
requirements.txt
fastapi==0.110.0
uvicorn[standard]==0.29.0
jinja2==3.1.3
python-multipart==0.0.9
aiosqlite==0.20.0
openpyxl==3.1.2
pandas==2.2.0
anthropic==0.25.0
python-dotenv==1.0.1
aiofiles==23.2.1
httpx==0.27.0
12. Templates de configuración
.env por cliente
# Generado automáticamente por el instalador
ANTHROPIC_API_KEY=sk-ant-... # copiada del .env.global
CLIENTE_NOMBRE=Librería Castillo
CLIENTE_ID=castillo
PUERTO=8201
ADMIN_PASSWORD=Xk7mQ2p # generada aleatoriamente
VENDOR_PIN=4821 # generada aleatoriamente
DOMINIO_BASE=rvconsultas.com
Nginx conf por cliente (/etc/nginx/conf.d/castillo.conf)
server {
listen 80;
server_name castillo.rvconsultas.com;
# Certbot agrega automáticamente el bloque SSL al correr certbot --nginx
location / {
proxy_pass http://127.0.0.1:8201;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s; # importante para WebSocket
proxy_send_timeout 300s;
}
}
13. Flujo completo de una venta
Este es el flujo técnico detallado de lo que pasa cuando el vendedor escribe en el chat:
1. Vendedor escribe "tenés birome bic azul?" en el chat
2. JS del frontend: WebSocket.send({ msg: "tenés birome bic azul?", session_id: "abc" })
3. Backend FastAPI recibe el mensaje en el WebSocket handler
4. Backend envía { typing: true } al frontend → aparece indicador "escribiendo..."
5. Backend pasa el mensaje a PicoClaw con:
- El historial de la conversación (hasta 20 turnos)
- El system prompt del negocio
- Las herramientas disponibles
6. PicoClaw llama a Claude API con todo lo anterior
7. Claude decide llamar la herramienta buscar_stock con { query: "birome bic azul" }
8. PicoClaw hace GET http://localhost:8000/stock/search?q=birome+bic+azul
9. FastAPI busca en SQLite: encuentra "Birome Bic Cristal", precio $850, stock 23
10. FastAPI devuelve el JSON de resultados a PicoClaw
11. PicoClaw pasa el resultado a Claude
12. Claude genera la respuesta final: "Sí, tenemos Bic Cristal azul a $850. Quedan 23. ¿Se vendió?"
13. Backend envía la respuesta al frontend via WebSocket: { msg: "Sí, tenemos...", typing: false }
14. Frontend muestra el mensaje del bot en el chat
--- (segunda vuelta) ---
15. Vendedor escribe "sí, 2"
16. PicoClaw (con el historial que incluye el producto encontrado) llama confirmar_venta
con { producto_id: 42, cantidad: 2, precio_vendido: 850 }
17. FastAPI:
- Verifica: producto 42 existe, activo, stock 23 >= 2 ✓
- INSERT INTO ventas (producto_id, cantidad, precio_vendido, ...) VALUES (42, 2, 850, ...)
- UPDATE productos SET stock = stock - 2, updated_at = now() WHERE id = 42
- Devuelve: { venta_id: 891, stock_nuevo: 21, total: 1700 }
18. Claude genera confirmación: "¡Listo! 2 Bic Cristal azul a $850 c/u = $1.700. Stock actualizado: quedan 21."
19. Frontend muestra la confirmación
14. Modelo de negocio y precios
Planes
| Plan | Precio | Límites |
|---|---|---|
| Starter | $15 USD/mes | 1 usuario vendedor, hasta 500 productos |
| Pro | $30 USD/mes | 3 usuarios, productos ilimitados, estadísticas |
| Red | $45 USD/mes | Todo Pro + tendencias zonales, red de PyMEs |
Costos operativos estimados (por cliente activo)
| Item | Costo mensual |
|---|---|
| Anthropic API (Claude) | ~$1-2 USD (estimado 500 consultas/mes) |
| VPS (prorrateado entre clientes) | ~$0.15 USD |
| Total costo | ~$2 USD/cliente |
Margen bruto estimado (plan Starter): $15 - $2 = $13 USD por cliente
15. Roadmap de implementación
Fase 1 — MVP funcional (semanas 1-4)
Objetivo: un vendedor real puede atender un cliente en menos de 2 minutos.
- Estructura de carpetas y Docker Compose template
- Backend FastAPI con endpoints:
/stock/search,/venta/confirmar,/health - WebSocket de chat (
/chat/ws/{session_id}) - Integración PicoClaw + Claude API con herramientas reales
- Parser de Excel para importar stock inicial
- Dashboard mínimo: vista chat (vendedor) + upload Excel (dueño)
- Nginx config + SSL en subdominio de prueba
- Primer cliente piloto real usando el sistema
Criterio de éxito: el vendedor completa una consulta completa (buscar → confirmar → stock descontado) en menos de 2 minutos sin entrenamiento previo.
Fase 2 — Dashboard completo (mes 2)
- Estadísticas de ventas con gráficos
- Sistema de promociones completo
- Armado de combos con filtro por edad y género
- Alertas de stock mínimo
- Vista admin completa
- Interfaz responsive para tablet y móvil
Fase 3 — Red de PyMEs (mes 3+)
- Módulo opt-in de datos compartidos
- Panel de tendencias zonales
- API para distribuidores
16. Reglas de desarrollo obligatorias
Estas reglas son no negociables. Si algo entra en conflicto con estas reglas, aplicar la regla.
Seguridad
- La API key de Anthropic NUNCA se hardcodea. Siempre desde variable de entorno.
- Nunca logear la contraseña sudo, la API key ni las contraseñas de admin.
- Cada cliente tiene su propio contenedor y su propia DB. Nunca compartir conexión de DB entre clientes.
- Todas las rutas de admin deben requerir autenticación. Nunca dejar una ruta admin sin protección.
Datos
- El campo
precio_vendidoen ventas es siempre el precio real de la venta, no una FK al precio actual del producto. - Nunca hacer DELETE en la tabla productos. Usar
activo = 0para desactivar. - Todos los timestamps en formato ISO 8601 UTC:
datetime('now')en SQLite.
Performance
- El endpoint
/stock/searchdebe responder en menos de 200ms. Si tarda más, optimizar los índices. - El WebSocket no debe bloquear. Usar
asynciocorrectamente. Nunca llamadas síncronas dentro de handlers async.
Código
- Usar tipado estricto de Python con Pydantic para todos los bodies de request y response.
- Nunca usar
*en imports (nofrom fastapi import *). - Cada endpoint debe tener docstring con descripción, parámetros y qué devuelve.
- Los errores de validación devuelven siempre JSON con el campo
detaildescriptivo en español. - Tests: al menos un test de integración por endpoint crítico (
/stock/search,/venta/confirmar).
UX
- Los mensajes de error que ve el vendedor deben estar en español coloquial, nunca mostrar stack traces.
- El chat debe mostrar el indicador de "escribiendo..." mientras el bot procesa. Nunca dejar al vendedor mirando una pantalla en blanco.
Fin del documento. Versión 1.0 — PymesBot Project Spec