# 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*