diff --git a/specs/01_PYMESBOT_PROJECT_SPEC.md b/specs/01_PYMESBOT_PROJECT_SPEC.md new file mode 100644 index 0000000..0fb35ac --- /dev/null +++ b/specs/01_PYMESBOT_PROJECT_SPEC.md @@ -0,0 +1,1262 @@ +# 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*