Files
demo/01_PYMESBOT_PROJECT_SPEC.md

1263 lines
43 KiB
Markdown

# 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
1. [Qué es PymesBot](#1-qué-es-pymesbot)
2. [Problema que resuelve](#2-problema-que-resuelve)
3. [Cómo funciona — visión general](#3-cómo-funciona--visión-general)
4. [Stack tecnológico completo](#4-stack-tecnológico-completo)
5. [Arquitectura multi-tenant](#5-arquitectura-multi-tenant)
6. [Estructura de carpetas en el servidor](#6-estructura-de-carpetas-en-el-servidor)
7. [Base de datos — esquema completo](#7-base-de-datos--esquema-completo)
8. [Backend FastAPI — todos los endpoints](#8-backend-fastapi--todos-los-endpoints)
9. [El agente IA — PicoClaw + Claude](#9-el-agente-ia--picoclaw--claude)
10. [Dashboard web — especificación UI](#10-dashboard-web--especificación-ui)
11. [Docker Compose por cliente](#11-docker-compose-por-cliente)
12. [Templates de configuración](#12-templates-de-configuración)
13. [Flujo completo de una venta](#13-flujo-completo-de-una-venta)
14. [Modelo de negocio y precios](#14-modelo-de-negocio-y-precios)
15. [Roadmap de implementación](#15-roadmap-de-implementación)
16. [Reglas de desarrollo obligatorias](#16-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:
1. **Precios aproximados** ("creo que está como $800...") → desconfianza del cliente
2. **Tiempo perdido** buscando productos en carpetas o cuadernos → el cliente se va
3. **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:
1. Vendedor escribe: `"tenés biromes bic azul?"`
2. El bot busca en el stock → encuentra: precio $850, stock 23 unidades, variantes azul/rojo/negro
3. Bot responde: `"Sí, Bic azul a $850 (quedan 23). También hay roja y negra al mismo precio."`
4. Bot pregunta: `"¿Se concretó la venta? ¿Cuántas unidades?"`
5. Vendedor: `"2"`
6. 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`:
```nginx
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:
```sql
-- ═══════════════════════════════════════════════════════
-- 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 `sqlite3` de la stdlib de Python directamente o `aiosqlite` para las queries async. Simple y sin magia.
- **El campo `variantes`** es un JSON string. Ejemplo: `'{"color": ["azul", "rojo", "negro"], "talle": ["M", "L", "XL"]}'`. El backend lo parsea con `json.loads()`.
- **El campo `precio_vendido` en 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`
```python
# 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):**
1. Búsqueda exacta por nombre (LIKE `%q%`)
2. Búsqueda por palabras clave individuales (split por espacios, AND de todos los términos)
3. Si hay menos de 3 resultados: búsqueda OR (al menos una palabra coincide)
4. Solo devolver productos con `activo = 1`
**Respuesta exitosa (200):**
```json
{
"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:**
```json
{
"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):**
```json
{
"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:**
```json
{"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):**
```json
{
"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 `.xlsx` o `.csv`
- `modo`: `"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):**
- `nombre` o `producto` o `descripcion` → campo `nombre` (REQUERIDO)
- `precio` o `precio_venta` o `price` → campo `precio` (REQUERIDO)
- `stock` o `cantidad` o `qty` → campo `stock` (default: 0 si no está)
- `categoria` o `rubro` o `category` → campo `categoria` (default: "general")
- `marca` o `brand` → campo `marca` (opcional)
- `codigo` o `ean` o `barcode` → campo `codigo` (opcional)
**Respuesta (200):**
```json
{
"importados": 245,
"actualizados": 12,
"errores": 0,
"errores_detalle": []
}
```
**Respuesta con errores (200, pero con detalle de errores):**
```json
{
"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):**
```json
{
"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:**
```json
{
"producto_id": 42,
"cantidad": 2,
"precio_vendido": 850.00,
"vendedor": "María"
}
```
**Validaciones antes de ejecutar:**
1. El producto existe y está activo
2. `cantidad > 0`
3. `stock actual >= cantidad` (si el stock quedaría negativo, devolver error)
4. Si hay promo activa aplicable, usar ese precio (pero respetar el precio_vendido enviado si difiere)
**Respuesta (200):**
```json
{
"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):**
```json
{
"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:00
- `fecha_hasta`: ISO date string. Default: ahora
- `producto_id`: filtrar por producto
- `vendedor`: filtrar por vendedor
- `limit`: default 100, max 1000
- `offset`: para paginación
**Respuesta (200):**
```json
{
"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):**
```json
{
"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:**
```json
{
"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 neutros
- `null` o ausente: sin preferencia de género
**Notas sobre `edad`:**
- `<= 6 años`: primaria inicial → lápices de colores grandes, plastilina, sin tijeras de punta
- `7-10 años`: primaria general → juego de geometría, cuadernos doble raya, colores largos
- `11-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:**
1. Filtrar productos por categorías solicitadas y con stock > 0
2. Ordenar por "esencialidad" (el backend tiene una lista hardcodeada de productos esenciales por edad)
3. Ir sumando productos desde los más esenciales hasta llegar al 95% del presupuesto
4. Si el presupuesto no alcanza para un producto esencial, marcarlo como "no incluido" pero mostrarlo igual
**Respuesta (200):**
```json
{
"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):**
```json
{
"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:**
```json
{
"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:**
```json
{ "msg": "texto del vendedor", "session_id": "abc123" }
```
**Formato de mensajes del backend al frontend:**
```json
{ "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_id` como 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`)
```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:
```json
{
"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.
```yaml
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`)
```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
```bash
# 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`)
```nginx
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
1. **La API key de Anthropic NUNCA se hardcodea.** Siempre desde variable de entorno.
2. **Nunca logear** la contraseña sudo, la API key ni las contraseñas de admin.
3. **Cada cliente tiene su propio contenedor y su propia DB.** Nunca compartir conexión de DB entre clientes.
4. **Todas las rutas de admin** deben requerir autenticación. Nunca dejar una ruta admin sin protección.
### Datos
5. **El campo `precio_vendido` en ventas** es siempre el precio real de la venta, no una FK al precio actual del producto.
6. **Nunca hacer DELETE en la tabla productos.** Usar `activo = 0` para desactivar.
7. **Todos los timestamps** en formato ISO 8601 UTC: `datetime('now')` en SQLite.
### Performance
8. **El endpoint `/stock/search`** debe responder en menos de 200ms. Si tarda más, optimizar los índices.
9. **El WebSocket** no debe bloquear. Usar `asyncio` correctamente. Nunca llamadas síncronas dentro de handlers async.
### Código
10. **Usar tipado estricto de Python** con Pydantic para todos los bodies de request y response.
11. **Nunca usar `*` en imports** (no `from fastapi import *`).
12. **Cada endpoint** debe tener docstring con descripción, parámetros y qué devuelve.
13. **Los errores de validación** devuelven siempre JSON con el campo `detail` descriptivo en español.
14. **Tests:** al menos un test de integración por endpoint crítico (`/stock/search`, `/venta/confirmar`).
### UX
15. **Los mensajes de error** que ve el vendedor deben estar en español coloquial, nunca mostrar stack traces.
16. **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*