Initial commit: PymesBot Demo with IA integration

- FastAPI backend with WebSocket chat
- SQLite database for products
- Z.AI (GLM-4.7) integration for AI responses
- Docker deployment ready
- Caddy proxy configuration
This commit is contained in:
Renato
2026-02-15 17:07:39 +01:00
commit 47264049e6
14 changed files with 4585 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
__pycache__/
*.pyc
*.pyo
.env
*.db
data/
*.log
.DS_Store

1262
01_PYMESBOT_PROJECT_SPEC.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

176
README.md Normal file
View File

@@ -0,0 +1,176 @@
# PymesBot Demo - Asistente de Ventas con IA
Demo funcional de asistente de ventas para librería con integración de IA.
## Características
- Chat en tiempo real con WebSocket
- Búsqueda de productos en base de datos SQLite
- Confirmación de ventas
- IA integrada con Z.AI (GLM-4.7)
- Interfaz web responsive
## Tech Stack
- **Backend**: FastAPI + Python 3.11
- **Base de datos**: SQLite
- **Frontend**: HTML/JS vanilla
- **IA**: Z.AI API (GLM-4.7)
- **Proxy**: Caddy
## Estructura
```
pymesbot/
├── backend/
│ ├── main.py # FastAPI app
│ ├── Dockerfile # Imagen Docker
│ ├── requirements.txt # Dependencias
│ └── templates/
│ └── chat.html # Interfaz
└── data/ # Base de datos SQLite
```
## Deployment
### Prerrequisitos
- Docker y Docker Compose
- Acceso a API de Z.AI (u otro provider)
- Servidor con Caddy (u otro proxy)
### Variables de Entorno
```bash
# API de Z.AI (requerido)
ZAI_API_KEY=tu_api_key_aqui
ANTHROPIC_MODEL=glm-4.7
```
### Paso 1: Clonar el repositorio
```bash
git clone https://gitea.cbcren.online/renato97/demo.git
cd demo
```
### Paso 2: Configurar variables de entorno
Crear archivo `.env`:
```bash
ZAI_API_KEY=tu_api_key
```
### Paso 3: Iniciar contenedores
```bash
docker compose up -d --build
```
### Paso 4: Configurar Caddy (proxy)
Agregar al Caddyfile:
```caddy
demo.tudominio.com {
reverse_proxy localhost:8201
}
```
Reiniciar Caddy:
```bash
docker exec caddy-ingress caddy reload
```
## API Endpoints
| Método | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/health` | Health check |
| GET | `/stock/search?q=producto` | Buscar productos |
| POST | `/venta/confirmar` | Confirmar venta |
| GET | `/stats/ventas?periodo=hoy` | Estadísticas |
| WS | `/chat/ws/{session_id}` | Chat en tiempo real |
### Ejemplo de búsqueda de productos
```bash
curl "http://localhost:8201/stock/search?q=birome"
```
### Ejemplo de confirmar venta
```bash
curl -X POST "http://localhost:8201/venta/confirmar" \
-H "Content-Type: application/json" \
-d '{
"producto_id": 1,
"cantidad": 2,
"precio_vendido": 850,
"vendedor": "test"
}'
```
## Credenciales
- **PIN vendedor**: 1234
- **Admin**: /admin (password: admin123)
## Configuración de IA
El sistema usa la API de Z.AI directamente. Para cambiar el modelo o API:
Editar `main.py` línea ~40:
```python
ZAI_API_KEY = "tu_api_key"
ZAI_API_URL = "https://api.z.ai/api/anthropic/v1"
MODEL = "glm-4.7"
```
## Known Issues / Bugs
### 1. Búsqueda limitada
- **Problema**: La búsqueda en DB solo soporta términos simples
- **Solución**: Usar la IA para búsquedas más complejas
### 2. Plurales no detectados
- **Problema**: "lápices" no encuentra "lápiz"
- **Workaround**: El sistema ahora tiene mapeo de plurales básico
### 3. Timeout en WebSocket
- **Problema**: La conexión puede cerrarse por inactividad
- **Solución**: El frontend reconecta automáticamente
### 4. Scope operator.write en OpenClaw
- **Problema**: OpenClaw Gateway requiere scopes específicos para usar el agente
- **Solución actual**: Se usa la API de Z.AI directamente en lugar de OpenClaw Gateway
- **Nota**: Para usar OpenClaw Gateway, configurar con `scopes: ["operator.read", "operator.write", "agent"]`
### 5. PicoClaw no conecta
- **Problema**: PicoClaw no reconoce modelos MiniMax
- **Solución**: Usar OpenClaw o API directa
## Mantenimiento
### Ver logs
```bash
docker logs pygmesbot_backend
```
### Reiniciar servicio
```bash
docker restart pygmesbot_backend
```
### Ver productos en DB
```bash
docker exec pygmesbot_backend python3 -c "
import sqlite3
conn = sqlite3.connect('/app/data/stock.db')
for row in conn.execute('SELECT nombre, stock FROM productos LIMIT 5'):
print(row)
"
```
##Licencia
MIT

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
version: '3.8'
services:
backend:
build:
context: ./pymesbot/backend
dockerfile: Dockerfile
container_name: pymesbot_backend
restart: unless-stopped
ports:
- "8201:8000"
volumes:
- ./pymesbot/data:/app/data
environment:
- OPENCLAW_TOKEN=wlillidan1-demo-token-12345
- OPENCLAW_WS_URL=ws://openclaw_gateway:18789
networks:
- default
- caddy
- openclaw_net
networks:
default:
name: pygmesbot_demo_net
external: false
caddy:
external: true
openclaw_net:
name: openclaw-config_pymesbot_net
external: true

52
inventario_ejemplo.csv Normal file
View File

@@ -0,0 +1,52 @@
nombre,marca,categoria,precio,stock,variantes,codigo
Birome Bic Cristal,Bic,escritura,850.0,45,"{""color"": [""azul"", ""rojo"", ""negro"", ""verde""]}",7501031311309
Birome Bic Cristal Fine,Bic,escritura,920.0,32,"{""color"": [""azul"", ""negro""]}",7501031311408
Birome Faber Castell Trilux,Faber Castell,escritura,780.0,28,"{""color"": [""azul"", ""rojo"", ""negro""]}",7891360302456
Lápiz Grafito HB,Faber Castell,escritura,350.0,120,"{""talle"": [""standard"", ""jumbo""]}",7891360301121
Marcador Permanente Sharpie,Sharpie,escritura,1200.0,18,"{""color"": [""negro"", ""azul"", ""rojo""]}",071641005024
Corrector Líquido Paper Mate,Paper Mate,escritura,650.0,24,,071641023456
Goma de Borrar Staedtler,Staedtler,escritura,480.0,56,,4007817331515
Marcadores de Colores,Faber Castell,escritura,2800.0,15,"{""cantidad"": [""12 colores"", ""24 colores""]}",7891360306782
Lápices de Colores Largos,Maped,escritura,1850.0,22,"{""cantidad"": [""12 unidades"", ""24 unidades""]}",3154140256123
Resaltadores Pastel,Stabilo,escritura,1450.0,19,"{""cantidad"": [""6 unidades"", ""8 unidades""]}",4006381567890
Cuaderno Rivadavia A4 Rayado,Rivadavia,cuadernos,2500.0,80,"{""color"": [""azul"", ""rojo"", ""negro""]}",7798021234567
Cuaderno Rivadavia A4 Cuadriculado,Rivadavia,cuadernos,2500.0,75,"{""color"": [""azul"", ""verde""]}",7798021234574
Cuaderno ABC A4 48h,ABC,cuadernos,1800.0,95,"{""tipo"": [""rayado"", ""cuadriculado""]}",7796543210001
Cuaderno Tilibra 80h,Tilibra,cuadernos,3200.0,40,"{""tipo"": [""rayado"", ""cuadriculado"", ""blanco""]}",7891023345678
Libreta Anotador Chico,Sin Marca,cuadernos,450.0,200,,
Block de Dibujo A4,Filgo,cuadernos,2200.0,30,"{""hojas"": [""20 hojas"", ""40 hojas""]}",7796578912345
Papel A4 Resma (500h),Chamex,cuadernos,8500.0,12,,7896321456987
Papel A4 Resma (500h),Catalyst,cuadernos,7900.0,15,,7897412589632
Carpeta A4 2 Anillos,Genérica,cuadernos,1200.0,45,"{""color"": [""azul"", ""rojo"", ""verde"", ""negro""]}",
Separadores A4,Genéricos,cuadernos,650.0,38,,
Regla 30cm Plástica,Maped,geometria,650.0,50,"{""color"": [""transparente"", ""azul"", ""rosa""]}",3154140151234
Regla 20cm Metálica,Maped,geometria,1200.0,25,,3154140151241
Escuadra 45° + Cartabón,Maped,geometria,1850.0,30,,3154140256789
Compás Metálico con Tiralíneas,Maped,geometria,3500.0,15,,3154140351234
Compás Escolar Plástico,Genérico,geometria,850.0,40,,
Transportador 180°,Maped,geometria,480.0,35,,3154140451234
Juego de Geometría Escolar,Maped,geometria,2800.0,20,,3154140551234
Lapicera Tiralíneas,Rotring,geometria,4200.0,8,"{""tamaño"": [""0.3mm"", ""0.5mm"", ""0.7mm""]}",3501179256123
Tijera Escolar Punta Roma,Maped,geometria,950.0,42,,3154140651234
Tijera Oficina 21cm,Maped,geometria,1850.0,18,,3154140751234
Plastilina 12 Colores,Faber Castell,colores,2800.0,25,,7891360309875
Plastilina 24 Colores,Faber Castell,colores,4500.0,12,,7891360309882
Temperas 6 Colores,Alba,colores,1800.0,35,"{""tamaño"": [""12ml"", ""25ml""]}",7798029876543
Temperas 12 Colores,Alba,colores,3200.0,20,"{""tamaño"": [""12ml"", ""25ml""]}",7798029876550
Pinceles Set x3,Alba,colores,950.0,40,"{""tamaño"": [""n°4, n°6, n°8"", ""n°6, n°8, n°10""]}",7798029876567
Pinceles Set x5,Alba,colores,1450.0,28,,7798029876574
Papel Glacé 10h,Filgo,colores,650.0,60,,7796578912346
Papel Crepe,Filgo,colores,450.0,80,"{""color"": [""rojo"", ""azul"", ""amarillo"", ""verde"", ""blanco"", ""negro"", ""rosa""]}",7796578912347
Cartulina 50x70,Filgo,colores,180.0,150,"{""color"": [""blanco"", ""negro"", ""rojo"", ""azul"", ""verde"", ""amarillo""]}",7796578912348
Fibras Maped 12 unidades,Maped,colores,1950.0,32,,3154140851234
Fibras Maped 24 unidades,Maped,colores,3200.0,18,,3154140951234
Grapadora Chica,Staples,accesorios,1200.0,22,,0071641098765
Grapas Caja x1000,Genéricas,accesorios,350.0,85,,
Sacagrapas,Genérico,accesorios,180.0,50,,
Engrapadora Oficina,Rapid,accesorios,2500.0,12,,7313460156123
Cinta Adhesiva 18mm,Scotch,accesorios,450.0,70,"{""tipo"": [""transparente"", ""papel marrón""]}",0511319876543
Cinta Adhesiva Doble Faz,Scotch,accesorios,850.0,35,,0511319876544
Pegamento Barra,Pritt,accesorios,680.0,55,"{""tamaño"": [""11g"", ""22g""]}",0791469876543
Pegamento Líquido Escolar,Pritt,accesorios,520.0,48,,0791469876544
Silicona Líquida 30ml,Genérica,accesorios,380.0,60,,
Lápices de Cera Gruesos,Filgo,colores,1600.0,28,"{""cantidad"": [""12 colores"", ""24 colores""]}",7796578912355
1 nombre marca categoria precio stock variantes codigo
2 Birome Bic Cristal Bic escritura 850.0 45 {"color": ["azul", "rojo", "negro", "verde"]} 7501031311309
3 Birome Bic Cristal Fine Bic escritura 920.0 32 {"color": ["azul", "negro"]} 7501031311408
4 Birome Faber Castell Trilux Faber Castell escritura 780.0 28 {"color": ["azul", "rojo", "negro"]} 7891360302456
5 Lápiz Grafito HB Faber Castell escritura 350.0 120 {"talle": ["standard", "jumbo"]} 7891360301121
6 Marcador Permanente Sharpie Sharpie escritura 1200.0 18 {"color": ["negro", "azul", "rojo"]} 071641005024
7 Corrector Líquido Paper Mate Paper Mate escritura 650.0 24 071641023456
8 Goma de Borrar Staedtler Staedtler escritura 480.0 56 4007817331515
9 Marcadores de Colores Faber Castell escritura 2800.0 15 {"cantidad": ["12 colores", "24 colores"]} 7891360306782
10 Lápices de Colores Largos Maped escritura 1850.0 22 {"cantidad": ["12 unidades", "24 unidades"]} 3154140256123
11 Resaltadores Pastel Stabilo escritura 1450.0 19 {"cantidad": ["6 unidades", "8 unidades"]} 4006381567890
12 Cuaderno Rivadavia A4 Rayado Rivadavia cuadernos 2500.0 80 {"color": ["azul", "rojo", "negro"]} 7798021234567
13 Cuaderno Rivadavia A4 Cuadriculado Rivadavia cuadernos 2500.0 75 {"color": ["azul", "verde"]} 7798021234574
14 Cuaderno ABC A4 48h ABC cuadernos 1800.0 95 {"tipo": ["rayado", "cuadriculado"]} 7796543210001
15 Cuaderno Tilibra 80h Tilibra cuadernos 3200.0 40 {"tipo": ["rayado", "cuadriculado", "blanco"]} 7891023345678
16 Libreta Anotador Chico Sin Marca cuadernos 450.0 200
17 Block de Dibujo A4 Filgo cuadernos 2200.0 30 {"hojas": ["20 hojas", "40 hojas"]} 7796578912345
18 Papel A4 Resma (500h) Chamex cuadernos 8500.0 12 7896321456987
19 Papel A4 Resma (500h) Catalyst cuadernos 7900.0 15 7897412589632
20 Carpeta A4 2 Anillos Genérica cuadernos 1200.0 45 {"color": ["azul", "rojo", "verde", "negro"]}
21 Separadores A4 Genéricos cuadernos 650.0 38
22 Regla 30cm Plástica Maped geometria 650.0 50 {"color": ["transparente", "azul", "rosa"]} 3154140151234
23 Regla 20cm Metálica Maped geometria 1200.0 25 3154140151241
24 Escuadra 45° + Cartabón Maped geometria 1850.0 30 3154140256789
25 Compás Metálico con Tiralíneas Maped geometria 3500.0 15 3154140351234
26 Compás Escolar Plástico Genérico geometria 850.0 40
27 Transportador 180° Maped geometria 480.0 35 3154140451234
28 Juego de Geometría Escolar Maped geometria 2800.0 20 3154140551234
29 Lapicera Tiralíneas Rotring geometria 4200.0 8 {"tamaño": ["0.3mm", "0.5mm", "0.7mm"]} 3501179256123
30 Tijera Escolar Punta Roma Maped geometria 950.0 42 3154140651234
31 Tijera Oficina 21cm Maped geometria 1850.0 18 3154140751234
32 Plastilina 12 Colores Faber Castell colores 2800.0 25 7891360309875
33 Plastilina 24 Colores Faber Castell colores 4500.0 12 7891360309882
34 Temperas 6 Colores Alba colores 1800.0 35 {"tamaño": ["12ml", "25ml"]} 7798029876543
35 Temperas 12 Colores Alba colores 3200.0 20 {"tamaño": ["12ml", "25ml"]} 7798029876550
36 Pinceles Set x3 Alba colores 950.0 40 {"tamaño": ["n°4, n°6, n°8", "n°6, n°8, n°10"]} 7798029876567
37 Pinceles Set x5 Alba colores 1450.0 28 7798029876574
38 Papel Glacé 10h Filgo colores 650.0 60 7796578912346
39 Papel Crepe Filgo colores 450.0 80 {"color": ["rojo", "azul", "amarillo", "verde", "blanco", "negro", "rosa"]} 7796578912347
40 Cartulina 50x70 Filgo colores 180.0 150 {"color": ["blanco", "negro", "rojo", "azul", "verde", "amarillo"]} 7796578912348
41 Fibras Maped 12 unidades Maped colores 1950.0 32 3154140851234
42 Fibras Maped 24 unidades Maped colores 3200.0 18 3154140951234
43 Grapadora Chica Staples accesorios 1200.0 22 0071641098765
44 Grapas Caja x1000 Genéricas accesorios 350.0 85
45 Sacagrapas Genérico accesorios 180.0 50
46 Engrapadora Oficina Rapid accesorios 2500.0 12 7313460156123
47 Cinta Adhesiva 18mm Scotch accesorios 450.0 70 {"tipo": ["transparente", "papel marrón"]} 0511319876543
48 Cinta Adhesiva Doble Faz Scotch accesorios 850.0 35 0511319876544
49 Pegamento Barra Pritt accesorios 680.0 55 {"tamaño": ["11g", "22g"]} 0791469876543
50 Pegamento Líquido Escolar Pritt accesorios 520.0 48 0791469876544
51 Silicona Líquida 30ml Genérica accesorios 380.0 60
52 Lápices de Cera Gruesos Filgo colores 1600.0 28 {"cantidad": ["12 colores", "24 colores"]} 7796578912355

508
inventario_ejemplo.html Normal file
View File

@@ -0,0 +1,508 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Inventario Librería El Rincón del Saber</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
h2 { color: #34495e; margin-top: 30px; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th { background-color: #3498db; color: white; padding: 12px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #ddd; }
tr:nth-child(even) { background-color: #f2f2f2; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ccc;
font-size: 12px; color: #666; text-align: center; }
.category-header { background-color: #ecf0f1; font-weight: bold; }
.price { text-align: right; }
.stock { text-align: center; }
.total { font-weight: bold; background-color: #e8f6f3; }
</style>
</head>
<body>
<h1>📚 Librería El Rincón del Saber</h1>
<h2>Inventario de Productos - Febrero 2026</h2>
<p><strong>Total de productos:</strong> 51 | <strong>Valor total aproximado:</strong> $225,080</p>
<table>
<thead>
<tr>
<th>Producto</th>
<th>Marca</th>
<th>Categoría</th>
<th class="price">Precio</th>
<th class="stock">Stock</th>
<th>Código</th>
</tr>
</thead>
<tbody>
<tr class="category-header"><td colspan="6">📁 ESCRITURA (10 productos)</td></tr>
<tr>
<td>Birome Bic Cristal</td>
<td>Bic</td>
<td>Escritura</td>
<td class="price">$850.00</td>
<td class="stock">45</td>
<td>7501031311309</td>
</tr>
<tr>
<td>Birome Bic Cristal Fine</td>
<td>Bic</td>
<td>Escritura</td>
<td class="price">$920.00</td>
<td class="stock">32</td>
<td>7501031311408</td>
</tr>
<tr>
<td>Birome Faber Castell Trilux</td>
<td>Faber Castell</td>
<td>Escritura</td>
<td class="price">$780.00</td>
<td class="stock">28</td>
<td>7891360302456</td>
</tr>
<tr>
<td>Lápiz Grafito HB</td>
<td>Faber Castell</td>
<td>Escritura</td>
<td class="price">$350.00</td>
<td class="stock">120</td>
<td>7891360301121</td>
</tr>
<tr>
<td>Marcador Permanente Sharpie</td>
<td>Sharpie</td>
<td>Escritura</td>
<td class="price">$1,200.00</td>
<td class="stock">18</td>
<td>071641005024</td>
</tr>
<tr>
<td>Corrector Líquido Paper Mate</td>
<td>Paper Mate</td>
<td>Escritura</td>
<td class="price">$650.00</td>
<td class="stock">24</td>
<td>071641023456</td>
</tr>
<tr>
<td>Goma de Borrar Staedtler</td>
<td>Staedtler</td>
<td>Escritura</td>
<td class="price">$480.00</td>
<td class="stock">56</td>
<td>4007817331515</td>
</tr>
<tr>
<td>Marcadores de Colores</td>
<td>Faber Castell</td>
<td>Escritura</td>
<td class="price">$2,800.00</td>
<td class="stock">15</td>
<td>7891360306782</td>
</tr>
<tr>
<td>Lápices de Colores Largos</td>
<td>Maped</td>
<td>Escritura</td>
<td class="price">$1,850.00</td>
<td class="stock">22</td>
<td>3154140256123</td>
</tr>
<tr>
<td>Resaltadores Pastel</td>
<td>Stabilo</td>
<td>Escritura</td>
<td class="price">$1,450.00</td>
<td class="stock">19</td>
<td>4006381567890</td>
</tr>
<tr class="category-header"><td colspan="6">📁 CUADERNOS (10 productos)</td></tr>
<tr>
<td>Cuaderno Rivadavia A4 Rayado</td>
<td>Rivadavia</td>
<td>Cuadernos</td>
<td class="price">$2,500.00</td>
<td class="stock">80</td>
<td>7798021234567</td>
</tr>
<tr>
<td>Cuaderno Rivadavia A4 Cuadriculado</td>
<td>Rivadavia</td>
<td>Cuadernos</td>
<td class="price">$2,500.00</td>
<td class="stock">75</td>
<td>7798021234574</td>
</tr>
<tr>
<td>Cuaderno ABC A4 48h</td>
<td>ABC</td>
<td>Cuadernos</td>
<td class="price">$1,800.00</td>
<td class="stock">95</td>
<td>7796543210001</td>
</tr>
<tr>
<td>Cuaderno Tilibra 80h</td>
<td>Tilibra</td>
<td>Cuadernos</td>
<td class="price">$3,200.00</td>
<td class="stock">40</td>
<td>7891023345678</td>
</tr>
<tr>
<td>Libreta Anotador Chico</td>
<td>Sin Marca</td>
<td>Cuadernos</td>
<td class="price">$450.00</td>
<td class="stock">200</td>
<td>-</td>
</tr>
<tr>
<td>Block de Dibujo A4</td>
<td>Filgo</td>
<td>Cuadernos</td>
<td class="price">$2,200.00</td>
<td class="stock">30</td>
<td>7796578912345</td>
</tr>
<tr>
<td>Papel A4 Resma 500h (Chamex)</td>
<td>Chamex</td>
<td>Cuadernos</td>
<td class="price">$8,500.00</td>
<td class="stock">12</td>
<td>7896321456987</td>
</tr>
<tr>
<td>Papel A4 Resma 500h (Catalyst)</td>
<td>Catalyst</td>
<td>Cuadernos</td>
<td class="price">$7,900.00</td>
<td class="stock">15</td>
<td>7897412589632</td>
</tr>
<tr>
<td>Carpeta A4 2 Anillos</td>
<td>Genérica</td>
<td>Cuadernos</td>
<td class="price">$1,200.00</td>
<td class="stock">45</td>
<td>-</td>
</tr>
<tr>
<td>Separadores A4</td>
<td>Genéricos</td>
<td>Cuadernos</td>
<td class="price">$650.00</td>
<td class="stock">38</td>
<td>-</td>
</tr>
<tr class="category-header"><td colspan="6">📁 GEOMETRÍA (10 productos)</td></tr>
<tr>
<td>Regla 30cm Plástica</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$650.00</td>
<td class="stock">50</td>
<td>3154140151234</td>
</tr>
<tr>
<td>Regla 20cm Metálica</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$1,200.00</td>
<td class="stock">25</td>
<td>3154140151241</td>
</tr>
<tr>
<td>Escuadra 45° + Cartabón</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$1,850.00</td>
<td class="stock">30</td>
<td>3154140256789</td>
</tr>
<tr>
<td>Compás Metálico c/ Tiralíneas</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$3,500.00</td>
<td class="stock">15</td>
<td>3154140351234</td>
</tr>
<tr>
<td>Compás Escolar Plástico</td>
<td>Genérico</td>
<td>Geometría</td>
<td class="price">$850.00</td>
<td class="stock">40</td>
<td>-</td>
</tr>
<tr>
<td>Transportador 180°</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$480.00</td>
<td class="stock">35</td>
<td>3154140451234</td>
</tr>
<tr>
<td>Juego de Geometría Escolar</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$2,800.00</td>
<td class="stock">20</td>
<td>3154140551234</td>
</tr>
<tr>
<td>Lapicera Tiralíneas</td>
<td>Rotring</td>
<td>Geometría</td>
<td class="price">$4,200.00</td>
<td class="stock">8</td>
<td>3501179256123</td>
</tr>
<tr>
<td>Tijera Escolar Punta Roma</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$950.00</td>
<td class="stock">42</td>
<td>3154140651234</td>
</tr>
<tr>
<td>Tijera Oficina 21cm</td>
<td>Maped</td>
<td>Geometría</td>
<td class="price">$1,850.00</td>
<td class="stock">18</td>
<td>3154140751234</td>
</tr>
<tr class="category-header"><td colspan="6">📁 ARTE Y COLORES (11 productos)</td></tr>
<tr>
<td>Plastilina 12 Colores</td>
<td>Faber Castell</td>
<td>Arte Y Colores</td>
<td class="price">$2,800.00</td>
<td class="stock">25</td>
<td>7891360309875</td>
</tr>
<tr>
<td>Plastilina 24 Colores</td>
<td>Faber Castell</td>
<td>Arte Y Colores</td>
<td class="price">$4,500.00</td>
<td class="stock">12</td>
<td>7891360309882</td>
</tr>
<tr>
<td>Temperas 6 Colores</td>
<td>Alba</td>
<td>Arte Y Colores</td>
<td class="price">$1,800.00</td>
<td class="stock">35</td>
<td>7798029876543</td>
</tr>
<tr>
<td>Temperas 12 Colores</td>
<td>Alba</td>
<td>Arte Y Colores</td>
<td class="price">$3,200.00</td>
<td class="stock">20</td>
<td>7798029876550</td>
</tr>
<tr>
<td>Pinceles Set x3</td>
<td>Alba</td>
<td>Arte Y Colores</td>
<td class="price">$950.00</td>
<td class="stock">40</td>
<td>7798029876567</td>
</tr>
<tr>
<td>Pinceles Set x5</td>
<td>Alba</td>
<td>Arte Y Colores</td>
<td class="price">$1,450.00</td>
<td class="stock">28</td>
<td>7798029876574</td>
</tr>
<tr>
<td>Papel Glacé 10h</td>
<td>Filgo</td>
<td>Arte Y Colores</td>
<td class="price">$650.00</td>
<td class="stock">60</td>
<td>7796578912346</td>
</tr>
<tr>
<td>Papel Crepe</td>
<td>Filgo</td>
<td>Arte Y Colores</td>
<td class="price">$450.00</td>
<td class="stock">80</td>
<td>7796578912347</td>
</tr>
<tr>
<td>Cartulina 50x70</td>
<td>Filgo</td>
<td>Arte Y Colores</td>
<td class="price">$180.00</td>
<td class="stock">150</td>
<td>7796578912348</td>
</tr>
<tr>
<td>Fibras Maped 12 unidades</td>
<td>Maped</td>
<td>Arte Y Colores</td>
<td class="price">$1,950.00</td>
<td class="stock">32</td>
<td>3154140851234</td>
</tr>
<tr>
<td>Fibras Maped 24 unidades</td>
<td>Maped</td>
<td>Arte Y Colores</td>
<td class="price">$3,200.00</td>
<td class="stock">18</td>
<td>3154140951234</td>
</tr>
<tr class="category-header"><td colspan="6">📁 ACCESORIOS (10 productos)</td></tr>
<tr>
<td>Grapadora Chica</td>
<td>Staples</td>
<td>Accesorios</td>
<td class="price">$1,200.00</td>
<td class="stock">22</td>
<td>0071641098765</td>
</tr>
<tr>
<td>Grapas Caja x1000</td>
<td>Genéricas</td>
<td>Accesorios</td>
<td class="price">$350.00</td>
<td class="stock">85</td>
<td>-</td>
</tr>
<tr>
<td>Sacagrapas</td>
<td>Genérico</td>
<td>Accesorios</td>
<td class="price">$180.00</td>
<td class="stock">50</td>
<td>-</td>
</tr>
<tr>
<td>Engrapadora Oficina</td>
<td>Rapid</td>
<td>Accesorios</td>
<td class="price">$2,500.00</td>
<td class="stock">12</td>
<td>7313460156123</td>
</tr>
<tr>
<td>Cinta Adhesiva 18mm</td>
<td>Scotch</td>
<td>Accesorios</td>
<td class="price">$450.00</td>
<td class="stock">70</td>
<td>0511319876543</td>
</tr>
<tr>
<td>Cinta Adhesiva Doble Faz</td>
<td>Scotch</td>
<td>Accesorios</td>
<td class="price">$850.00</td>
<td class="stock">35</td>
<td>0511319876544</td>
</tr>
<tr>
<td>Pegamento Barra</td>
<td>Pritt</td>
<td>Accesorios</td>
<td class="price">$680.00</td>
<td class="stock">55</td>
<td>0791469876543</td>
</tr>
<tr>
<td>Pegamento Líquido Escolar</td>
<td>Pritt</td>
<td>Accesorios</td>
<td class="price">$520.00</td>
<td class="stock">48</td>
<td>0791469876544</td>
</tr>
<tr>
<td>Silicona Líquida 30ml</td>
<td>Genérica</td>
<td>Accesorios</td>
<td class="price">$380.00</td>
<td class="stock">60</td>
<td>-</td>
</tr>
<tr>
<td>Lápices de Cera Gruesos</td>
<td>Filgo</td>
<td>Accesorios</td>
<td class="price">$1,600.00</td>
<td class="stock">28</td>
<td>7796578912355</td>
</tr>
</tbody>
</table>
<div class="footer">
<p><strong>Documento generado para PymesBot</strong></p>
<p>Este inventario puede importarse al sistema mediante la función de importación de Excel/CSV</p>
<p>Fecha de generación: Febrero 2026 | Total: 51 productos</p>
</div>
</body>
</html>

99
inventario_ejemplo.sql Normal file
View File

@@ -0,0 +1,99 @@
-- Inventario de ejemplo para PymesBot
-- Este es un archivo SQL que puede importarse a la base de datos SQLite
-- Ejecutar: sqlite3 stock.db < inventario_ejemplo.sql
-- ═══════════════════════════════════════════════════════
-- DATOS DE EJEMPLO: Librería "El Rincón del Saber"
-- ═══════════════════════════════════════════════════════
-- Productos de escritura
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
('Birome Bic Cristal', 'Bic', 'escritura', 850.00, 45, '{"color": ["azul", "rojo", "negro", "verde"]}', '7501031311309'),
('Birome Bic Cristal Fine', 'Bic', 'escritura', 920.00, 32, '{"color": ["azul", "negro"]}', '7501031311408'),
('Birome Faber Castell Trilux', 'Faber Castell', 'escritura', 780.00, 28, '{"color": ["azul", "rojo", "negro"]}', '7891360302456'),
('Lápiz Grafito HB', 'Faber Castell', 'escritura', 350.00, 120, '{"talle": ["standard", "jumbo"]}', '7891360301121'),
('Marcador Permanente Sharpie', 'Sharpie', 'escritura', 1200.00, 18, '{"color": ["negro", "azul", "rojo"]}', '071641005024'),
('Corrector Líquido Paper Mate', 'Paper Mate', 'escritura', 650.00, 24, NULL, '071641023456'),
('Goma de Borrar Staedtler', 'Staedtler', 'escritura', 480.00, 56, NULL, '4007817331515'),
('Marcadores de Colores', 'Faber Castell', 'escritura', 2800.00, 15, '{"cantidad": ["12 colores", "24 colores"]}', '7891360306782'),
('Lápices de Colores Largos', 'Maped', 'escritura', 1850.00, 22, '{"cantidad": ["12 unidades", "24 unidades"]}', '3154140256123'),
('Resaltadores Pastel', 'Stabilo', 'escritura', 1450.00, 19, '{"cantidad": ["6 unidades", "8 unidades"]}', '4006381567890');
-- Cuadernos y papel
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
('Cuaderno Rivadavia A4 Rayado', 'Rivadavia', 'cuadernos', 2500.00, 80, '{"color": ["azul", "rojo", "negro"]}', '7798021234567'),
('Cuaderno Rivadavia A4 Cuadriculado', 'Rivadavia', 'cuadernos', 2500.00, 75, '{"color": ["azul", "verde"]}', '7798021234574'),
('Cuaderno ABC A4 48h', 'ABC', 'cuadernos', 1800.00, 95, '{"tipo": ["rayado", "cuadriculado"]}', '7796543210001'),
('Cuaderno Tilibra 80h', 'Tilibra', 'cuadernos', 3200.00, 40, '{"tipo": ["rayado", "cuadriculado", "blanco"]}', '7891023345678'),
('Libreta Anotador Chico', 'Sin Marca', 'cuadernos', 450.00, 200, NULL, NULL),
('Block de Dibujo A4', 'Filgo', 'cuadernos', 2200.00, 30, '{"hojas": ["20 hojas", "40 hojas"]}', '7796578912345'),
('Papel A4 Resma (500h)', 'Chamex', 'cuadernos', 8500.00, 12, NULL, '7896321456987'),
('Papel A4 Resma (500h)', 'Catalyst', 'cuadernos', 7900.00, 15, NULL, '7897412589632'),
('Carpeta A4 2 Anillos', 'Genérica', 'cuadernos', 1200.00, 45, '{"color": ["azul", "rojo", "verde", "negro"]}', NULL),
('Separadores A4', 'Genéricos', 'cuadernos', 650.00, 38, NULL, NULL);
-- Geometría
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
('Regla 30cm Plástica', 'Maped', 'geometria', 650.00, 50, '{"color": ["transparente", "azul", "rosa"]}', '3154140151234'),
('Regla 20cm Metálica', 'Maped', 'geometria', 1200.00, 25, NULL, '3154140151241'),
('Escuadra 45° + Cartabón', 'Maped', 'geometria', 1850.00, 30, NULL, '3154140256789'),
('Compás Metálico con Tiralíneas', 'Maped', 'geometria', 3500.00, 15, NULL, '3154140351234'),
('Compás Escolar Plástico', 'Genérico', 'geometria', 850.00, 40, NULL, NULL),
('Transportador 180°', 'Maped', 'geometria', 480.00, 35, NULL, '3154140451234'),
('Juego de Geometría Escolar', 'Maped', 'geometria', 2800.00, 20, NULL, '3154140551234'),
('Lapicera Tiralíneas', 'Rotring', 'geometria', 4200.00, 8, '{"tamaño": ["0.3mm", "0.5mm", "0.7mm"]}', '3501179256123'),
('Tijera Escolar Punta Roma', 'Maped', 'geometria', 950.00, 42, NULL, '3154140651234'),
('Tijera Oficina 21cm', 'Maped', 'geometria', 1850.00, 18, NULL, '3154140751234');
-- Artículos de arte y colores
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
('Plastilina 12 Colores', 'Faber Castell', 'colores', 2800.00, 25, NULL, '7891360309875'),
('Plastilina 24 Colores', 'Faber Castell', 'colores', 4500.00, 12, NULL, '7891360309882'),
('Temperas 6 Colores', 'Alba', 'colores', 1800.00, 35, '{"tamaño": ["12ml", "25ml"]}', '7798029876543'),
('Temperas 12 Colores', 'Alba', 'colores', 3200.00, 20, '{"tamaño": ["12ml", "25ml"]}', '7798029876550'),
('Pinceles Set x3', 'Alba', 'colores', 950.00, 40, '{"tamaño": ["n°4, n°6, n°8", "n°6, n°8, n°10"]}', '7798029876567'),
('Pinceles Set x5', 'Alba', 'colores', 1450.00, 28, NULL, '7798029876574'),
('Papel Glacé 10h', 'Filgo', 'colores', 650.00, 60, NULL, '7796578912346'),
('Papel Crepe', 'Filgo', 'colores', 450.00, 80, '{"color": ["rojo", "azul", "amarillo", "verde", "blanco", "negro", "rosa"]}', '7796578912347'),
('Cartulina 50x70', 'Filgo', 'colores', 180.00, 150, '{"color": ["blanco", "negro", "rojo", "azul", "verde", "amarillo"]}', '7796578912348'),
('Fibras Maped 12 unidades', 'Maped', 'colores', 1950.00, 32, NULL, '3154140851234'),
('Fibras Maped 24 unidades', 'Maped', 'colores', 3200.00, 18, NULL, '3154140951234');
-- Accesorios y otros
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
('Grapadora Chica', 'Staples', 'accesorios', 1200.00, 22, NULL, '0071641098765'),
('Grapas Caja x1000', 'Genéricas', 'accesorios', 350.00, 85, NULL, NULL),
('Sacagrapas', 'Genérico', 'accesorios', 180.00, 50, NULL, NULL),
('Engrapadora Oficina', 'Rapid', 'accesorios', 2500.00, 12, NULL, '7313460156123'),
('Cinta Adhesiva 18mm', 'Scotch', 'accesorios', 450.00, 70, '{"tipo": ["transparente", "papel marrón"]}', '0511319876543'),
('Cinta Adhesiva Doble Faz', 'Scotch', 'accesorios', 850.00, 35, NULL, '0511319876544'),
('Pegamento Barra', 'Pritt', 'accesorios', 680.00, 55, '{"tamaño": ["11g", "22g"]}', '0791469876543'),
('Pegamento Líquido Escolar', 'Pritt', 'accesorios', 520.00, 48, NULL, '0791469876544'),
('Silicona Líquida 30ml', 'Genérica', 'accesorios', 380.00, 60, NULL, NULL),
('Lápices de Cera Gruesos', 'Filgo', 'colores', 1600.00, 28, '{"cantidad": ["12 colores", "24 colores"]}', '7796578912355');
-- Configuración inicial del negocio
INSERT OR REPLACE INTO config (clave, valor) VALUES
('nombre_negocio', 'Librería El Rincón del Saber'),
('moneda', 'ARS'),
('moneda_simbolo', '$'),
('combo_categorias', '["escritura","cuadernos","geometria","colores"]'),
('alerta_stock_minimo', '5'),
('vendedor_pin', '1234'),
('rubro', 'libreria');
-- Ejemplo de promociones
INSERT INTO promociones (nombre, tipo, valor, categorias, activa, fecha_inicio, fecha_fin) VALUES
('Vuelta al Cole 15% off en Cuadernos', 'descuento_pct', 15, '["cuadernos"]', 1, '2026-02-01', '2026-03-31'),
('Día del Padre - 10% off en Escritura', 'descuento_pct', 10, '["escritura"]', 0, '2026-06-15', '2026-06-16');
-- Notas adicionales:
-- Este inventario incluye 51 productos distribuidos en 5 categorías:
-- - escritura: 10 productos
-- - cuadernos: 10 productos
-- - geometria: 10 productos
-- - colores: 11 productos
-- - accesorios: 10 productos
--
-- Para usar: importar este archivo a un cliente de prueba con:
-- sqlite3 /opt/pymesbot/{cliente}/data/stock.db < inventario_ejemplo.sql

23
inventario_ejemplo.xls Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/data /app/templates
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]

688
pymesbot/backend/main.py Normal file
View File

@@ -0,0 +1,688 @@
import asyncio
import json
import sqlite3
import os
import logging
import uuid
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from typing import Optional
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
from fastapi import (
FastAPI,
WebSocket,
WebSocketDisconnect,
HTTPException,
UploadFile,
File,
Depends,
)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel
OPENCLAW_WS_URL = os.getenv("OPENCLAW_WS_URL", "ws://openclaw_gateway:18789")
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN", "wlillidan1-demo-token-12345")
async def chat_with_ai(message: str, session_id: str = "pymesbot") -> Optional[str]:
"""Send message to Z.AI API directly"""
import httpx
ZAI_API_KEY = "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS"
ZAI_API_URL = "https://api.z.ai/api/anthropic/v1/messages"
system_prompt = """Sos el asistente de ventas de Demo Librería en Argentina.
Productos: biromes, lápices, cuadernos, colores, reglas, etc.
Precios en pesos argentinos.
Respondé de forma útil, breve y siempre preguntá si se concretó la venta."""
try:
response = httpx.post(
ZAI_API_URL,
headers={
"Authorization": f"Bearer {ZAI_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": "glm-4.7",
"max_tokens": 200,
"system": system_prompt,
"messages": [{"role": "user", "content": message}],
},
timeout=30.0,
)
if response.status_code == 200:
data = response.json()
content = data.get("content", [])
if content and len(content) > 0:
return content[0].get("text", "")
else:
logger.error(f"[Z.AI] API error: {response.status_code} - {response.text}")
return None
except Exception as e:
logger.error(f"[Z.AI] Error: {e}")
return None
return None
# Alias para compatibilidad
chat_with_openclaw = chat_with_ai
DATA_DIR = Path(__file__).parent / "data"
DB_PATH = DATA_DIR / "stock.db"
TEMPLATES_DIR = Path(__file__).parent / "templates"
def get_db():
if not DB_PATH.exists():
init_db()
return DB_PATH
def init_db():
DATA_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS productos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
marca TEXT,
categoria TEXT NOT NULL DEFAULT 'general',
precio REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
variantes TEXT,
codigo TEXT,
activo INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS ventas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
producto_id INTEGER NOT NULL,
cantidad INTEGER NOT NULL,
precio_vendido REAL NOT NULL,
vendedor TEXT,
notas TEXT,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (producto_id) REFERENCES productos(id)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS config (
clave TEXT PRIMARY KEY,
valor TEXT NOT NULL
)
""")
default_config = [
("nombre_negocio", "Demo Librería"),
("moneda", "ARS"),
("moneda_simbolo", "$"),
("alerta_stock_minimo", "5"),
("vendedor_pin", "1234"),
("admin_password", "admin123"),
("rubro", "libreria"),
]
for clave, valor in default_config:
cursor.execute(
"INSERT OR IGNORE INTO config (clave, valor) VALUES (?, ?)", (clave, valor)
)
sample_products = [
(
"Birome Bic Cristal Azul",
"Bic",
"escritura",
850,
50,
'{"color": ["azul"]}',
"7501031311309",
),
(
"Birome Bic Cristal Rojo",
"Bic",
"escritura",
850,
30,
'{"color": ["rojo"]}',
"7501031311316",
),
(
"Birome Bic Cristal Negro",
"Bic",
"escritura",
850,
45,
'{"color": ["negro"]}',
"7501031311323",
),
(
"Cuaderno Rivadavia 48 Hojas",
"Rivadavia",
"cuadernos",
2500,
20,
'{"tipo": ["rayado", "blanco"]}',
None,
),
("Lápiz Faber Castell 2B", "Faber Castell", "escritura", 450, 100, None, None),
("Goma de borrar Staedtler", "Staedtler", "escritura", 320, 25, None, None),
("Regla 30cm", "Maped", "geometria", 650, 15, None, None),
("Compás Prisma", "Prisma", "geometria", 2500, 8, None, None),
("Caja de colores 12", "Maped", "colores", 3200, 18, None, None),
("Papel glasé x 20", "Laprida", "colores", 850, 40, None, None),
]
for nombre, marca, categoria, precio, stock, variantes, codigo in sample_products:
v = variantes if variantes else None
cursor.execute(
"""
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(nombre, marca, categoria, precio, stock, v, codigo),
)
conn.commit()
conn.close()
class BuscarStockRequest(BaseModel):
query: str
limit: int = 5
class ConfirmarVentaRequest(BaseModel):
producto_id: int
cantidad: int
precio_vendido: float
vendedor: str = "vendedor"
class LoginRequest(BaseModel):
pin: str
app = FastAPI(title="PymesBot Backend")
if TEMPLATES_DIR.exists():
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
else:
templates = None
@app.on_event("startup")
async def startup():
init_db()
@app.get("/health")
async def health():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT valor FROM config WHERE clave = 'nombre_negocio'")
nombre = cursor.fetchone()
conn.close()
return {"status": "ok", "negocio": nombre[0] if nombre else "Demo"}
@app.get("/stock/search")
async def buscar_stock(q: str, limit: int = 5):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
q_lower = q.lower()
cursor.execute(
"""
SELECT * FROM productos
WHERE activo = 1 AND (
LOWER(nombre) LIKE ? OR
LOWER(marca) LIKE ? OR
LOWER(categoria) LIKE ?
)
ORDER BY
CASE
WHEN LOWER(nombre) LIKE ? THEN 1
ELSE 2
END,
stock DESC
LIMIT ?
""",
(f"%{q_lower}%", f"%{q_lower}%", f"%{q_lower}%", f"%{q_lower}%", limit),
)
rows = cursor.fetchall()
resultados = []
for row in rows:
resultados.append(
{
"id": row["id"],
"nombre": row["nombre"],
"marca": row["marca"],
"categoria": row["categoria"],
"precio": row["precio"],
"precio_formateado": f"${row['precio']:.0f}",
"stock": row["stock"],
"variantes": json.loads(row["variantes"]) if row["variantes"] else {},
"hay_stock": row["stock"] > 0,
"promo_activa": None,
}
)
conn.close()
return {"resultados": resultados, "total": len(resultados), "query": q}
@app.get("/stock/producto/{producto_id}")
async def get_producto(producto_id: int):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM productos WHERE id = ?", (producto_id,))
row = cursor.fetchone()
conn.close()
if not row:
raise HTTPException(
status_code=404, detail=f"Producto con id {producto_id} no encontrado"
)
return {
"id": row["id"],
"nombre": row["nombre"],
"marca": row["marca"],
"categoria": row["categoria"],
"precio": row["precio"],
"stock": row["stock"],
"variantes": json.loads(row["variantes"]) if row["variantes"] else {},
"codigo": row["codigo"],
"activo": bool(row["activo"]),
"created_at": row["created_at"],
"updated_at": row["updated_at"],
}
@app.post("/venta/confirmar")
async def confirmar_venta(req: ConfirmarVentaRequest):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM productos WHERE id = ? AND activo = 1", (req.producto_id,)
)
producto = cursor.fetchone()
if not producto:
conn.close()
raise HTTPException(status_code=404, detail="Producto no encontrado")
if producto[4] < req.cantidad:
conn.close()
raise HTTPException(
status_code=400,
detail=f"Stock insuficiente. Stock actual: {producto[4]}, solicitado: {req.cantidad}",
)
cursor.execute(
"""
INSERT INTO ventas (producto_id, cantidad, precio_vendido, vendedor)
VALUES (?, ?, ?, ?)
""",
(req.producto_id, req.cantidad, req.precio_vendido, req.vendedor),
)
cursor.execute(
"""
UPDATE productos SET stock = stock - ?, updated_at = datetime('now')
WHERE id = ?
""",
(req.cantidad, req.producto_id),
)
conn.commit()
cursor.execute("SELECT stock FROM productos WHERE id = ?", (req.producto_id,))
nuevo_stock = cursor.fetchone()[0]
venta_id = cursor.lastrowid
conn.close()
return {
"venta_id": venta_id,
"producto": producto[1],
"cantidad": req.cantidad,
"precio_vendido": req.precio_vendido,
"total": req.cantidad * req.precio_vendido,
"stock_anterior": producto[4],
"stock_nuevo": nuevo_stock,
"timestamp": datetime.now().isoformat(),
}
@app.get("/stats/ventas")
async def ventas_stats(periodo: str = "hoy"):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
if periodo == "hoy":
cursor.execute("""
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
FROM ventas
WHERE date(timestamp) = date('now')
""")
elif periodo == "semana":
cursor.execute("""
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
FROM ventas
WHERE timestamp >= datetime('now', '-7 days')
""")
else:
cursor.execute("""
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
FROM ventas
WHERE timestamp >= datetime('now', '-30 days')
""")
row = cursor.fetchone()
conn.close()
return {
"periodo": periodo,
"total_ventas": row[0] or 0,
"total_pesos": row[1] or 0,
"ticket_promedio": row[2] or 0,
}
@app.get("/")
async def root():
if templates:
return templates.TemplateResponse(
"chat.html", {"request": {}, "nombre_negocio": "Demo Librería"}
)
return HTMLResponse("""
<html>
<head>
<title>PymesBot - Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
.chat-box { background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); height: 70vh; display: flex; flex-direction: column; }
.chat-header { padding: 16px 20px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 12px; }
.chat-header h1 { font-size: 18px; color: #333; }
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; }
.message { margin-bottom: 16px; max-width: 80%; }
.message.bot { margin-right: auto; }
.message.user { margin-left: auto; text-align: right; }
.message .bubble { display: inline-block; padding: 12px 16px; border-radius: 18px; }
.message.bot .bubble { background: #f0f0f0; color: #333; }
.message.user .bubble { background: #007bff; color: white; }
.typing { color: #888; font-style: italic; padding: 8px; }
.chat-input { padding: 16px 20px; border-top: 1px solid #eee; display: flex; gap: 12px; }
.chat-input input { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 24px; outline: none; font-size: 16px; }
.chat-input button { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 24px; cursor: pointer; font-size: 16px; }
.chat-input button:hover { background: #0056b3; }
.sidebar { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.sidebar h2 { font-size: 16px; margin-bottom: 12px; color: #333; }
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
.stat-value { font-weight: 600; color: #007bff; }
</style>
</head>
<body>
<div class="container">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div class="chat-box">
<div class="chat-header">
<h1>🛒 Demo Librería</h1>
</div>
<div class="chat-messages" id="messages">
<div class="message bot">
<div class="bubble">¡Hola! Soy el asistente de ventas de Demo Librería. ¿Qué estás buscando?</div>
</div>
</div>
<div class="chat-input">
<input type="text" id="msgInput" placeholder="Escribí tu consulta..." autocomplete="off">
<button onclick="sendMessage()">Enviar</button>
</div>
</div>
<div>
<div class="sidebar">
<h2>📊 Hoy</h2>
<div class="stat">
<span>Ventas</span>
<span class="stat-value" id="ventas-hoy">0</span>
</div>
<div class="stat">
<span>Total</span>
<span class="stat-value" id="total-hoy">$0</span>
</div>
</div>
<div class="sidebar">
<h2>⚠️ Stock bajo</h2>
<div id="alertas-stock"></div>
</div>
</div>
</div>
</div>
<script>
let ws = null;
const sessionId = Math.random().toString(36).substring(7);
function connect() {
ws = new WebSocket(`wss://${window.location.host}/chat/ws/${sessionId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.msg) {
addMessage(data.msg, 'bot');
}
if (data.stats) {
document.getElementById('ventas-hoy').textContent = data.stats.total_ventas;
document.getElementById('total-hoy').textContent = '$' + data.stats.total_pesos.toLocaleString();
}
};
}
function addMessage(text, type) {
const div = document.createElement('div');
div.className = `message ${type}`;
div.innerHTML = `<div class="bubble">${text}</div>`;
document.getElementById('messages').appendChild(div);
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
}
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !ws) return;
addMessage(text, 'user');
ws.send(JSON.stringify({ msg: text, session_id: sessionId }));
input.value = '';
}
document.getElementById('msgInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
connect();
fetch('/stats/ventas?periodo=hoy')
.then(r => r.json())
.then(data => {
document.getElementById('ventas-hoy').textContent = data.total_ventas;
document.getElementById('total-hoy').textContent = '$' + data.total_pesos.toLocaleString();
});
</script>
</body>
</html>
""")
chat_sessions = {}
@app.websocket("/chat/ws/{session_id}")
async def websocket_chat(websocket: WebSocket, session_id: str):
await websocket.accept()
logger.info(f"[WS] Accepted connection for session: {session_id}")
if session_id not in chat_sessions:
chat_sessions[session_id] = []
try:
while True:
data = await websocket.receive_json()
logger.info(f"[WS] Received: {data}")
mensaje = data.get("msg", "")
session_id = data.get("session_id", session_id)
await websocket.send_json({"typing": True})
logger.info(f"[WS] Processing message: {mensaje}")
# Mejorar búsqueda: manejar plurales y buscar en nombre, marca y categoría
mensaje_lower = mensaje.lower()
# Mapeo de plurales a singular
plurales = {
"lapices": "lapiz",
"lápices": "lápiz",
"lápices": "lápiz",
"cuadernos": "cuaderno",
"biromes": "birome",
"gomas": "goma",
"marcadores": "marcador",
"colores": "colores",
"fibras": "fibras",
"reglas": "regla",
"tijeras": "tijera",
}
for plural, singular in plurales.items():
mensaje_lower = mensaje_lower.replace(plural, singular)
# Extraer palabras clave de búsqueda (más de 3 letras)
palabras = mensaje_lower.split()
stopwords = {
"hola",
"buenos",
"buenas",
"que",
"tiene",
"tenes",
"busco",
"quiero",
"necesito",
"para",
"una",
"clienta",
"vino",
"buscar",
"necesita",
"quanto",
"cuanto",
"cuántos",
"cuántas",
}
terminos = [p for p in palabras if len(p) > 3 and p not in stopwords]
if not terminos:
# Si no hay términos claros, usar el mensaje completo para buscar
terminos = [mensaje_lower]
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# Buscar todos los términos juntos
all_results = []
for term in terminos:
cursor.execute(
"""
SELECT * FROM productos
WHERE activo = 1 AND (
LOWER(nombre) LIKE ? OR
LOWER(marca) LIKE ? OR
LOWER(categoria) LIKE ?
)
ORDER BY stock DESC
LIMIT 10
""",
(f"%{term}%", f"%{term}%", f"%{term}%"),
)
rows = cursor.fetchall()
for row in rows:
if row["id"] not in [r["id"] for r in all_results]:
all_results.append(dict(row))
rows = all_results[:5]
logger.info(f"[WS] Found {len(rows)} rows")
try:
if rows:
resultados = []
for row in rows:
resultados.append(
f"{row['nombre']} - ${row['precio']:.0f} (stock: {row['stock']})"
)
respuesta = f"Encontre estos productos:\n" + "\n".join(resultados)
respuesta += "\n\n¿Se concretó la venta? ¿Cuántas unidades?"
logger.info(f"[WS] Respuesta: {respuesta[:50]}")
else:
# Usar OpenClaw cuando no hay match en DB
logger.info(
f"[WS] No products found, trying OpenClaw for: {mensaje}"
)
respuesta = await chat_with_openclaw(mensaje, session_id)
if not respuesta:
respuesta = "No encontré productos con esa descripción. ¿Podés ser más específico?"
logger.info(
f"[WS] OpenClaw response: {respuesta[:50] if respuesta else 'None'}"
)
chat_sessions[session_id].append({"role": "user", "content": mensaje})
chat_sessions[session_id].append(
{"role": "assistant", "content": respuesta}
)
await websocket.send_json({"typing": False})
await websocket.send_json({"msg": respuesta})
logger.info("[WS] Response sent")
except Exception as e:
logger.error(f"[WS] Error sending response: {e}")
conn.close()
except WebSocketDisconnect:
pass
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,10 @@
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
python-dotenv==1.0.1
aiofiles==23.2.1
httpx==0.27.0

View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo Librería - PymesBot</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
height: 100%;
}
.app {
display: grid;
grid-template-columns: 1fr 300px;
gap: 20px;
height: calc(100vh - 40px);
}
.chat-box {
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
gap: 12px;
}
.chat-header h1 { font-size: 18px; color: #333; }
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message { margin-bottom: 16px; max-width: 80%; }
.message.bot { margin-right: auto; }
.message.user { margin-left: auto; text-align: right; }
.message .bubble {
display: inline-block;
padding: 12px 16px;
border-radius: 18px;
white-space: pre-wrap;
}
.message.bot .bubble { background: #f0f0f0; color: #333; }
.message.user .bubble { background: #007bff; color: white; }
.typing { color: #888; font-style: italic; padding: 8px; }
.chat-input {
padding: 16px 20px;
border-top: 1px solid #eee;
display: flex;
gap: 12px;
}
.chat-input input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
outline: none;
font-size: 16px;
}
.chat-input input:focus { border-color: #007bff; }
.chat-input button {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-size: 16px;
}
.chat-input button:hover { background: #0056b3; }
.sidebar { display: flex; flex-direction: column; gap: 20px; }
.sidebar-box {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.sidebar-box h2 { font-size: 16px; margin-bottom: 12px; color: #333; }
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
.stat-value { font-weight: 600; color: #007bff; }
.product-list { max-height: 200px; overflow-y: auto; }
.product-item { padding: 8px 0; border-bottom: 1px solid #eee; font-size: 14px; }
.product-item .name { font-weight: 500; }
.product-item .price { color: #007bff; }
.product-item .stock { color: #28a745; font-size: 12px; }
.product-item .no-stock { color: #dc3545; font-size: 12px; }
@media (max-width: 768px) {
.app { grid-template-columns: 1fr; }
.sidebar { display: none; }
}
</style>
</head>
<body>
<div class="container">
<div class="app">
<div class="chat-box">
<div class="chat-header">
<span style="font-size: 24px;">🛒</span>
<h1>Demo Librería</h1>
</div>
<div class="chat-messages" id="messages">
<div class="message bot">
<div class="bubble">¡Hola! Soy el asistente de ventas de Demo Librería. ¿Qué estás buscando?</div>
</div>
</div>
<div id="typing-indicator" class="typing" style="display: none;">
Escribiendo...
</div>
<div class="chat-input">
<input type="text" id="msgInput" placeholder="Escribí tu consulta..." autocomplete="off">
<button onclick="sendMessage()">Enviar</button>
</div>
</div>
<div class="sidebar">
<div class="sidebar-box">
<h2>📊 Hoy</h2>
<div class="stat">
<span>Ventas</span>
<span class="stat-value" id="ventas-hoy">0</span>
</div>
<div class="stat">
<span>Total</span>
<span class="stat-value" id="total-hoy">$0</span>
</div>
</div>
<div class="sidebar-box">
<h2>📦 Productos</h2>
<div class="product-list" id="product-list">
<div style="color: #888; font-size: 14px;">Escribí para buscar...</div>
</div>
</div>
</div>
</div>
</div>
<script>
let ws = null;
const sessionId = Math.random().toString(36).substring(7);
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/chat/ws/${sessionId}`);
ws.onopen = () => console.log('Conectado');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.typing !== undefined) {
document.getElementById('typing-indicator').style.display = data.typing ? 'block' : 'none';
}
if (data.msg) {
addMessage(data.msg, 'bot');
}
};
ws.onerror = (e) => console.error('WS Error:', e);
ws.onclose = () => {
console.log('Desconectado, reconectando...');
setTimeout(connect, 3000);
};
}
function addMessage(text, type) {
const div = document.createElement('div');
div.className = `message ${type}`;
div.innerHTML = `<div class="bubble">${escapeHtml(text)}</div>`;
document.getElementById('messages').appendChild(div);
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
addMessage(text, 'user');
ws.send(JSON.stringify({ msg: text, session_id: sessionId }));
input.value = '';
}
document.getElementById('msgInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
connect();
fetch('/stats/ventas?periodo=hoy')
.then(r => r.json())
.then(data => {
document.getElementById('ventas-hoy').textContent = data.total_ventas || 0;
document.getElementById('total-hoy').textContent = '$' + (data.total_pesos || 0).toLocaleString();
})
.catch(() => {});
</script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
{
"agents": {
"defaults": {
"workspace": "/root/.picoclaw/workspace",
"model": "gpt-4o",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20,
"system": "Sos el asistente de ventas de Demo PymesBot, una librería en Argentina.\n\nREGLAS:\n1. Respondé en español argentino.\n2. Sé conciso (máximo 3 líneas).\n3. Siempre preguntá si se concretó la venta."
}
},
"providers": {
"openai": {
"api_key": "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS",
"api_base": "https://api.z.ai/v1"
}
}
}