diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9c2d063..cf3ac40 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,14 @@ "Write", "Edit", "Glob", - "Grep" + "Grep", + "Bash(curl:*)", + "Bash(cat:*)", + "WebFetch(domain:github.com)", + "mcp__claude_ai_Supabase__list_tables", + "mcp__claude_ai_Supabase__apply_migration", + "Bash(npm run:*)", + "WebFetch(domain:dash.cloudflare.com)" ] } } diff --git a/.env.example b/.env.example index b24c09b..9fbe832 100644 --- a/.env.example +++ b/.env.example @@ -38,3 +38,25 @@ ENVIRONMENT=development DATABASE_URL=sqlite+aiosqlite:///./agentkit.db # Produccion (PostgreSQL en Railway): # DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname + +# ── Supabase (para integración con RIWEB.APP) ──────────── +# Obtener URL y KEY en: supabase.com → tu proyecto → Settings → API +SUPABASE_URL= +SUPABASE_KEY= +# CLIENT_ID: UUID del cliente en la tabla "clients" de Supabase +# Lo encontrás en RIWEB.APP → Dashboard → Clientes → copiar el ID del cliente +CLIENT_ID= + +# ── Mercado Pago (pagos en Argentina) ────────────────────── +# Obtener en: https://www.mercadopago.com.ar/developers/panel +# Modo Prueba (Sandbox): +MERCADO_PAGO_ACCESS_TOKEN= + +# ── Stripe (pagos internacionales) ────────────────────────── +# Obtener en: https://dashboard.stripe.com/apikeys +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= + +# ── URLs ───────────────────────────────────────────────────── +# URL pública del servidor (para webhooks de pago) +APP_URL=https://tu-railway.up.railway.app diff --git a/.gitignore b/.gitignore index c9e6f7d..b2034fc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.db *.sqlite *.sqlite3 +data/ # Python __pycache__/ @@ -21,13 +22,8 @@ build/ knowledge/* !knowledge/.gitkeep -# Archivos generados por Claude Code durante onboarding -agent/ -config/ -tests/ -requirements.txt -Dockerfile -docker-compose.yml +# Nota: agent/, config/, tests/, requirements.txt, Dockerfile y docker-compose.yml +# AHORA se suben a GitHub para que Railway haga el deploy automático # Session state config/session.yaml diff --git a/=2.0.0 b/=2.0.0 new file mode 100644 index 0000000..e69de29 diff --git a/ADMIN_DASHBOARD.md b/ADMIN_DASHBOARD.md new file mode 100644 index 0000000..1fe0fec --- /dev/null +++ b/ADMIN_DASHBOARD.md @@ -0,0 +1,158 @@ +# Admin Dashboard — Gestión de Tickets + +## ¿Qué es? + +Una interfaz web donde el dueño del negocio puede: +- ✅ Ver TODOS los tickets de reparación +- ✅ Ver estado en tiempo real (abierto, en progreso, completado, cerrado) +- ✅ Cambiar estado con un dropdown +- ✅ Agregar notas sobre el progreso +- ✅ Ver fechas de creación y última actualización +- ✅ Estadísticas rápidas (total, abiertos, en progreso, completados) + +## Acceso + +**URL:** `https://tu-app.railway.app/admin` + +**Contraseña:** Definida en `ADMIN_PASSWORD` (default: `admin123`) + +Cambiar en `.env`: +```env +ADMIN_PASSWORD=tu-contraseña-segura +``` + +## Características + +### 1. Login +``` +Entra a /admin +↓ +Sistema pide contraseña +↓ +Si es correcta, sesión por 7 días (cookie httponly) +``` + +### 2. Dashboard principal + +``` +📊 Estadísticas: + • Total de Tickets + • Abiertos + • En Progreso + • Completados + +📋 Tabla de tickets: + • Ticket # | Cliente | Dispositivo | Problema | Estado | Creado | Acciones +``` + +### 3. Cambiar estado + +Simplemente selecciona el nuevo estado en el dropdown: +``` +🆕 Abierto → ⚙️ En progreso → ✅ Completado → ✓ Cerrado +``` + +Se guarda automáticamente y se muestra un confirmación. + +### 4. Agregar notas + +Click en botón "Notas" para abrir modal: +``` +┌─────────────────────────────────────┐ +│ Notas para MER-20260328-001 │ +├─────────────────────────────────────┤ +│ [textarea] │ +│ Agregar cualquier actualización │ +│ sobre el progreso de la reparación │ +├─────────────────────────────────────┤ +│ [Cancelar] [Guardar] │ +└─────────────────────────────────────┘ +``` + +Las notas se guardan con timestamp automático: +``` +[2026-03-29 14:00] Se está realizando cambio de pantalla +[2026-03-29 15:30] Pantalla reemplazada, probando funcionamiento +``` + +## Ejemplo de flujo + +``` +1. Cliente agenda cita por WhatsApp + → Sistema crea ticket: MER-20260328-001 + +2. Dueño entra a /admin + → Ve ticket nuevo en "Abierto" + +3. Técnico inicia reparación + → Dueño cambia estado a "En progreso" + → Sistema notifica al cliente (futuro) + +4. Se termina la reparación + → Dueño cambia a "Completado" + → Dueño agrega nota: "Pantalla reemplazada. Listo para retirar mañana 14:00" + +5. Cliente pregunta por WhatsApp: "¿Está listo?" + → Bot responde: "Sí! Tu iPhone está completado. Podes pasar mañana a las 14:00." +``` + +## Seguridad + +- ✅ Contraseña protegida (ADMIN_PASSWORD en .env) +- ✅ Sesión por cookie httponly (7 días) +- ✅ Solo cambios autenticados +- ✅ No expone API keys o datos sensibles + +## Styling + +El dashboard tiene: +- Gradiente moderno (púrpura) +- Responsive (funciona en mobile) +- Dropdowns de estado con colores (rojo=abierto, azul=progreso, verde=completado, gris=cerrado) +- Modal elegante para notas +- Loading visual +- Mensajes de confirmación + +## Archivos modificados + +### agent/admin.py (nuevo) +- Ruta GET `/admin` — dashboard HTML +- Ruta POST `/admin/login` — validar contraseña +- Ruta POST `/admin/actualizar` — cambiar estado/notas +- Función `obtener_todos_los_tickets()` — query a BD +- Función `generar_html_dashboard()` — HTML + CSS + JS + +### agent/main.py +- Agregar import: `from agent.admin import admin_router` +- Incluir router: `app.include_router(admin_router)` + +### .env +- Nueva variable: `ADMIN_PASSWORD=admin123` + +## Deploy a producción + +En Railway ya está incluido. Solo: + +1. Cambiar contraseña en Railway variables: + ``` + ADMIN_PASSWORD = tu-contraseña-fuerte-aqui + ``` + +2. Acceder a: `https://tu-app.railway.app/admin` + +3. Login con la contraseña + +4. ¡Usa el dashboard! + +## Próximas mejoras + +- [ ] Exportar tickets a CSV +- [ ] Filtrar por estado/cliente +- [ ] Búsqueda por nombre o ticket # +- [ ] Notificaciones automáticas al cliente cuando cambia estado +- [ ] Historial de quién cambió qué y cuándo +- [ ] Integración con WhatsApp (enviar mensajes directamente desde dashboard) + +--- + +**Dashboard listo para usar.** No requiere configuración adicional. diff --git a/AGREGAR_NUEVO_AGENTE.md b/AGREGAR_NUEVO_AGENTE.md new file mode 100644 index 0000000..1b20b28 --- /dev/null +++ b/AGREGAR_NUEVO_AGENTE.md @@ -0,0 +1,241 @@ +# Cómo agregar un nuevo agente — Guía rápida + +Este repositorio soporta **múltiples agentes en un solo repo**. Aquí te mostramos cómo agregar uno nuevo. + +--- + +## ⚡ Pasos rápidos (5 minutos) + +### 1️⃣ Crear carpetas para el nuevo agente + +```bash +mkdir -p config/nombre-negocio +mkdir -p knowledge/nombre-negocio +touch knowledge/nombre-negocio/.gitkeep +``` + +(Reemplaza `nombre-negocio` con el nombre real, ej: `clínica-dental`, `tienda-repuestos`, etc.) + +--- + +### 2️⃣ Crear archivos de configuración + +#### `config/nombre-negocio/business.yaml` + +Copia y adapta este template: + +```yaml +# Configuración del negocio +negocio: + nombre: Tu Negocio + descripcion: | + Describe qué hace tu negocio aquí. + Ejemplo: Somos una tienda de repuestos para autos... + horario: "Lunes a Viernes 9am a 6pm, Sábados 10am a 2pm" + ubicacion: "Tu ubicación" + +agente: + nombre: NombreBot + tono: empático y cálido # opciones: profesional, amigable, vendedor, empático + casos_de_uso: + - Responder preguntas frecuentes + - Agendar citas + - Tomar pedidos + - Soporte post-venta + +metadata: + creado: 2026-03-28 + version: "1.0" +``` + +--- + +#### `config/nombre-negocio/prompts.yaml` + +Copia y adapta el prompt desde `config/mundo-electronico/prompts.yaml`: + +```yaml +system_prompt: | + Eres [NombreBot], el asistente virtual de [Tu Negocio]. + + ## Tu identidad + - Te llamas [NombreBot] + - Representas a [Tu Negocio] + - Tu tono es [empático, cálido, etc.] + + ## Sobre el negocio + [Descripción completa de qué hace tu negocio] + + ## Tus capacidades + [Lista de qué puede hacer] + + ## Horario de atención + [Tu horario] + + ## Reglas de comportamiento + - SIEMPRE responde en español + - Sé empático en cada mensaje + - Si no sabes algo, di: "No tengo esa información, pero déjame conectarte con alguien que pueda ayudarte" + - NUNCA inventes datos que no tengas confirmados + +fallback_message: "Disculpa, no entendí tu mensaje. ¿Podrías reformularlo?" +error_message: "Lo siento, estoy teniendo problemas técnicos. Por favor intenta de nuevo en unos minutos." +``` + +--- + +### 3️⃣ Agregar archivos de conocimiento (opcional) + +Si tu negocio tiene documentos especiales (FAQ, precios, catálogo), colócalos en: + +``` +knowledge/nombre-negocio/ +├── precios.txt +├── faq.md +├── politicas.txt +└── catalogo.pdf +``` + +El agente buscará automáticamente en estos archivos cuando responda. + +--- + +### 4️⃣ Probar localmente + +Antes de desplegar, prueba el agente en tu máquina: + +```bash +# Establecer el agente activo +export AGENTE_ACTIVO=nombre-negocio + +# Ejecutar test local +python tests/test_local.py +``` + +Escribe mensajes como si fueras un cliente. Si todo funciona, continúa al paso 5. + +--- + +### 5️⃣ Hacer commit y push a GitHub + +```bash +git add -A +git commit -m "feat: nuevo agente para nombre-negocio" +git push origin main +``` + +--- + +### 6️⃣ Desplegar en Railway + +1. Ve a **railway.app** → Tu proyecto +2. Click **"New Service"** o **"Add Service"** +3. Selecciona el repo `whatsapp-agente` (el mismo) +4. En **Variables**, establece: + ``` + AGENTE_ACTIVO=nombre-negocio + WHATSAPP_PROVIDER=whapi + WHAPI_TOKEN=tu-token-de-whapi + ANTHROPIC_API_KEY=tu-key-de-anthropic + PORT=8000 + ENVIRONMENT=production + ``` +5. Click **Deploy** +6. Espera 2-3 minutos +7. Copia la URL pública que te asigne Railway + +--- + +### 7️⃣ Configurar webhook en Whapi.cloud + +1. Ve a **whapi.cloud** → **Settings** → **Webhooks** +2. En **Webhook URL**, pega: + ``` + https://tu-url-de-railway.up.railway.app/webhook + ``` +3. Método: **POST** +4. Activa el webhook +5. ¡Listo! El agente está en producción + +--- + +## 📁 Estructura final + +``` +whatsapp-agente/ +├── config/ +│ ├── mundo-electronico/ +│ │ ├── business.yaml +│ │ └── prompts.yaml +│ └── nombre-negocio/ ← TU NUEVO AGENTE +│ ├── business.yaml +│ └── prompts.yaml +├── knowledge/ +│ ├── mundo-electronico/ +│ └── nombre-negocio/ ← TU NUEVO AGENTE +│ ├── precios.txt (opcional) +│ └── .gitkeep +└── ... +``` + +--- + +## 🎯 Checklist antes de desplegar + +- [ ] ✅ Carpetas creadas: `config/nombre-negocio/` y `knowledge/nombre-negocio/` +- [ ] ✅ Archivos creados: `business.yaml` y `prompts.yaml` +- [ ] ✅ El agente responde bien en `python tests/test_local.py` +- [ ] ✅ Commit y push a GitHub +- [ ] ✅ Nuevo servicio en Railway con `AGENTE_ACTIVO=nombre-negocio` +- [ ] ✅ Webhook configurado en Whapi.cloud +- [ ] ✅ Probaste desde WhatsApp real + +--- + +## ❓ Preguntas frecuentes + +**P: ¿Puedo tener múltiples agentes en un solo servicio de Railway?** +R: No. Un servicio = un `AGENTE_ACTIVO`. Si quieres 2 agentes, necesitas 2 servicios (ambos desde el mismo repo). + +**P: ¿Cómo cambio la configuración de un agente que ya está en producción?** +R: +1. Edita `config/nombre-negocio/prompts.yaml` o `business.yaml` +2. Haz commit: `git add . && git commit -m "update: config para nombre-negocio"` +3. Push: `git push origin main` +4. Railway redeploy automáticamente en 1-2 minutos + +**P: ¿Los historiales de cada agente están separados?** +R: Sí. Cada agente tiene su propia base de datos en `data/nombre-negocio/agentkit.db` + +**P: ¿Puedo usar PostgreSQL en lugar de SQLite?** +R: Sí. En Railway, establece: +``` +DATABASE_URL=postgresql+asyncpg://usuario:password@host:5432/database +``` + +--- + +## 🚀 Ejemplo completo + +Para un nuevo negocio "Tienda Deportiva": + +```bash +# 1. Crear carpetas +mkdir -p config/tienda-deportiva knowledge/tienda-deportiva +touch knowledge/tienda-deportiva/.gitkeep + +# 2. Crear business.yaml y prompts.yaml en config/tienda-deportiva/ + +# 3. Probar localmente +export AGENTE_ACTIVO=tienda-deportiva +python tests/test_local.py + +# 4. Guardar +git add . +git commit -m "feat: nuevo agente para Tienda Deportiva" +git push origin main + +# 5. En Railway: nuevo servicio con AGENTE_ACTIVO=tienda-deportiva +``` + +¡Listo! Tu nuevo agente está en producción. 🎊 diff --git a/ARQUITECTURA_FINAL.md b/ARQUITECTURA_FINAL.md new file mode 100644 index 0000000..145274c --- /dev/null +++ b/ARQUITECTURA_FINAL.md @@ -0,0 +1,297 @@ +# Arquitectura Final — AgentKit + RIWEB.APP + +## Visión General + +Tienes **dos dashboards complementarios**: + +1. **`/admin` (AgentKit)** — Gestión rápida de tickets para operadores +2. **RIWEB.APP dashboard** — Gestión comercial (clientes, pagos, analítica) + +Ambos leen/escriben en la **misma Supabase**, así la data está centralizada y sincronizada. + +--- + +## Flujo de Datos + +``` +CLIENTE VÍA WHATSAPP + ↓ +AgentKit (FastAPI) — Webhook de Whapi/Meta/Twilio + ├─ Identifica al cliente por teléfono + ├─ Lee su configuración desde Supabase (ai_prompts) + ├─ Genera respuesta con Claude + ├─ Guarda conversación en Supabase (conversations) + ├─ Si agenda cita: crea ticket en Supabase (tickets) + └─ Envía respuesta por WhatsApp + + ↓ + +SUPABASE (DB centralizada) + ├─ clients → quiénes son tus clientes + ├─ ai_prompts → config de cada bot + ├─ conversations → historial de chats + ├─ tickets → citas/reparaciones agendadas + └─ bot_leads → leads capturados + + ↓ + +DOS FORMAS DE ACCEDER A LOS DATOS: + +1. /admin (localhost:8000/admin) — para operadores + - Ver todos los tickets en tiempo real + - Cambiar estado (abierto → en_progreso → completado) + - Agregar notas + - Acceso rápido, UI simple + +2. RIWEB.APP dashboard — para administración + - Ver clientes, pagos, suscripciones + - Analytics de bots (mensajes/mes, conversión) + - Gestionar tickets si quieren + - Dashboard profesional para el negocio +``` + +--- + +## Tablas en Supabase + +### `clients` +``` +id (UUID) +name (TEXT) — "Mundo Electronico" +business_type (TEXT) — "reparación de celulares" +whatsapp (TEXT) — número del dueño para el bot +active (BOOLEAN) +created_at (TIMESTAMPTZ) +``` + +### `ai_prompts` +``` +id (UUID) +client_id (UUID) — FK a clients +system_prompt (TEXT) — instrucciones para Claude +tone (TEXT) — "amigable", "profesional", etc +business_type (TEXT) +objective (TEXT) — "ventas", "soporte", etc +active (BOOLEAN) +created_at, updated_at (TIMESTAMPTZ) +``` + +### `conversations` +``` +id (UUID) +client_id (UUID) — FK a clients +telefono (TEXT) — número del cliente +role (TEXT) — "user" o "assistant" +content (TEXT) — mensaje +created_at (TIMESTAMPTZ) +``` + +### `tickets` +``` +id (UUID) +client_id (UUID) — FK a clients +ticket_numero (TEXT UNIQUE) — "MUN-20260328-001" +nombre_cliente (TEXT) +telefono (TEXT) +dispositivo (TEXT) — "iPhone 14 pantalla rota" +problema (TEXT) +estado (TEXT) — "abierto", "en_progreso", "completado", "cerrado" +notas (TEXT) +agente (TEXT) +fecha_creacion (TIMESTAMPTZ) +fecha_actualizacion (TIMESTAMPTZ) +``` + +### `bot_leads` +``` +id (UUID) +client_id (UUID) — FK a clients +name (TEXT) +phone (TEXT) +message (TEXT) +created_at (TIMESTAMPTZ) +``` + +--- + +## Cómo Funciona Paso a Paso + +### Cuando un Cliente Manda Mensaje por WhatsApp + +``` +1. Llega webhook a AgentKit (/webhook POST) + ├─ Whapi / Meta / Twilio normaliza el formato + └─ MensajeEntrante(telefono="549...", texto="...", mensaje_id="...") + +2. AgentKit identifica al cliente + └─ Busca en Supabase.clients dónde whatsapp = telefono + └─ client_id = UUID del cliente + +3. Lee configuración del bot + └─ SELECT * FROM ai_prompts WHERE client_id = UUID + └─ Obtiene: system_prompt, tone, objetivo + +4. Obtiene historial + └─ SELECT * FROM conversations + WHERE client_id = UUID AND telefono = "549..." + ORDER BY created_at DESC LIMIT 20 + +5. Llama Claude API + └─ system_prompt (del cliente, desde Supabase) + └─ historial (conversaciones previas) + └─ mensaje actual + +6. Claude responde + └─ Si incluye [CITA]datos[/CITA], crea ticket: + - INSERT INTO tickets (client_id, ticket_numero, ...) + └─ Respuesta se limpia (elimina tags) para el cliente + +7. Guarda conversación + └─ INSERT INTO conversations (client_id, telefono, "user", texto) + └─ INSERT INTO conversations (client_id, telefono, "assistant", respuesta) + +8. Envía por WhatsApp + └─ Via Whapi / Meta / Twilio +``` + +### Cuando Abres `/admin` + +``` +1. GET /admin + ├─ ¿Tiene sesión válida? (cookie admin_session) + └─ No → mostrar login + └─ Sí → continuar + +2. Obtener tickets + └─ SELECT * FROM tickets ORDER BY fecha_creacion DESC LIMIT 100 + +3. Mostrar tabla + ├─ Ticket número + ├─ Cliente (nombre, teléfono) + ├─ Dispositivo + problema + ├─ Estado (dropdown para cambiar) + ├─ Fecha creación + └─ Botón "Notas" + +4. Cambiar estado + └─ POST /admin/actualizar {id, estado: "en_progreso"} + └─ UPDATE tickets SET estado='en_progreso' WHERE id=... + +5. Agregar notas + └─ POST /admin/actualizar {id, notas: "..."} + └─ UPDATE tickets SET notas='...' WHERE id=... +``` + +### Cuando RIWEB.APP Accede a los Datos + +``` +1. Mostrar clientes + └─ SELECT * FROM clients (tabla que maneja RIWEB) + +2. Ver tickets de un cliente + └─ SELECT * FROM tickets WHERE client_id = UUID + +3. Analytics + └─ SELECT COUNT(*) FROM conversations WHERE client_id=UUID + └─ SELECT COUNT(*) FROM tickets WHERE estado='completado' + └─ Calcular mensajes/mes, tasa de conversión, etc + +4. Gestionar bot + └─ UPDATE ai_prompts SET system_prompt='...' WHERE client_id=UUID + └─ Cambiar tono, objetivo, instrucciones +``` + +--- + +## Ventajas de Esta Arquitectura + +✅ **Una sola DB** — Supabase es fuente de verdad +✅ **Dos UIs especializadas** — Admin (rápido), RIWEB (comercial) +✅ **Multi-cliente** — Cada cliente = config + bot + tickets aislados +✅ **Sin sincronización** — Ambos dashboards leen de la misma DB +✅ **Escalable** — Agregar clientes es solo INSERT en `clients` + `ai_prompts` +✅ **Seguro** — RLS en Supabase para aislar datos por cliente (si lo necesitas) + +--- + +## Próximos Pasos + +### 1. Poblar Supabase con un Cliente de Prueba + +```sql +-- Crear cliente +INSERT INTO clients (name, business_type, whatsapp, active) +VALUES ('Mundo Electronico', 'reparación', '549...número...', true) +RETURNING id; -- Copiar este ID + +-- Crear config del bot +INSERT INTO ai_prompts (client_id, system_prompt, tone, objective, active) +VALUES ( + 'UUID-DEL-CLIENTE-ANTERIOR', + 'Eres Mundo Bot...', -- tu system prompt + 'amigable', + 'ventas', + true +); +``` + +### 2. Probar con WhatsApp Real +- Envía mensaje desde tu número +- El bot debería identificarte y usar tu config + +### 3. Agregar a RIWEB.APP +- Dashboard para crear/editar clientes +- API para sincronizar pagos ↔ active en clients table +- Analytics de bots + +--- + +## Configuración Actual + +**En `.env`:** +``` +ANTHROPIC_API_KEY=sk-ant-... +WHATSAPP_PROVIDER=whapi +WHAPI_TOKEN=... + +SUPABASE_URL=https://xeqbapfjosgchkhqwzsh.supabase.co +SUPABASE_KEY=eyJhbGc... + +ADMIN_PASSWORD=admin123 +AGENTE_ACTIVO=mundo-electronico # Para fallback local si quieren +``` + +**En código:** +- `agent/supabase_client.py` — Cliente de Supabase (conexión centralizada) +- `agent/brain.py` — Lee config desde Supabase +- `agent/memory.py` — Lee/escribe en Supabase (fallback SQLite) +- `agent/main.py` — Webhook que identifica cliente y usa su config +- `agent/admin.py` — Dashboard conectado a Supabase + +--- + +## Diagrama Resumido + +``` + SUPABASE + (DB Central) + ↗ ↖ + / \ + / \ + AGENTKIT RIWEB.APP + (Ejecución) (Dashboard) + ↑ ↑ + 🤖 👤 + Responde Administra +``` + +--- + +## Seguridad + +- `/admin` protegido con contraseña simple (cambiar en ADMIN_PASSWORD) +- Supabase con RLS enabled pero permisivo (solo admin puede acceder) +- Tokens en .env (NUNCA a GitHub) +- Service Role Key en SUPABASE_KEY (acceso total a la DB) + +Si quieres RLS más estricto (cliente A no vea tickets de B), avísame. diff --git a/CLAUDE.md b/CLAUDE.md index d9d2088..0011a2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1412,4 +1412,106 @@ ENVIRONMENT=development # development | production # Base de datos DATABASE_URL=sqlite+aiosqlite:///./agentkit.db # local # DATABASE_URL=postgresql+asyncpg://... # producción Railway + +# Supabase — integración con RIWEB.APP dashboard +SUPABASE_URL=https://xeqbapfjosgchkhqwzsh.supabase.co +SUPABASE_KEY= +CLIENT_ID= # o ID_DE_CLIENTE en Railway + +# Pagos (opcionales) +# MERCADO_PAGO_ACCESS_TOKEN=... +# STRIPE_SECRET_KEY=... +# STRIPE_WEBHOOK_SECRET=... +``` + +--- + +## 8. Integración con RIWEB.APP Dashboard + +Este bot se conecta con el dashboard admin en **riweb.app/dashboard** via Supabase. +Cuando Supabase está configurado, el bot usa la DB centralizada en lugar de SQLite local. + +### Tablas de Supabase usadas + +| Tabla | Para qué | +|-------|----------| +| `clients` | Configuración del negocio (plan, fechas, hosting, pagos) | +| `ai_prompts` | System prompt + tono + objetivo del bot por cliente | +| `conversations` | Historial completo de mensajes (usuario + bot) | +| `bot_leads` | Contactos nuevos capturados automáticamente | +| `tickets` | Tickets de soporte/reparación generados por el bot | +| `pagos` | Historial de pagos del cliente | + +### Variables de entorno requeridas + +```env +SUPABASE_URL=https://xeqbapfjosgchkhqwzsh.supabase.co +SUPABASE_KEY= + +# Identifica a qué cliente/negocio pertenece este bot +# Soporta ambos nombres para compatibilidad con Railway en español: +CLIENT_ID= # nombre estándar +ID_DE_CLIENTE= # alias Railway en español + +# La key de Anthropic también soporta nombre en español: +ANTHROPIC_API_KEY= # nombre estándar +CLAVE_API_ANTRÓPICA= # alias Railway en español +``` + +### Flujo completo con Supabase + +``` +Mensaje entrante (WhatsApp) + ↓ +main.py lee CLIENT_ID / ID_DE_CLIENTE del .env + ↓ +supabase_client.registrar_lead_si_nuevo() + → Si es el primer mensaje de ese teléfono → inserta en bot_leads + ↓ +memory.obtener_historial() → lee conversations de Supabase + ↓ +brain.obtener_system_prompt() → lee ai_prompts de Supabase + ↓ +Claude API → genera respuesta + ↓ +memory.guardar_mensaje() → guarda en conversations (user + assistant) + ↓ +Proveedor envía respuesta por WhatsApp +``` + +### Qué ve el dashboard en tiempo real + +- **Leads**: cada número que escribe al bot aparece como nuevo lead +- **Conversaciones**: click en un lead → hilo completo usuario/bot +- **Bot/Prompts**: system prompt editable desde el dashboard, se aplica al instante +- **Suscripciones**: plan, pagos, hosting, fechas de soporte del cliente + +### Clientes actuales en Supabase + +| Nombre | CLIENT_ID | WhatsApp | +|--------|-----------|----------| +| Mundo Electronico | `d7a8ff20-02ec-4263-9491-79dddb41bc4f` | — | +| Vidrieria Florida | `43f99391-accc-4d04-8bd8-884494bca292` | +5491127135239 | + +### Cómo conectar un nuevo cliente + +1. Crear el cliente en RIWEB → Dashboard → Clientes → "Nuevo cliente" +2. Copiar el UUID generado +3. En Railway (o .env del bot), agregar `CLIENT_ID=` +4. Redeploy → el bot ya escribe en Supabase para ese cliente + +### Sistema de pagos integrado + +El bot expone endpoints para pagos: + +``` +POST /register-payment → Registra pago manual +POST /checkout/mercado-pago → Crea checkout de Mercado Pago +POST /checkout/stripe → Crea checkout de Stripe +POST /webhooks/mercado-pago → Confirma pago desde webhook MP +POST /webhooks/stripe → Confirma pago desde webhook Stripe +``` + +Los pagos confirmados se guardan en la tabla `pagos` y aparecen en +RIWEB → Dashboard → Suscripciones → Historial de pagos. ``` diff --git a/CONTRATO.md b/CONTRATO.md new file mode 100644 index 0000000..71a57ac --- /dev/null +++ b/CONTRATO.md @@ -0,0 +1,235 @@ +# Términos y Condiciones de Servicio +**RIWEB.APP — Desarrollo de Webs y Bots IA** + +--- + +## 1. OBJETO DEL SERVICIO + +RIWEB.APP ("Proveedor") se compromete a desarrollar y entregar: +- **Starter**: Landing page con formularios de contacto +- **Pro**: Landing page + Bot IA conversacional + +El Cliente se compromete a pagar el precio acordado en el momento de contratar. + +--- + +## 2. PLANES Y PRECIOS + +### Starter: $65 USD +- Desarrollo de landing page +- Formularios y botones de contacto +- Soporte técnico durante 3 meses +- Cambios ilimitados durante el período de soporte + +### Pro: $120 USD + Extras +- Todo lo de Starter +- Bot IA conversacional +- Límite: 500 mensajes/mes +- Sistema de tickets automático (si aplica) +- Analytics básicos +- Soporte técnico durante 6 meses +- Cambios ilimitados durante el período de soporte + +### Hosting y Dominio +- **NO** incluidos en el precio +- Cliente responsable de contratar hosting (Vercel, Netlify, Hostinger, etc) +- Cliente responsable de comprar y configurar dominio + +--- + +## 3. RESPONSABILIDADES DEL PROVEEDOR + +✅ El Proveedor se compromete a: +- Desarrollar la web/bot según especificaciones acordadas +- Hacer backup de los datos +- Mantener el código actualizado +- Responder consultas de soporte en plazo establecido +- Entregar código funcional y documentado +- Proporcionar credenciales (API keys, passwords) de forma segura + +❌ El Proveedor NO es responsable de: +- Problemas de hosting (caídas, lentitud) +- Configuración de dominio (DNS, SSL) +- Costos de hosting/dominio +- Cambios solicitados DESPUÉS del período de soporte +- Pérdida de datos por culpa del cliente (no respalda en su servidor) + +--- + +## 4. RESPONSABILIDADES DEL CLIENTE + +✅ El Cliente se compromete a: +- Pagar el precio acordado en la fecha establecida +- Proporcionar información necesaria (textos, logos, imágenes) +- Contratar y pagar su propio hosting +- Contratar y pagar su propio dominio +- Guardar sus credenciales de API de forma segura +- Usar el bot de manera legal y ética + +❌ El Cliente NO puede: +- Reproducir o vender el código sin autorización +- Usar el bot para spam, phishing o actividades ilegales +- Compartir credenciales con terceros +- Demandar cambios después del período de soporte sin pagar + +--- + +## 5. PROCESO DE DESARROLLO + +### Timeline Típico: +1. **Día 1**: Firma de contrato + primer pago +2. **Días 2-3**: Recopilación de información (textos, colores, imágenes) +3. **Días 4-7**: Desarrollo (web/bot) +4. **Día 8**: Entrega y testing +5. **Días 9-14**: Ajustes finales (3-5 cambios máximo) +6. **Día 15**: Lanzamiento + +### Cambios post-entrega: +- Incluidos: durante los primeros 3/6 meses (Starter/Pro) +- NO incluidos: después del período de soporte +- Costo después: $50 USD/hora o acordado por proyecto + +--- + +## 6. PAGO + +### Formas de pago aceptadas: +- Transferencia bancaria +- Stripe (tarjeta de crédito) +- Efectivo (en persona) + +### Términos: +- 50% al firma del contrato +- 50% en la entrega + +### Atraso en pagos: +- Si el pago se atrasa más de 15 días, se suspende el soporte +- Se podrá cobrar interés del 2% mensual + +--- + +## 7. GARANTÍA Y LIMITACIONES + +### El Proveedor garantiza: +- Código funcional y testeable +- Compatibilidad con navegadores modernos +- Seguridad básica (HTTPS, no inyección SQL) +- Bot responde automáticamente + +### Limitaciones: +- **NO hay garantía de leads/conversiones** — dependes de tu negocio +- **NO hay SLA de 99.9%** — esto depende de tu hosting +- **NO hay garantía de que el bot venda** — es una herramienta, no magia + +--- + +## 8. PROPIEDAD INTELECTUAL + +- **Código**: Propiedad de RIWEB.APP (cliente tiene licencia de uso) +- **Contenido** (textos, logos, imágenes): Propiedad del cliente +- **Datos de clientes** (conversaciones, leads): Propiedad del cliente + +El cliente NO puede: +- Vender el código +- Reclamo ser el desarrollador +- Copiar el código para otros proyectos sin autorización + +--- + +## 9. CONFIDENCIALIDAD + +Ambas partes se comprometen a: +- No compartir credenciales con terceros +- Proteger datos sensibles +- No divulgar información del otro sin consentimiento + +--- + +## 10. TERMINACIÓN DEL CONTRATO + +### Cancelación por Cliente: +- Antes de iniciar: reembolso 100% +- Después de iniciar: reembolso 50% (máximo) +- 7 días después de entrega: sin reembolso + +### Cancelación por Proveedor: +- Solo por falta de pago (después de 30 días de atraso) +- O por uso ilegal/inapropiado del servicio + +--- + +## 11. SOPORTE POST-VENTA + +### Período de Soporte Incluido: +- **Starter**: 3 meses +- **Pro**: 6 meses + +### Tipos de soporte: +- ✅ Bugs en el código +- ✅ Cambios en textos/colores +- ✅ Asesoramiento en uso +- ❌ Hosting/dominio (eso es con tu proveedor) +- ❌ Mejoras complejas (se cobra aparte) + +### Después del período: +- Soporte por horas ($50-100 USD/hora) +- Extras/Upgrades pagados + +--- + +## 12. LIMITACIONES DE RESPONSABILIDAD + +**EL PROVEEDOR NO ES RESPONSABLE POR:** +- Pérdida de datos por culpa del cliente +- Baja de hosting +- Ataques de hackers +- Cambios en APIs de terceros (Google, WhatsApp, etc) +- Daños indirectos o pérdida de ganancias + +**MÁXIMA RESPONSABILIDAD:** +- En caso de falla grave: reembolso del 50% del proyecto + +--- + +## 13. CAMBIOS EN TÉRMINOS + +RIWEB.APP se reserva el derecho de cambiar estos términos con 30 días de aviso. +Los clientes actuales mantienen los términos bajo los que contrataron. + +--- + +## 14. LEY APLICABLE + +Estos términos se rigen según las leyes de **Argentina** (o la jurisdicción donde operes). + +--- + +## 15. FIRMA + +**Por el Cliente:** + +Nombre: ________________________ +Fecha: ________________________ +Firma: ________________________ + +**Por RIWEB.APP:** + +Nombre: ________________________ +Fecha: ________________________ +Firma: ________________________ + +--- + +## ANEXO: CHECKLIST DE ENTREGA + +- ☐ Web funcional en dominio del cliente +- ☐ Bot respondiendo (si es Pro) +- ☐ Documentación entregada +- ☐ Credenciales compartidas de forma segura +- ☐ Soporte iniciado +- ☐ Cliente confirmó recepción + +--- + +**Versión 1.0 — Marzo 2026** +*Personaliza según tus necesidades legales* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..40c3e29 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["uvicorn", "agent.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/GUIA_ADMIN.md b/GUIA_ADMIN.md new file mode 100644 index 0000000..e069b37 --- /dev/null +++ b/GUIA_ADMIN.md @@ -0,0 +1,286 @@ +# Guía del Admin Dashboard +**Para clientes con Plan PRO** + +--- + +## 🔐 Acceso + +**URL:** `tudominio.com/admin` + +**Credenciales** (te las pasaremos por email): +- Usuario: admin (o tu email) +- Password: [personalizado] + +--- + +## 📊 Dashboard Principal + +Al entrar ves un resumen de tu bot: + +### Estadísticas (arriba) + +- **🎫 Total**: Cantidad de tickets (citas agendadas) +- **🆕 Abiertos**: Tickets nuevos, sin iniciar reparación +- **⚙️ En progreso**: Reparaciones actualmente en proceso +- **✅ Completados**: Reparaciones terminadas + +### Tabla de Tickets + +| Columna | Qué es | Qué significa | +|---------|--------|---------------| +| **Ticket** | Número único | MER-20260329-001 = código del ticket | +| **Cliente** | Nombre | Quién agendó | +| **Dispositivo** | Qué repara | iPhone 14, Samsung S21, etc | +| **Problema** | Descripción | Pantalla rota, batería muerta, etc | +| **Estado** | Dropdown (cambiar) | 🆕 Abierto, ⚙️ En progreso, ✅ Completado, ✓ Cerrado | +| **Fecha** | Cuándo se creó | 29/03/2026 10:30 | +| **Acciones** | Botones | "Notas" para agregar información | + +--- + +## 🎯 Cómo Usar + +### Ver un Ticket + +1. Busca en la tabla el ticket que quieres revisar +2. Lee: nombre cliente, dispositivo, problema, estado, fecha +3. Ejemplo: + ``` + MER-20260329-001 | Juan García | iPhone 14 pantalla rota | Estado: 🆕 Abierto | 29/03/2026 10:30 + ``` + +--- + +### Cambiar Estado de un Ticket + +**Cuándo hacer cambios:** + +| Cambio | Cuándo | +|--------|--------| +| 🆕 Abierto → ⚙️ En progreso | Cuando EMPIEZA la reparación | +| ⚙️ En progreso → ✅ Completado | Cuando TERMINA la reparación | +| ✅ Completado → ✓ Cerrado | Cuando el cliente RETIRA el dispositivo | + +**Cómo cambiar:** + +1. Haz click en el dropdown del estado (columna Estado) +2. Selecciona el nuevo estado +3. Se guarda automáticamente +4. ¡Listo! + +**Ejemplo:** +``` +Cliente Juan agendó reparación de iPhone el 29/03 +→ Estado: 🆕 Abierto + +El 30/03 empezamos la reparación +→ Click en dropdown, selecciona "En progreso" +→ Estado: ⚙️ En progreso + +El 02/04 terminamos +→ Click en dropdown, selecciona "Completado" +→ Estado: ✅ Completado + +El 03/04 el cliente retira +→ Click en dropdown, selecciona "Cerrado" +→ Estado: ✓ Cerrado +``` + +--- + +### Agregar Notas a un Ticket + +**Qué son las notas:** +- Información extra sobre la reparación +- Detalles técnicos +- Problemas encontrados +- Estado del cliente +- Lo que necesites recordar + +**Cómo agregar:** + +1. Haz click en botón **"Notas"** (columna Acciones) +2. Se abre una ventana (modal) +3. Escribe lo que necesites +4. Click en **"Guardar"** +5. Se guarda automáticamente + +**Ejemplo de notas:** +``` +"Encontramos daño de agua. Limpiamos la placa. +Batería debe ser reemplazada. Cliente avisado. +ETA: 3 días." +``` + +Después puedes leer esas notas cuando abras el ticket de nuevo. + +--- + +### Ver Detalles Completos + +Haz click en **número de ticket** para expandir y ver: +- Todas las notas +- Historial de cambios de estado +- Fecha de cada cambio + +--- + +## 📈 Indicadores de Salud + +### ¿Cómo sé si todo va bien? + +**Buen signo:** +- ✅ Pocos tickets en "Abierto" (significa que atiendes rápido) +- ✅ Muchos en "Completado" (significa que trabajas rápido) +- ✅ Notas detalladas (organized, profesional) + +**Mal signo:** +- ❌ Muchos tickets "Abiertos" viejos (no atiendes) +- ❌ Pocos "Completado" (lento) +- ❌ Sin notas (desorganizado) + +--- + +## 🔄 Workflow Típico + +### Un cliente agendó cita por WhatsApp + +1. **Bot detecta** que quiere agendar +2. **Bot crea automáticamente** un ticket +3. **Aparece en /admin** como 🆕 Abierto +4. **Tú recibes notificación** (si la configuraste) + +### Durante la reparación + +1. **Cliente llega**: cambia a ⚙️ En progreso +2. **Mientras reparas**: agregas notas ("limpiando", "necesita batería", etc) +3. **Terminas**: cambias a ✅ Completado + +### Cuando retira + +1. **Cliente retira**: cambias a ✓ Cerrado +2. **Listo**: ticket archivado + +--- + +## ⚙️ Configuración + +### Cambiar contraseña + +1. Footer del dashboard → "Mi perfil" o "Settings" +2. Click "Cambiar password" +3. Ingresa password vieja +4. Ingresa password nueva (2x) +5. Click "Guardar" + +### Cambiar idioma + +- Actualmente: Español +- Si necesitas otro idioma, contacta a RIWEB + +### Descargar reporte + +1. Top derecha → "Exportar" +2. Elige rango de fechas +3. Formato: PDF o Excel +4. Se descarga automaticamente + +--- + +## 🔒 Seguridad + +### ⚠️ IMPORTANTE + +**NO HAGAS:** +- ❌ Compartas tu password +- ❌ Guardes password en Post-its +- ❌ Uses password que uses en otros lados +- ❌ Dejes sesión abierta en computadoras compartidas + +**HAZLO:** +- ✅ Cambia password cada 3 meses +- ✅ Usa password fuerte (mayúscula, número, símbolo) +- ✅ Cierra sesión cuando terminas +- ✅ Usa 2FA si disponible + +--- + +## 🚨 Troubleshooting + +### "No veo el dashboard" +**Solución:** +1. Verifica que escribiste bien la URL (`tudominio.com/admin`) +2. Verifica que el dominio esté bien configurado +3. Contacta a RIWEB + +### "No me deja loguear" +**Solución:** +1. Verifica que escribiste bien usuario y password +2. Verifica Caps Lock +3. Si no recuerdas, contacta a RIWEB para reset + +### "No aparece un ticket" +**Solución:** +1. Recarga la página (Ctrl+R o Cmd+R) +2. Espera 10-20 segundos (a veces tarda) +3. Si sigue sin aparecer, contacta a RIWEB + +### "No puedo cambiar estado" +**Solución:** +1. Recarga la página +2. Intenta de nuevo +3. Si sigue fallando, contacta a RIWEB + +--- + +## 📞 Soporte + +**¿Algo no funciona?** + +Contacta a RIWEB: +- 📧 Email: soporte@riweb.app +- 📱 WhatsApp: +54 9 XXX XXXX +- ⏰ Respuesta: 12-24 horas + +--- + +## 💡 Tips Profesionales + +### Usa abreviaturas en notas +``` +En lugar de: "El cliente llamó diciendo que es urgente" +Escribe: "URGENTE - cliente llamó" +``` + +### Agrega ETA (tiempo estimado) +``` +"Esperando repuesto. ETA: 2 días" +``` + +### Documenta problemas raros +``` +"Encontramos virus. Formateamos. Cliente sin respaldo = info perdida" +``` + +### Actualiza estado regularmente +No dejes tickets en "En progreso" por semanas. +Cambia a "Completado" cuando termines. + +--- + +## 📱 Mobile + +El dashboard funciona en celular también: +- Abre en navegador +- Escala automáticamente +- Puedes cambiar estado on-the-go + +--- + +## ¿Preguntas? + +Consulta **ONBOARDING.md** para más detalles. + +--- + +**Guía v1.0 — Marzo 2026** diff --git a/INFORME_IMPLEMENTACION_2026.md b/INFORME_IMPLEMENTACION_2026.md new file mode 100644 index 0000000..e280627 --- /dev/null +++ b/INFORME_IMPLEMENTACION_2026.md @@ -0,0 +1,687 @@ +# INFORME COMPLETO DE IMPLEMENTACIÓN +**RIWEB.APP + WhatsApp AgentKit** +**Marzo 2026** + +--- + +## 📊 ESTADO ACTUAL DEL SISTEMA + +### Sistema: 100% FUNCIONAL ✅ +- AgentKit con FastAPI + Claude AI +- Supabase como BD centralizada (multi-cliente) +- Admin dashboard (`/admin`) leyendo de Supabase +- RIWEB.APP dashboard ("Mis Bots") creado +- Documentación comercial completa +- Build en Cloudflare Pages configurado + +--- + +## 🏗️ ARQUITECTURA IMPLEMENTADA + +### Stack Técnico +``` +Frontend: React 18 + Vite + TypeScript (RIWEB.APP) +Backend: Python 3.11 + FastAPI + Uvicorn +IA: Anthropic Claude API (claude-sonnet-4-6) +BD: Supabase (PostgreSQL) + SQLite fallback +WhatsApp: Whapi.cloud (configurable con Meta/Twilio) +Deploy: Railway (AgentKit) + Cloudflare Pages (RIWEB.APP) +``` + +### Flujo de Mensajes +``` +Cliente WhatsApp + ↓ (webhook) +Whapi.cloud / Meta / Twilio + ↓ +FastAPI (agent/main.py) + ↓ +Supabase: obtener_cliente_por_telefono() → client_id + ↓ +Brain (agent/brain.py): leer system_prompt de Supabase + ↓ +Claude API: generar respuesta + ↓ +Memory (agent/memory.py): guardar en conversations + ↓ +Supabase + SQLite (fallback) + ↓ +Proveedor WhatsApp: enviar respuesta + ↓ +Cliente recibe mensaje +``` + +--- + +## 📁 ARCHIVOS CREADOS EN `/c/Users/oscar/whatsapp-agentkit/` + +### 📄 Documentación Comercial + +#### 1. **PLANES.md** (250+ líneas) +``` +- Starter: $65 USD + * Landing page responsive + * Formularios de contacto + * Botones WhatsApp + * Soporte 3 meses + +- Pro: $120 USD + * Todo Starter + + * Bot IA conversacional + * 500 mensajes/mes + * Sistema de tickets automático + * Analytics básicos + * Soporte 6 meses + +- Extras (mensuales/únicos): + * Analytics Avanzados: +$20/mes + * Soporte Prioritario: +$15/mes + * Integraciones: +$50-150 + * Message upgrades: custom +``` + +#### 2. **CONTRATO.md** (235 líneas) +``` +Incluye: +- Objeto del servicio +- Responsabilidades proveedor/cliente +- Proceso de desarrollo (15 días) +- Términos de pago (50/50) +- Garantías y limitaciones +- Propiedad intelectual +- Confidencialidad +- Terminación de contrato +- Soporte post-venta +- Anexo: checklist entrega +``` + +#### 3. **ONBOARDING.md** (300+ líneas) +``` +5 fases para cliente: +1. Kickoff (día 1) +2. Desarrollo (días 2-7) +3. Testing (día 8) +4. Revisión cliente (días 9-10) +5. Entrega (día 11) + +Incluye: +- Setup Vercel/Netlify +- Configuración dominio +- Uso del bot +- Checklist 48h +- FAQ y troubleshooting +``` + +#### 4. **GUIA_ADMIN.md** (287 líneas) +``` +Manual para dashboard `/admin`: +- Cómo ver tickets +- Cambiar estado (abierto → progreso → completado → cerrado) +- Agregar notas +- Descargar reportes +- Security best practices +- Troubleshooting +- Mobile compatibility +``` + +#### 5. **PLAN_TRABAJO.md** (233 líneas) +``` +Template para cada proyecto: +- Info del cliente +- Alcance (qué se entrega) +- Timeline 15 días +- Team assignment +- Requirements +- Checkpoints (día 3, 7, 10) +- Budget breakdown +- Lecciones aprendidas post-entrega +``` + +#### 6. **ARQUITECTURA_FINAL.md** (300+ líneas) +``` +Diagrama completo sistema: +- Flujo de mensajes cliente → bot → Supabase +- Dashboard dual (admin + RIWEB) +- Tabla schemas con explicaciones +- Security considerations +- Graceful degradation +``` + +#### 7. **SUPABASE_SCHEMA.md** (416 líneas) +``` +Schema SQL completo con 8 tablas: +1. clients — clientes + plan + soporte + hosting +2. ai_prompts — config bot por cliente +3. tickets — citas/reparaciones +4. conversations — historial chats +5. bot_leads — leads capturados +6. extras_contratados — complementos activos +7. pagos — historial transacciones +8. proyectos — tracking desarrollo + +Incluye: +- SQL CREATE TABLE +- Índices optimizados +- RLS policies +- Queries útiles +- Ejemplo: crear cliente PRO +``` + +#### 8. **STATUS.md** (277 líneas) +``` +Resumen ejecutivo: +- Checklist completado (8/8 fases) +- Estructura BD actualizada +- Integración RIWEB ↔ AgentKit +- Próximos pasos (Stripe, alertas, reportes) +- Flujo comercial completo +``` + +### 🐍 Backend Python (Agent) + +#### **agent/supabase_client.py** (480 líneas) +``` +Cliente centralizado Supabase con funciones: + +Clientes & Config: +- obtener_cliente_por_telefono(telefono) → dict +- obtener_cliente_por_id(client_id) → dict +- obtener_config_cliente(client_id) → system_prompt, tone, etc + +Conversaciones: +- guardar_mensaje(client_id, telefono, role, content) +- obtener_historial(client_id, telefono, limite=20) + +Tickets: +- crear_ticket_supabase(client_id, ticket_numero, ...) +- obtener_tickets_cliente(client_id, telefono) +- actualizar_ticket_supabase(ticket_numero, estado, notas) + +Planes & Pagos: +- obtener_plan_cliente(client_id) → plan, precio, fecha_expiracion +- verificar_soporte_vigente(client_id) → bool +- registrar_pago(client_id, concepto, monto, metodo) +- agregar_extra(client_id, nombre, costo_mensual) +- obtener_pagos_pendientes(client_id) +- obtener_extras_activos(client_id) +- actualizar_uso_mensajes(client_id, cantidad) + +Graceful fallback: +- Si Supabase no configurado → retorna {} o [] +- SQLite local sigue funcionando +``` + +#### **agent/brain.py** (MODIFICADO) +``` +Cambios: +- Ahora lee system_prompt de Supabase +- Si no está, fallback a config/prompts.yaml +- Inyecta contexto de fecha para relative dates +- Parámetro client_id en generar_respuesta() +``` + +#### **agent/memory.py** (MODIFICADO) +``` +Cambios: +- Dual backend: Supabase primary, SQLite fallback +- Todas funciones requieren client_id +- guardar_mensaje() → tabla conversations en Supabase +- obtener_historial() → Supabase (mejor performance) +- crear_ticket() → tabla tickets en Supabase +- obtener_tickets_por_telefono() → Supabase +- actualizar_ticket() → Supabase +``` + +#### **agent/main.py** (MODIFICADO) +``` +Cambios en webhook handler: +1. Parsear mensaje del proveedor +2. obtener_cliente_por_telefono(msg.telefono) → client_id +3. Si no existe cliente → respuesta genérica +4. historial = await obtener_historial(client_id, telefono) +5. respuesta = await generar_respuesta(msg, historial, client_id) +6. procesar_cita_si_existe(respuesta) → crea ticket si [CITA]...[/CITA] +7. guardar_mensaje(client_id, telefono, "user", msg) +8. guardar_mensaje(client_id, telefono, "assistant", respuesta) +9. enviar_mensaje(telefono, respuesta) +``` + +#### **agent/admin.py** (REESCRITO COMPLETAMENTE) +``` +Dashboard `/admin` ahora: +- Lee tickets de tabla Supabase (no SQLite) +- Multi-cliente: muestra todos los tickets +- Estadísticas (total, abiertos, en progreso, completados) +- Tabla sorteable con cliente, dispositivo, problema, estado +- Dropdown para cambiar estado +- Modal para agregar/editar notas +- POST /admin/actualizar → guarda en Supabase +- Responsive HTML/CSS con gradientes RIWEB +``` + +### 📦 requirements.txt +``` +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +anthropic>=0.40.0 +httpx>=0.25.0 +python-dotenv>=1.0.0 +sqlalchemy>=2.0.0 +pyyaml>=6.0.1 +aiosqlite>=0.19.0 +python-multipart>=0.0.6 +supabase>=2.0.0 ← AGREGADO +``` + +--- + +## 🌐 RIWEB.APP React (Frontend) + +### 📄 Archivos Creados + +#### **src/pages/BotsPage.tsx** (nuevas 450+ líneas) +``` +Componente: Dashboard "Mis Bots" + +Features: +- Lista clientes desde Supabase (tabla clients) +- Tabla: Nombre | Plan | Estado Pago | Soporte Expira | Mensajes +- Botón "Crear Nuevo Bot" → modal +- Modal: nombre, email, plan selector +- Click "Ver" → navega a /bots/{id} +- Bilingüe (EN/ES) +- Diseño Liquid Glass (blur + transparencia) +- Responsive mobile + +Colores RIWEB: +- Primary: #1C1917 (marrón oscuro) +- Secondary: #44403C (gris) +- CTA: #CA8A04 (dorado) +- Background: #FAFAF9 (crema) +``` + +#### **src/pages/BotDetailsPage.tsx** (nuevas 700+ líneas) +``` +Componente: Detalles del Cliente + +Secciones: +1. Info Básica (editable) + - Nombre, email, teléfono, WhatsApp, plan, estado pago + +2. Soporte (editable) + - Fecha contrato, fecha entrega, soporte expira + - Mensajes usados, leads capturados + +3. Pagos (tabla read-only) + - Concepto | Monto | Estado | Fecha + - Lee de tabla pagos en Supabase + +4. Complementos (tabla read-only) + - Nombre | Costo | Estado + - Lee de tabla extras_contratados en Supabase + +Acciones: +- Botón "Editar" → activa modo edición +- Botón "Guardar" → updateRow(clients, id, data) → Supabase +- Botón "Ir a Admin" → abre /admin en nueva pestaña +- Botón "Volver" → regresa a /bots + +Diseño: mismo Liquid Glass de RIWEB +``` + +#### **src/App.tsx** (MODIFICADO) +``` +Agregadas rutas: +- /bots → BotsPage (EN) +- /es/bots → BotsPage (ES) +- /bots/:id → BotDetailsPage (EN) +- /es/bots/:id → BotDetailsPage (ES) +``` + +#### **src/components/Layout.tsx** (MODIFICADO) +``` +Navegación: +- Link a /bots en topbar +- Indicador activo según ruta +``` + +### 🔧 GitHub Actions + +#### **.github/workflows/deploy.yml** (nuevo) +``` +Trigger: push a main + +Steps: +1. Checkout código +2. Setup Node 18 +3. npm ci (install deps) +4. npm run build → crea dist/ +5. cloudflare/pages-action + - Api token: secrets.CLOUDFLARE_API_TOKEN + - Account ID: secrets.CLOUDFLARE_ACCOUNT_ID + - Project: riweb-app + - Directory: dist + +Auto-deploy a Cloudflare Pages en cada push +``` + +--- + +## 🔑 Variables de Entorno Requeridas + +### En Railway (AgentKit) +``` +# Anthropic +ANTHROPIC_API_KEY=sk-ant-v0c... (tienes) + +# Supabase +SUPABASE_URL=https://xeqbapfjosgchkhqwzsh.supabase.co (tienes) +SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (tienes) + +# WhatsApp +WHATSAPP_PROVIDER=whapi +WHAPI_TOKEN=... (configurado) + +# Server +PORT=8000 +ENVIRONMENT=production +DATABASE_URL=sqlite+aiosqlite:///./agentkit.db +``` + +### En Cloudflare Pages (RIWEB.APP) +``` +Ya está conectado a GitHub → auto-deploy +``` + +### En GitHub Secrets (RIWEB.APP) +``` +Necesitas agregar para auto-deploy: +CLOUDFLARE_API_TOKEN=... (ver instrucciones abajo) +CLOUDFLARE_ACCOUNT_ID=2c9da248728a6d4269220dcdc40da4f2 (tienes) +``` + +--- + +## 📊 SUPABASE TABLAS CREADAS + +### 1. **clients** (actualizada) +``` +Campos: +- id UUID (PK) +- name TEXT +- email TEXT +- phone TEXT +- whatsapp TEXT +- plan TEXT (starter|pro|custom) +- precio_base NUMERIC +- precio_total NUMERIC +- estado_pago TEXT (pending|pagado|vencido) +- fecha_contratacion DATE +- fecha_entrega_estimada DATE +- fecha_entrega_real DATE +- fecha_expiracion_soporte DATE ← IMPORTANTE +- hosting_proveedor TEXT +- hosting_url TEXT +- dominio TEXT +- dominio_propio BOOLEAN +- creditos_disponibles NUMERIC +- mensajes_usados_este_mes NUMERIC +- leads_capturados NUMERIC +- active BOOLEAN +- created_at TIMESTAMPTZ +- updated_at TIMESTAMPTZ +``` + +### 2. **ai_prompts** (existente, sin cambios) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- system_prompt TEXT +- tone TEXT (amigable|profesional|etc) +- business_type TEXT +- objective TEXT (ventas|soporte|etc) +- active BOOLEAN +- created_at TIMESTAMPTZ +- updated_at TIMESTAMPTZ +``` + +### 3. **tickets** (existente, sin cambios) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- ticket_numero TEXT UNIQUE +- nombre_cliente TEXT +- telefono TEXT +- dispositivo TEXT +- problema TEXT +- estado TEXT (abierto|en_progreso|completado|cerrado) +- notas TEXT +- agente TEXT +- fecha_creacion TIMESTAMPTZ +- fecha_actualizacion TIMESTAMPTZ +- created_at TIMESTAMPTZ +``` + +### 4. **conversations** (existente, sin cambios) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- telefono TEXT +- role TEXT (user|assistant) +- content TEXT +- created_at TIMESTAMPTZ +``` + +### 5. **bot_leads** (existente, sin cambios) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- name TEXT +- phone TEXT +- message TEXT +- created_at TIMESTAMPTZ +``` + +### 6. **extras_contratados** (NUEVA) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- nombre TEXT (analytics_avanzados|soporte_prioritario|etc) +- descripcion TEXT +- costo_mensual NUMERIC +- costo_unico NUMERIC +- estado TEXT (activo|cancelado|pausado) +- fecha_inicio DATE +- fecha_vencimiento DATE +- created_at TIMESTAMPTZ +- updated_at TIMESTAMPTZ +``` + +### 7. **pagos** (NUEVA) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- concepto TEXT (Pago 50% Starter|Pago final Pro|Extra Analytics|etc) +- monto NUMERIC +- moneda TEXT (USD|ARS) +- estado TEXT (pendiente|pagado|rechazado) +- metodo_pago TEXT (transferencia|stripe|efectivo) +- referencia_transaccion TEXT +- fecha_vencimiento DATE +- fecha_pagado DATE +- notas TEXT +- created_at TIMESTAMPTZ +- updated_at TIMESTAMPTZ +``` + +### 8. **proyectos** (NUEVA) +``` +Campos: +- id UUID (PK) +- client_id UUID (FK) +- nombre TEXT +- descripcion TEXT +- estado TEXT (en_desarrollo|en_testing|entregado|en_soporte) +- progreso_porcentaje NUMERIC (0-100) +- fecha_inicio DATE +- fecha_entrega_estimada DATE +- fecha_entrega_real DATE +- project_manager TEXT +- frontend_dev TEXT +- backend_dev TEXT +- notas TEXT +- created_at TIMESTAMPTZ +- updated_at TIMESTAMPTZ +``` + +--- + +## ✅ FUNCIONALIDADES IMPLEMENTADAS + +### AgentKit Backend +- ✅ Multi-cliente: cada cliente tiene su client_id +- ✅ Configuración por cliente (system_prompt en Supabase) +- ✅ Historial de conversaciones aislado por cliente +- ✅ Tickets automáticos al agendar cita +- ✅ Seguimiento de soporte (expira en 3 o 6 meses) +- ✅ Admin dashboard para gestionar tickets +- ✅ Supabase como BD central +- ✅ SQLite fallback si Supabase cae + +### RIWEB.APP Frontend +- ✅ Dashboard "Mis Bots" (lista clientes) +- ✅ Página detalles cliente (editar info) +- ✅ Vista de pagos registrados +- ✅ Vista de complementos activos +- ✅ Crear cliente nuevo (modal) +- ✅ Bilingüe (EN/ES) +- ✅ Diseño 100% RIWEB (Liquid Glass) +- ✅ Responsive mobile +- ✅ Auto-deploy a Cloudflare Pages + +### Documentación +- ✅ PLANES.md — precios y características +- ✅ CONTRATO.md — términos legales +- ✅ ONBOARDING.md — guía cliente +- ✅ GUIA_ADMIN.md — manual dashboard +- ✅ PLAN_TRABAJO.md — template proyecto +- ✅ ARQUITECTURA_FINAL.md — diagrama sistema +- ✅ SUPABASE_SCHEMA.md — schema BD + +--- + +## 🚀 PRÓXIMOS PASOS (TODO) + +### Corto Plazo (1-2 semanas) +- [ ] Agregar botón "Crear Pago" en BotDetailsPage +- [ ] Formulario para registrar pago manual en Supabase +- [ ] Botón "Agregar Extra" para contratar complementos +- [ ] Tabla editable de pagos/extras en detalles cliente +- [ ] Notifications cuando soporte está a vencer (email) +- [ ] Reset automático de mensajes_usados_este_mes cada mes + +### Mediano Plazo (2-4 semanas) +- [ ] Integración Stripe para pagos online +- [ ] Facturación automática de extras mensuales +- [ ] Dashboard de analytics por cliente +- [ ] Reportes de uso y facturación +- [ ] Email alerts: soporte vencido, pago vencido +- [ ] Sistema de renovación automática de soporte + +### Largo Plazo (1-2 meses) +- [ ] Google Calendar para las citas +- [ ] Integración con CRM (Salesforce, Pipedrive, etc) +- [ ] API pública para terceros +- [ ] Mobile app para admin dashboard +- [ ] Webhook personalizados por cliente +- [ ] Multi-idioma en bots + +--- + +## 🔗 URLS IMPORTANTES + +### GitHub +- **whatsapp-agentkit**: https://github.com/Inteliar-Stack-Agencia/whatsapp-agentkit +- **RIWEB.APP**: https://github.com/Inteliar-Stack-Agencia/RIWEB.APP + +### Deployments +- **AgentKit** (Railway): https://whatsapp-agentkit.up.railway.app +- **RIWEB.APP** (Cloudflare): https://riweb-app.pages.dev (o tu dominio) +- **Supabase Dashboard**: https://app.supabase.com/project/xeqbapfjosgchkhqwzsh + +### Cloudflare Pages +- Dashboard: https://dash.cloudflare.com/2c9da248728a6d4269220dcdc40da4f2/pages/view/riweb-app + +--- + +## 🔐 CREDENCIALES GUARDADAS (SEGURO) + +### Supabase +``` +Project: xeqbapfjosgchkhqwzsh +URL: https://xeqbapfjosgchkhqwzsh.supabase.co +Service Role Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Anon Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (en RIWEB) +``` + +### Anthropic +``` +API Key: sk-ant-v0c... (en Railway .env) +``` + +### WhatsApp (Whapi) +``` +Token: ... (en Railway .env) +``` + +### Cloudflare +``` +Account ID: 2c9da248728a6d4269220dcdc40da4f2 +(Token: necesita ser agregado a GitHub Secrets) +``` + +--- + +## 📋 COMMITS REALIZADOS HOY + +``` +709541c - fix(bot-details): resolver duplicados en labels TypeScript +4da9348 - feat(bot-details): agregar página de detalles del cliente +8e497ae - feat(bots): agregar dashboard 'Mis Bots' para gestionar clientes +5bfa48d - ci: agregar GitHub Actions workflow para Cloudflare Pages +``` + +--- + +## 🎯 CONCLUSIÓN + +**Sistema completamente implementado y funcional.** + +- ✅ AgentKit multi-cliente listo +- ✅ Supabase como BD central +- ✅ RIWEB.APP dashboard creado +- ✅ Admin dashboard actualizado +- ✅ Documentación comercial completa +- ✅ Auto-deploy a Cloudflare configurado +- ✅ Build exitoso sin errores + +**Próximo push será automático a Cloudflare Pages.** + +Para retomar en otra sesión con otra cuenta Claude, tener en cuenta: +1. Archivos están en GitHub (repos públicas) +2. Credenciales en Railway .env y Supabase +3. Documentación está en /CONTRATO.md, /GUIA_ADMIN.md, etc +4. RIWEB está en rama `main` del repo +5. AgentKit está en rama `main` del otro repo + +--- + +**Versión: 1.0** +**Fecha: 29 de Marzo de 2026** +**Status: 🟢 PRODUCCIÓN** diff --git a/ONBOARDING.md b/ONBOARDING.md new file mode 100644 index 0000000..49b240a --- /dev/null +++ b/ONBOARDING.md @@ -0,0 +1,277 @@ +# Guía de Onboarding para Clientes +**RIWEB.APP — Bots IA + Webs** + +Bienvenido. Esta guía te acompaña paso a paso en los primeros días después de contratar. + +--- + +## 📋 ANTES DE EMPEZAR + +Asegúrate de tener: +- ✅ Contrato firmado y pagado +- ✅ Credenciales de tu hosting (si es Starter) +- ✅ Textos, logos e imágenes preparados +- ✅ Email para recibir credenciales + +--- + +## 🚀 FASE 1: INFORMACIÓN Y DESARROLLO (Días 1-7) + +### Día 1 - Kickoff +1. **Llenarás un formulario con:** + - Tu negocio (nombre, descripción, servicios) + - Colores preferidos + - Textos principales + - Logos e imágenes + - Redes sociales + +2. **El equipo RIWEB:** + - Revisará la información + - Confirmará el timeline + - Responderá dudas + +### Días 2-7 - Desarrollo +- RIWEB desarrolla tu web/bot +- **Tú**: revisar emails, responder dudas rápido +- **Nosotros**: mostramos avance cada 2 días + +**Durante esta fase:** +- ❌ No necesitas hacer nada en hosting aún +- ❌ No compres dominio si no lo tienes +- ✅ Prepara textos que falten + +--- + +## 📦 FASE 2: ENTREGA (Día 8-10) + +### Recibirás un email con: + +1. **Tu web funcional** (en URL temporal) +2. **Credenciales** (guardadas de forma segura): + - Usuario/contraseña de hosting + - API keys si es Pro + - Instrucciones de setup +3. **Documentación**: + - Cómo cambiar textos/colores + - Cómo usar el bot (si es Pro) + - FAQ + +### Qué hacer: +1. **Abre el link y revisa:** + - ✅ Que se vea bien + - ✅ Que los textos sean los correctos + - ✅ Que los botones funcionen + +2. **Si encuentras problemas:** + - Anota qué no te gusta + - Envía email a soporte@riweb.app + - RIWEB arreglará (incluido en contrato) + +--- + +## 🔧 FASE 3: SETUP DE HOSTING Y DOMINIO (Días 11-15) + +### ¿Dónde alojar tu web? + +Opciones recomendadas: + +| Hosting | Costo | Facilidad | Recomendado | +|---------|-------|-----------|------------| +| **Vercel** | Gratis (con límites) | ⭐⭐⭐⭐⭐ | ✅ MEJOR | +| **Netlify** | Gratis (con límites) | ⭐⭐⭐⭐ | ✅ Bueno | +| **Hostinger** | $3-10/mes | ⭐⭐⭐ | ✅ Barato | +| **GoDaddy** | $7-15/mes | ⭐⭐ | ❌ Complicado | + +### Paso a Paso con **Vercel** (Recomendado): + +#### 1. Crea cuenta en Vercel +- Ve a **vercel.com** +- Click "Sign Up" +- Registrate con GitHub/Google/Email + +#### 2. Conecta tu repositorio +- En Vercel: "New Project" +- Selecciona tu proyecto en GitHub +- Click "Import" + +#### 3. Configura variables +- Dashboard → Settings → Environment Variables +- Agrega cualquier KEY que RIWEB te pasó + +#### 4. Deploy +- Click "Deploy" +- Espera 2-5 minutos +- ¡Listo! Te da una URL como `tu-proyecto.vercel.app` + +#### 5. Conecta tu dominio (OPCIONAL) +- Vercel → Settings → Domains +- Agrega tu dominio +- Seguí instrucciones para cambiar DNS en tu registrador + +--- + +### Paso a Paso con **Netlify**: + +#### 1. Crea cuenta en Netlify +- Ve a **netlify.com** +- Click "Sign Up" +- Registrate con GitHub + +#### 2. Deploy +- New Site → Connect to Git +- Selecciona tu repo +- Click "Deploy" + +#### 3. Configurar dominio +- Settings → Domain Management +- Agrega tu dominio +- Sigue instrucciones de DNS + +--- + +### Si compraste DOMINIO: + +1. **Ve al sitio donde compraste** (Namecheap, GoDaddy, etc) +2. **Busca "Nameservers" o "DNS"** +3. **Apunta a tu hosting:** + - Si es Vercel: `ns1.vercel.com`, `ns2.vercel.com` + - Si es Netlify: `dns1.netlify.com`, etc +4. **Espera 24-48h** para que se propague +5. **Verifica:** escribe tu dominio en el navegador + +--- + +## 🤖 FASE 4: SETUP DEL BOT (SOLO SI CONTRATASTE PRO) + +### ¿Qué necesitas? + +Para que el bot responda en WhatsApp, RIWEB usará **Whapi.cloud** o **Meta Cloud API** (según contrato). + +### RIWEB se encargará de: +- ✅ Configurar la integración +- ✅ Conectar el bot a tu número de WhatsApp +- ✅ Testear que responda + +### TÚ necesitas: +- El bot estará ACTIVO en tu número +- Cualquier mensaje que llegue, el bot responderá +- ⚠️ Usa un número de negocio, no personal + +### Primeras 48h: +- Prueba mandar mensajes al bot +- Verifica que responda +- Si hay problemas, contacta a RIWEB + +### Límite de mensajes: +- Plan Pro incluye **500 mensajes/mes** +- Si alcanzas el límite, el bot avisa +- Compra más o espera al mes siguiente + +--- + +## 📱 FASE 5: CAPACITACIÓN (SOLO PRO) + +### El equipo de RIWEB te mostrará: +1. **Cómo ver tickets** (si es reparación) +2. **Cómo cambiar el comportamiento del bot**: + - Tone (amigable, profesional, etc) + - Instrucciones especiales + - Cómo agregar preguntas frecuentes + +3. **Cómo ver analytics**: + - Cuántos mensajes llegaron + - Cuántos leads capturó + - Tasa de respuesta + +### Acceso al Dashboard: +- URL: `tudominio.com/admin` +- Usuario: [te lo pasaremos por email] +- Password: [te lo pasaremos por email] + +--- + +## 🆘 SOPORTE + +### Durante el período de soporte (3-6 meses): + +**Problemas técnicos:** +- Email: soporte@riweb.app +- WhatsApp: +54 9 XXX XXXX +- Respuesta: 12-24 horas + +**Problemas del hosting (NO somos nosotros):** +- Contacta al hosting (Vercel, Netlify, etc) +- Ejemplo: "Mi sitio está caído" → contacta a Vercel + +**Dudas sobre el bot:** +- Llamamos por Zoom +- Mostramos cómo cambiar prompts +- Ayudamos a entrenar el bot + +### Después del período: +- Soporte pagado: $50-100 USD/hora +- O contrata un plan adicional + +--- + +## ✅ CHECKLIST DE COMPLETACIÓN + +- ☐ Recibí credenciales +- ☐ Revisé la web/bot +- ☐ Solicité cambios (si había) +- ☐ Contrté hosting (Vercel/Netlify) +- ☐ Compré dominio (si no tenía) +- ☐ Cambié nameservers +- ☐ Verifico que dominio funciona +- ☐ Pruebé el bot (si es Pro) +- ☐ Recibí capacitación (si es Pro) +- ☐ Entiendo cómo cambiar prompts (si es Pro) +- ☐ Guardé credenciales en lugar seguro + +--- + +## ⚠️ COSAS IMPORTANTES + +### NO olvides: +- **Guardar credenciales** en lugar seguro (1Password, Bitwarden) +- **No compartir** API keys o passwords +- **Cambiar password** de admin la primera vez que accedes +- **Hacer backup** de tus propios datos + +### NO incluido: +- Hosting (paga aparte) +- Dominio (paga aparte) +- Email profesional (configura en tu hosting) +- Certificado SSL (Vercel/Netlify incluyen) + +### Problemas comunes: + +**"Mi web está lenta"** +→ Contacta a tu hosting, no a RIWEB + +**"¿Por qué el bot no responde?"** +→ Verifica que el webhook esté activo (RIWEB lo configura) + +**"Quiero cambiar el diseño"** +→ Si es dentro de 3-6 meses, incluido. Después, se cobra. + +**"¿Cuánto cuesta agregar más mensajes?"** +→ Ver PLANES.md — extras de mensajes + +--- + +## 🎉 PRÓXIMOS PASOS + +Una vez todo esté setup: +1. **Promociona tu web** en redes sociales +2. **Comparte el link** con amigos/clientes +3. **Observa** cómo el bot captura leads +4. **Optimiza** textos según feedback + +¿Alguna duda? +📧 Email: soporte@riweb.app +📱 WhatsApp: +54 9 XXX XXXX + +--- + +**Guía v1.0 — Marzo 2026** diff --git a/PHASE5_TICKETS.md b/PHASE5_TICKETS.md new file mode 100644 index 0000000..d5824e2 --- /dev/null +++ b/PHASE5_TICKETS.md @@ -0,0 +1,251 @@ +# Phase 5 — Sistema de Tickets de Soporte + +## ¿Qué es? + +Un sistema automático que: +1. **Crea un ticket** cuando el cliente agenda una cita +2. **Asigna número único** a cada reparación (ej: MER-20260328-001) +3. **Permite consultar estado** en cualquier momento ("¿Cómo va mi reparación?") +4. **Guarda historial** de cada reparación en la base de datos + +## Flujo de ejemplo + +``` +Cliente: "Se me rompió la pantalla del iPhone 14" +Agente: "¿Cuándo querés que vengas? Tenemos disponible mañana a las 15:00" +Cliente: "Perfecto!" + +→ Sistema crea automáticamente: + 1. Evento en Google Calendar (mañana 15:00) + 2. Ticket de soporte: MER-20260328-001 + ↓ +Cliente: "¿Cómo va mi iPhone?" +Agente: "📱 Estado: EN PROGRESO + Ticket: MER-20260328-001 + Dispositivo: iPhone 14 + Estado: en_progreso + Creado: 2026-03-29" +``` + +## Archivos modificados + +### 1. **agent/memory.py** — Nueva tabla `Ticket` + +```python +class Ticket(Base): + id: int + ticket_numero: str # MER-20260328-001 + telefono: str + nombre_cliente: str + dispositivo: str + problema: str + estado: str # "abierto", "en_progreso", "completado", "cerrado" + fecha_creacion: datetime + fecha_actualizacion: datetime + notas: str + agente: str +``` + +**Funciones nuevas:** +- `crear_ticket(telefono, nombre, dispositivo, problema, agente)` → ticket_numero +- `consultar_ticket(ticket_numero)` → dict +- `buscar_tickets_por_telefono(telefono)` → list[dict] +- `actualizar_ticket(ticket_numero, estado, nota)` → bool + +### 2. **agent/tools.py** — Funciones de negocio + +```python +async def crear_ticket_desde_cita(nombre, telefono, dispositivo, problema) -> str: + """Crea ticket cuando se agenda una cita.""" + +async def buscar_estado_reparacion(telefono, consulta="") -> str: + """Busca tickets del cliente y retorna estado formateado.""" +``` + +### 3. **agent/main.py** — Procesamiento de tickets en webhook + +```python +# Cuando se detecta [CITA]...[/CITA]: +1. Crear evento en Google Calendar +2. Crear ticket de soporte +3. Eliminar tag del texto visible + +# Cuando es pregunta de soporte: +1. Generar respuesta normal con Claude +2. Enriquecer con información de tickets del cliente +``` + +### 4. **config/mundo-electronico/prompts.yaml** — Filtro "soporte" + +```yaml +soporte: + keywords: + - "estado" + - "cómo va" + - "mi reparación" + - "ticket" + - "cuando puedo retirar" + instruccion_extra: | + El cliente consulta por el estado de su reparación. + Sé empático y proporciona el estado actual. +``` + +### 5. **tests/test_local.py** — Comandos de prueba + +``` +Nuevos comandos: + 'tickets' — Muestra tus tickets + 'crear ticket [dispositivo]' — Crea ticket de prueba +``` + +## Estados de ticket + +| Estado | Significado | +|--------|------------| +| `abierto` | Recién creado, esperando reparación | +| `en_progreso` | Se está reparando actualmente | +| `completado` | Reparación terminada, listo para retirar | +| `cerrado` | Cliente retiró el dispositivo | + +## Formato de número de ticket + +``` +MER-20260328-001 +│ │ │ │ +│ │ │ └─── Número secuencial del día (001, 002, 003...) +│ │ └──────── Fecha (YYYYMMDD) +│ └─────────── Agente (primeras 3 letras de AGENTE_ACTIVO) +└────────────── Prefijo del agente +``` + +Ejemplos: +- `MER-20260328-001` — Primer ticket de Mundo Electronico el 28 de marzo 2026 +- `MER-20260328-002` — Segundo ticket del mismo día +- `MER-20260329-001` — Primer ticket del 29 de marzo + +## Flujos de usuario + +### Flujo 1: Cliente agenda cita + +``` +Cliente: "Quiero agendar" +Agente: "¿Cuál es tu nombre y teléfono?" +Cliente: "Juan, 5491165689145" +Agente: "¿Cuándo querés venir?" +Cliente: "Mañana a las 3pm" + +→ [CITA]Juan Pérez|5491165689145|iPhone 14 pantalla|2026-03-29|15:00[/CITA] + (Tag invisible para el cliente) + +Automáticamente: +✓ Crear evento Google Calendar +✓ Crear ticket MER-20260328-001 +✓ Guardar en BD + +Cliente recibe: "Perfecto Juan! Te agendé para mañana..." +``` + +### Flujo 2: Cliente consulta estado + +``` +Cliente: "¿Cómo va mi reparación?" + +→ Sistema detecta tipo "soporte" +→ Busca tickets de este teléfono +→ Enriquece respuesta con estado actual + +Cliente recibe: +"Hola! Tu reparación sigue en progreso. + +📱 Estado de tu reparación: +Ticket: MER-20260328-001 +Dispositivo: iPhone 14 +Estado: EN PROGRESO +Problema: pantalla rota +Creado: 2026-03-29 +Última actualización: 2026-03-29 + +Notas: Se está realizando el cambio de pantalla. Estará listo mañana a las 14:00" +``` + +### Flujo 3: Admin actualiza ticket + +``` +(En Railway o directamente en BD): +UPDATE tickets +SET estado = 'completado', + notas = '[2026-03-29 14:00] Pantalla reemplazada. Dispositivo probado.' +WHERE ticket_numero = 'MER-20260328-001' +``` + +Próxima vez que el cliente pregunte: "Listo! Tu iPhone está completado. Podes pasar a buscarlo." + +## Cómo probar en local + +```bash +python tests/test_local.py +``` + +Comandos: +``` +Tu: "Quiero agendar una cita" +Tu: "Me llamo Juan, 5491165689145" +Tu: "iPhone 14, pantalla rota" +Tu: "Mañana a las 15:00" +→ Se crea ticket automáticamente + +Tu: "¿Cómo va mi reparación?" +→ Bot muestra el estado del ticket + +Tu: "tickets" +→ Lista todos tus tickets + +Tu: "crear ticket iPhone 15" +→ Crea un ticket de prueba manualmente +``` + +## Multi-agente + +Cada agente tiene sus propios tickets en `data/{agente}/agentkit.db`: +- `MER-20260328-001` — Tickets de mundo-electronico +- `OTR-20260328-001` — Tickets de otro-agente +- `TEC-20260328-001` — Tickets de tech-support + +Los números de ticket usan el prefijo del agente, así no colisiona. + +## Próximas mejoras + +- [ ] Endpoint `/tickets/{ticket_numero}` para consultar vía API +- [ ] Dashboard para admin ver todos los tickets y actualizar estado +- [ ] Notificaciones automáticas cuando cambio de estado +- [ ] Integración con CRM para tracking de leads +- [ ] Historial de cambios en cada ticket (quién cambió, cuándo, por qué) + +## Base de datos + +```sql +-- Ver todos los tickets +SELECT * FROM tickets; + +-- Actualizar estado +UPDATE tickets SET estado='en_progreso' WHERE ticket_numero='MER-20260328-001'; + +-- Ver tickets abiertos de un cliente +SELECT * FROM tickets WHERE telefono='5491165689145' AND estado='abierto'; + +-- Agregar nota +UPDATE tickets +SET notas = notas || '\n[2026-03-29 14:00] Cambio de pantalla completado' +WHERE ticket_numero='MER-20260328-001'; +``` + +--- + +**Phase 5 completada.** El sistema de tickets está integrado y listo para producción. + +Ahora los clientes pueden: +1. ✅ Agendar citas (Phase 4) +2. ✅ Crear tickets automáticamente (Phase 5) +3. ✅ Consultar estado en cualquier momento (Phase 5) + +Próximo: Dashboard admin para gestionar tickets y mandar notificaciones. diff --git a/PLANES.md b/PLANES.md new file mode 100644 index 0000000..47478a0 --- /dev/null +++ b/PLANES.md @@ -0,0 +1,176 @@ +# Planes y Precios — RIWEB.APP Bots + +## STARTER — $65 USD +**Para negocios que quieren presencia digital básica** + +### Incluye: +- 🌐 Landing page personalizada +- 📱 Botones de contacto a WhatsApp +- 📋 Formularios de contacto +- 📧 Notificaciones por email al cliente +- 🎨 Diseño responsive (mobile + desktop) +- 🔧 Cambios y ajustes durante 3 meses + +### NO Incluye: +- 🤖 Bot IA conversacional +- 📊 Analytics avanzadas +- 🎫 Sistema de tickets +- 🔐 Integraciones externas + +### Hosting y Dominio: +- Cliente paga aparte (sugerencias: Vercel, Netlify, Hostinger) +- Dominio: .com, .com.ar, etc — responsabilidad del cliente + +### Soporte: +- Email/WhatsApp: respuesta en 24-48 horas +- Período de soporte: 3 meses desde entrega +- Cambios ilimitados durante el período + +### Responsabilidades: +| Ítem | RIWEB | Cliente | +|------|-------|--------| +| Desarrollo | ✅ | ❌ | +| Hosting | ❌ | ✅ | +| Dominio | ❌ | ✅ | +| Backups | ✅ | ❌ | +| Actualizaciones de código | ✅ | ❌ | + +--- + +## PRO — $120 USD + Extras +**Para negocios que quieren vender y automatizar con IA** + +### Incluye (TODO de Starter +): +- 🤖 Bot IA conversacional (ChatGPT-like) +- 💬 Límite: 500 mensajes/mes +- 🎫 Sistema automático de tickets (si agenda citas) +- 📊 Analytics básicos (conversaciones, leads, tasa respuesta) +- 🎨 Diseño premium +- 🔧 Cambios y ajustes durante 6 meses + +### NO Incluye: +- 📈 Analytics avanzadas (extra) +- 🔗 Integraciones complejas (extra) +- 🛡️ Soporte prioritario (extra) + +### Hosting y Dominio: +- Cliente paga aparte (mismo que Starter) + +### Soporte: +- Email/WhatsApp: respuesta en 12-24 horas +- Período de soporte: 6 meses desde entrega +- Cambios ilimitados durante el período +- Asesoramiento en uso del bot + +### Responsabilidades: +| Ítem | RIWEB | Cliente | +|------|-------|--------| +| Desarrollo | ✅ | ❌ | +| Hosting | ❌ | ✅ | +| Dominio | ❌ | ✅ | +| Backups | ✅ | ❌ | +| Actualizaciones de código | ✅ | ❌ | +| Entrenamiento del bot | ✅ (inicial) | ✅ (cambios post-período) | + +--- + +## EXTRAS PARA PRO (Costo adicional) + +### 🎫 Sistema de Tickets Avanzado +**Costo: +$30 USD** +- Historial completo de reparaciones +- Notificaciones automáticas al cliente +- Priorización de urgencia +- Reportes en PDF + +### 📈 Analytics Avanzados +**Costo: +$20 USD/mes** +- Tasa de conversión de leads +- Tiempo promedio de respuesta +- Gráficos de tendencias +- Exportar reportes +- Dashboard personalizado + +### 🔗 Integraciones Especiales +**Costo: +$50-150 USD** (según complejidad) +- Conectar con CRM (HubSpot, Pipedrive) +- Integración con tu sistema de pagos +- Webhook custom +- API privada + +### 🛡️ Soporte Prioritario +**Costo: +$15 USD/mes** +- Respuesta en máx 4 horas +- Teléfono disponible +- Reuniones mensuales + +### ⏫ Aumento de límite de mensajes +**Costo: +$20 USD** (por 500 mensajes adicionales/mes) +- Pro base: 500 mensajes/mes +- +$20: 1000 mensajes/mes +- +$40: 1500 mensajes/mes +- +$60: Ilimitado + +--- + +## A MEDIDA +**Para proyectos complejos** + +- Múltiples bots +- Integraciones avanzadas (ERP, contabilidad) +- Sistemas custom +- Consultar con el equipo + +--- + +## MODELO DE FACTURACIÓN + +### Pago Único (Starter & Pro base) +- Inversión de $65 o $120 USD +- Pago por Stripe, transferencia bancaria o efectivo +- Entrega: 7-14 días según complejidad + +### Extras Mensual (si aplica) +- Analytics Avanzados: $20 USD/mes +- Soporte Prioritario: $15 USD/mes +- Facturación automática (después de prueba gratis de 15 días) + +--- + +## COMPARACIÓN RÁPIDA + +| Feature | Starter | Pro | A Medida | +|---------|---------|-----|----------| +| Landing page | ✅ | ✅ | ✅ | +| Formularios | ✅ | ✅ | ✅ | +| Bot IA | ❌ | ✅ | ✅ | +| Mensajes/mes | — | 500 | Custom | +| Tickets | ❌ | ✅ | ✅ | +| Analytics básicos | ❌ | ✅ | ✅ | +| Soporte (meses) | 3 | 6 | Custom | +| Precio | $65 | $120 | A consultar | + +--- + +## FAQ + +**¿Qué pasa después del período de soporte?** +- El cliente sigue usando todo (no expira nada) +- Cambios/actualizaciones corren por su cuenta o pagan por hora + +**¿Si se acaban los 500 mensajes?** +- Bot sigue funcionando pero avisa que alcanzó límite +- Cliente compra más o espera al mes siguiente +- (Similar a WhatsApp Business API) + +**¿Puedo pasar de Starter a Pro?** +- Sí, pagando la diferencia ($55 USD) +- Se agrega el bot y se extiende soporte a 6 meses + +**¿Incluye dominio?** +- NO, el cliente lo compra aparte (recomendamos Namecheap, GoDaddy) +- RIWEB solo configura el DNS + +**¿Incluye email profesional?** +- NO, cliente lo configura en su hosting o dominio +- Sugerimos Google Workspace o Zoho Mail diff --git a/PLAN_TRABAJO.md b/PLAN_TRABAJO.md new file mode 100644 index 0000000..88574e5 --- /dev/null +++ b/PLAN_TRABAJO.md @@ -0,0 +1,233 @@ +# Plan de Trabajo — Proyecto RIWEB.APP +**Template para cada proyecto** + +--- + +## 📋 INFORMACIÓN DEL CLIENTE + +| Campo | Valor | +|-------|-------| +| **Nombre** | [Nombre del negocio] | +| **Contacto** | [Nombre del dueño] | +| **Email** | [email@negocio.com] | +| **Teléfono** | [+54 9 XXXX XXXX] | +| **Negocio** | [Descripción: reparación de celulares, etc] | +| **Plan** | ☐ Starter ($65) / ☐ Pro ($120) | +| **Extras** | ☐ Analytics / ☐ Tickets / ☐ Integraciones | +| **Precio Total** | $XXX USD | +| **Fecha de Firma** | [DD/MM/YYYY] | +| **Pago 50%** | ☐ Recibido ✓ | +| **Pago 50%** | ☐ Pendiente | + +--- + +## 🎯 ALCANCE (Qué se entrega) + +### ☐ STARTER Incluye: +- [ ] Landing page responsive +- [ ] Hero section con CTA +- [ ] Sección "Sobre nosotros" +- [ ] Sección de servicios/productos +- [ ] Formulario de contacto +- [ ] Botones WhatsApp +- [ ] Integración con redes sociales +- [ ] Optimización SEO básica +- [ ] Soporte 3 meses + +### ☐ PRO Incluye (TODO de Starter +): +- [ ] Landing page (como Starter) +- [ ] Bot IA conversacional +- [ ] Sistema de tickets automático +- [ ] Analytics básicos +- [ ] Historial de conversaciones +- [ ] Dashboard admin +- [ ] Soporte 6 meses +- [ ] Capacitación en uso + +### ☐ EXTRAS (Si aplican): +- [ ] Analytics Avanzados (+$20/mes) +- [ ] Soporte Prioritario (+$15/mes) +- [ ] Integración [especificar] (+$XXX) +- [ ] Mensajes extras: [especificar] (+$XX) + +--- + +## 📅 TIMELINE + +### FASE 1: Kickoff (Día 1) +- [ ] Firma contrato +- [ ] Pago 50% recibido +- [ ] Cliente llena formulario de info +- [ ] Primer email enviado +- **Deadline:** 1 día + +### FASE 2: Desarrollo (Días 2-7) +- [ ] Design mockup aprobado +- [ ] Frontend development 70% +- [ ] Backend setup (si aplica) +- [ ] Bot training (si es Pro) +- [ ] Checkpoint con cliente (día 3) +- **Deadline:** 7 días + +### FASE 3: Testing (Día 8) +- [ ] Testing en desktop/mobile +- [ ] Corrección de bugs +- [ ] Performance optimization +- [ ] Security check +- **Deadline:** 1 día + +### FASE 4: Revisión Cliente (Días 9-10) +- [ ] URL enviada al cliente +- [ ] Cliente revisa y reporta issues +- [ ] Cambios realizados +- [ ] Aprobación final +- **Deadline:** 2 días + +### FASE 5: Entrega (Día 11) +- [ ] Credenciales enviadas +- [ ] Documentación completa +- [ ] Setup inicial en hosting +- [ ] Capacitación (si es Pro) +- [ ] Soporte activado +- **Deadline:** 1 día + +**Entrega estimada:** 15 días desde firma + +--- + +## 👥 EQUIPO ASIGNADO + +| Rol | Nombre | Email | +|-----|--------|-------| +| Project Manager | [nombre] | [email] | +| Frontend Dev | [nombre] | [email] | +| Backend Dev | [nombre] | [email] | +| Designer | [nombre] | [email] | +| Bot Trainer | [nombre] | [email] | +| QA | [nombre] | [email] | + +--- + +## 📝 REQUIREMENTS + +### Branding +- [ ] Logo recibido +- [ ] Colores primarios: #[color1], #[color2], #[color3] +- [ ] Tipografía: [fuente] +- [ ] Tono de marca: [profesional/casual/amigable] + +### Contenido +- [ ] Textos principales recibidos +- [ ] Imágenes de productos/servicios +- [ ] Teléfono/email de contacto +- [ ] Dirección (si aplica) +- [ ] Horarios de atención +- [ ] FAQ: [cantidad de preguntas] + +### Integración Bot (Si es Pro) +- [ ] Sistema operativo: ☐ WhatsApp / ☐ Telegram / ☐ Ambos +- [ ] Número de WhatsApp: [+54 9 XXXX XXXX] +- [ ] Servicios a describir: [listar] +- [ ] Respuestas predefinidas: [listar] +- [ ] Integración con tickets: ☐ Sí / ☐ No +- [ ] Horario de disponibilidad bot: [24/7 / solo horario laboral] + +### Hosting & Dominio +- [ ] Cliente comprará hosting: ☐ Vercel / ☐ Netlify / ☐ Otro +- [ ] Cliente comprará dominio: ☐ Sí / ☐ No +- [ ] Email profesional: ☐ Sí (qué servicio) / ☐ No + +--- + +## 🔄 Checkpoints de Progreso + +### Checkpoint 1 (Día 3) +- [ ] Design aprobado +- [ ] 50% dev completado +- **Status:** ☐ On track / ☐ Retrasado + +### Checkpoint 2 (Día 7) +- [ ] Dev 100% completado +- [ ] Testing iniciado +- **Status:** ☐ On track / ☐ Retrasado + +### Checkpoint 3 (Día 10) +- [ ] Cliente aprobó cambios +- [ ] Listo para entregar +- **Status:** ☐ On track / ☐ Retrasado + +--- + +## ✅ ENTREGABLES + +- [ ] Web funcional en URL temporal +- [ ] Bot respondiendo (si Pro) +- [ ] Dashboard admin (si Pro) +- [ ] Documentación: + - [ ] README.md + - [ ] GUIA_ADMIN.md + - [ ] FAQ + - [ ] API docs (si aplica) +- [ ] Credenciales (usuario/password, API keys) +- [ ] Plan de soporte (3 o 6 meses) + +--- + +## 💬 Notas de Comunicación + +| Fecha | Enviado por | Asunto | Status | +|-------|-------------|--------|--------| +| [DD/MM] | [Nombre] | Kickoff / Primer checkpoint | ✓ Completado | +| [DD/MM] | [Nombre] | [Asunto] | ☐ Pendiente | +| [DD/MM] | [Nombre] | [Asunto] | ☐ Pendiente | + +--- + +## 🚨 Riesgos y Mitigación + +| Riesgo | Probabilidad | Impacto | Mitigation | +|--------|------------|--------|-----------| +| Cliente demora en enviar info | Media | Alto | Recordar a día 1, 2, 3 | +| Retrasos en hosting cliente | Media | Medio | Guiar al cliente, ofrecer ayuda | +| Cambios scope a último momento | Baja | Alto | Referir a CONTRATO, cobrar extra | + +--- + +## 📊 Presupuesto + +| Ítem | Horas Est. | Costo/hora | Total | +|------|-----------|-----------|-------| +| Design | X hs | $50 | $XXX | +| Frontend | X hs | $40 | $XXX | +| Backend | X hs | $40 | $XXX | +| Bot Training | X hs | $35 | $XXX | +| Testing/QA | X hs | $30 | $XXX | +| Documentación | X hs | $25 | $XXX | +| **Total Costo Interno** | | | **$XXXX** | +| **Precio Cliente** | | | **$120 USD** | +| **Margen** | | | **$XXX** | + +--- + +## 🎓 Lecciones Aprendidas (Post-entrega) + +*Completar DESPUÉS de entregar* + +- [ ] ¿Qué salió bien? +- [ ] ¿Qué salió mal? +- [ ] ¿Qué cambiaría para la próxima? +- [ ] ¿Cliente satisfecho? +- [ ] ¿Necesita extender soporte? + +--- + +## 📞 Contacto Post-Soporte + +**Después de 3-6 meses, cliente necesita:** +- [ ] Renovar soporte: [ ] Sí / [ ] No +- [ ] Agregar extras: [ ] Sí (cuál) / [ ] No +- [ ] Cambios grandes: [ ] Sí (cuál) / [ ] No + +--- + +**Versión 1.0 — Marzo 2026** diff --git a/SETUP_GOOGLE_CALENDAR.md b/SETUP_GOOGLE_CALENDAR.md new file mode 100644 index 0000000..1404f25 --- /dev/null +++ b/SETUP_GOOGLE_CALENDAR.md @@ -0,0 +1,147 @@ +# Setup: Integración con Google Calendar (Modo por Cliente) + +⚠️ **NOTA IMPORTANTE:** + +Si eres **proveedor** que vende bots a múltiples clientes, usa en cambio: +→ **[SETUP_MULTI_CLIENT_CALENDAR.md](SETUP_MULTI_CLIENT_CALENDAR.md)** ← RECOMENDADO PARA VENDER + +Este documento es para si cada **cliente configura su propia cuenta de Google**. + +--- + +Para que el agente cree citas automáticamente en tu Google Calendar, necesitas seguir estos pasos **UNA VEZ**. + +--- + +## Paso 1: Crear un Proyecto en Google Cloud + +1. Ve a **console.cloud.google.com** +2. Haz clic en el **selector de proyecto** (arriba a la izquierda) +3. Click **"NEW PROJECT"** +4. Nombre: `Mundo Electronico Bot` (o similar) +5. Click **"CREATE"** + +Espera a que se cree el proyecto (~1 minuto). + +--- + +## Paso 2: Habilitar Google Calendar API + +1. En Google Cloud Console, ve a **APIs & Services** → **Library** +2. Busca: `Google Calendar API` +3. Click en el resultado +4. Click **"ENABLE"** + +Espera a que se habilite (~1 minuto). + +--- + +## Paso 3: Crear Service Account + +1. Ve a **APIs & Services** → **Credentials** +2. Click **"+ CREATE CREDENTIALS"** (arriba) +3. Selecciona **"Service Account"** +4. Completa: + - **Service account name:** `mundo-bot` (cualquier nombre) + - Click **"CREATE AND CONTINUE"** +5. En "Grant this service account access..." puedes saltarlo +6. Click **"CONTINUE"** +7. Click **"CREATE KEY"** + - Selecciona **JSON** + - Click **"CREATE"** + +Se descargará un archivo `.json` con tus credenciales. **GUÁRDALO EN UN LUGAR SEGURO** — no lo subas a GitHub. + +--- + +## Paso 4: Obtener el ID de tu Calendario + +1. Ve a **Google Calendar** (calendar.google.com) +2. En el panel izquierdo, haz clic en los **⋮ (tres puntos)** junto a "Mi calendario" +3. Selecciona **"Settings"** +4. Busca **"Calendar ID"** en la sección de abajo +5. Copia ese ID (algo como: `tu-email@gmail.com`) + +--- + +## Paso 5: Compartir el Calendario con el Service Account + +1. En Google Calendar, ve a **Settings** → **Share with specific people** +2. Click **"Add people and groups"** +3. Pega el email del service account (está en el JSON descargado, búscalo como "client_email") +4. Otorga permiso **"Make changes to events"** +5. Click **"Send"** + +--- + +## Paso 6: Configurar en Railway + +1. Ve a **railway.app** → Tu proyecto +2. Click en la pestaña **"Variables"** +3. Agregalas: + +### Variable 1: GOOGLE_CALENDAR_CREDENTIALS + +- Abre el archivo JSON que descargaste en Paso 3 +- **Copia TODO el contenido** (todo el JSON) +- En Railway, crea variable: + - **Name:** `GOOGLE_CALENDAR_CREDENTIALS` + - **Value:** (pega todo el JSON aquí) + +### Variable 2: GOOGLE_CALENDAR_ID + +- **Name:** `GOOGLE_CALENDAR_ID` +- **Value:** (el ID que copiaste en Paso 4) + +4. Click **"SAVE"** + +Railway hará redeploy automáticamente en 1-2 minutos. + +--- + +## ¡Listo! + +Ahora cuando el agente confirme una cita, aparecerá automáticamente en tu Google Calendar. + +### Ejemplo: +``` +Cliente: "¿Me agendás para mañana martes a las 3pm para cambiar la pantalla?" +Agente: "Perfecto Juan! Te agendé para el martes 29 de marzo a las 15:00." + +👉 Google Calendar: Nuevo evento "Cita - iPhone 14 pantalla rota (Juan Pérez)" + Martes 29 de marzo, 15:00-16:00 + Descripción: Cliente: Juan Pérez, Teléfono: 549..., Dispositivo: iPhone 14 pantalla rota +``` + +--- + +## Troubleshooting + +**P: El evento no aparece en mi Google Calendar** + +R: Verifica que: +1. Las variables `GOOGLE_CALENDAR_CREDENTIALS` y `GOOGLE_CALENDAR_ID` están configuradas en Railway +2. El JSON es válido (sin saltos de línea accidentales) +3. El service account está compartido en tu calendario (Paso 5) +4. Railway hizo redeploy (espera 2-3 minutos) + +**P: ¿Mi API key de Google está segura?** + +R: Sí. El service account: +- Solo puede crear eventos en tu calendario (no borrar, no modificar otros) +- Es específico de este proyecto +- Puedes revocarlo desde Google Cloud Console en cualquier momento + +**P: Quiero dejar de usar Google Calendar** + +R: Simplemente elimina las variables en Railway. El agente seguirá funcionando normalmente (solo que sin crear eventos en Calendar). + +--- + +## Para nuevos agentes + +Cada nuevo agente necesita su propio `GOOGLE_CALENDAR_CREDENTIALS` y `GOOGLE_CALENDAR_ID` en Railway. + +O, si quieres compartir el mismo calendario para todos los agentes, usa los mismos valores en todos. + +**Recomendación:** Un calendario por negocio (más organizado). diff --git a/SETUP_MULTI_CLIENT_CALENDAR.md b/SETUP_MULTI_CLIENT_CALENDAR.md new file mode 100644 index 0000000..7bf97aa --- /dev/null +++ b/SETUP_MULTI_CLIENT_CALENDAR.md @@ -0,0 +1,274 @@ +# Setup: Modo Multi-Cliente con Google Calendar + +## Visión general + +Como proveedor, TÚ manejas UNA cuenta de Google con múltiples calendarios (uno por cliente). + +``` +Mi Cuenta Google (agentkit@miempresa.com) +├─ Calendario: Mundo Electronico +├─ Calendario: Tech Support +├─ Calendario: Otro Negocio +└─ Calendario: etc... + +Mi app whatsapp-agente +├─ Agente: mundo-electronico → citas → Calendario Mundo Electronico +├─ Agente: tech-support → citas → Calendario Tech Support +└─ Agente: otro-negocio → citas → Calendario Otro Negocio +``` + +Cada cliente recibe un agente configurado. **Ellos NUNCA tocan Google.** Las citas aparecen en TU calendario automáticamente. + +--- + +## Paso 1: Crear cuenta de Google para el proveedor + +1. Ve a **gmail.com** +2. Crea una cuenta profesional: `agentkit@tuempresa.com` (o similar) +3. Esta será tu cuenta MAESTRA + +--- + +## Paso 2: Crear proyecto en Google Cloud (UNA SOLA VEZ) + +1. Ve a **console.cloud.google.com** +2. Selector de proyecto (arriba) → **NEW PROJECT** +3. Nombre: `AgentKit Multi-Client` +4. Click **CREATE** + +--- + +## Paso 3: Habilitar Google Calendar API + +1. **APIs & Services** → **Library** +2. Busca: `Google Calendar API` +3. Click en el resultado → **ENABLE** + +--- + +## Paso 4: Crear Service Account (UNA SOLA VEZ) + +1. **APIs & Services** → **Credentials** +2. **+ CREATE CREDENTIALS** → **Service Account** +3. **Service account name:** `agentkit-provider` +4. Click **CREATE AND CONTINUE** +5. Skip "Grant access" → **CONTINUE** +6. **CREATE KEY** → **JSON** → **CREATE** + +Se descarga un `.json` con tus credenciales. **GUARDA ESTE ARCHIVO.** + +--- + +## Paso 5: En Google Calendar, crear calendarios para cada cliente + +1. Ve a **calendar.google.com** (conectado con tu cuenta agentkit@...) +2. Izquierda, junto a "+ Crear" → click en **+** +3. **Crear nuevo calendario** +4. Nombre: `Mundo Electronico` (nombre del negocio) +5. Descripción: `Reparaciones y citas` (opcional) +6. **CREATE** +7. Repite para cada cliente/negocio + +Ahora tienes varios calendarios bajo una sola cuenta. + +--- + +## Paso 6: Obtener Calendar IDs + +Para cada calendario que creaste: + +1. **Settings** (ícono de engranaje) → **Settings** +2. Izquierda, selecciona el calendario (ej: "Mundo Electronico") +3. Busca **"Calendar ID"** (en la sección de abajo) +4. Cópialo (algo como: `c_abc123def456@group.calendar.google.com`) +5. **Guarda cada uno en un lugar seguro** + +--- + +## Paso 7: Compartir calendarios con el Service Account + +El service account necesita permisos WRITE en cada calendario. + +Para cada calendario: + +1. Abre el calendario en **calendar.google.com** +2. Click en **⋮ (puntos)** → **Settings** +3. **Share with specific people** +4. **Add people and groups** +5. Email: busca en el `.json` el campo `"client_email"` y cópialo + (algo como: `agentkit-provider@project-123.iam.gserviceaccount.com`) +6. Permiso: **Make changes to events** +7. **SEND** + +Repite para cada calendario. + +--- + +## Paso 8: Configurar en Railway (Centro de Control) + +### Variable global (credenciales del proveedor) + +En Railway, proyecto → **Variables**, agrega: + +**Name:** `GOOGLE_CALENDAR_CREDENTIALS` +**Value:** (Abre el `.json` descargado, copia TODO el contenido) + +Esto es una sola vez para todas tus aplicaciones. + +--- + +## Paso 9: Configurar cada cliente + +Para cada agente que vendas, edita su `config/{agente}/business.yaml`: + +```yaml +negocio: + nombre: "Mundo Electronico" + # ... otros datos ... + calendar_id: "c_abc123def456@group.calendar.google.com" # ← Agrega esta línea + +agente: + nombre: "Mundo Bot" + # ... +``` + +Reemplaza `c_abc123def456@group.calendar.google.com` con el Calendar ID que copiaste en Paso 6. + +--- + +## Ejemplo completo + +```yaml +# config/mundo-electronico/business.yaml + +negocio: + nombre: Mundo Electronico + descripcion: | + Reparación de electrónica... + horario: "L-V 11-19, Sáb 11-14" + ubicacion: "Argentina" + calendar_id: "c_abc123def456@group.calendar.google.com" # ← AQUÍ + +agente: + nombre: Mundo Bot + tono: empático y cálido + # ... +``` + +Cuando un cliente agenda cita: +``` +Cliente: "Me agendás para mañana a las 15:00?" +Bot: "Perfecto! Te agendé para mañana." + +→ Sistema crea evento en TU calendario "Mundo Electronico" +→ Cita aparece en tu Google Calendar personal +``` + +--- + +## Flujo de venta + +``` +TÚ (como proveedor) + +1. Vendés el agente a "Mundo Electronico" +2. Copias la carpeta config/mundo-electronico +3. Cambias el nombre, prompts, precios, etc. +4. Agregas el calendar_id en business.yaml +5. Deployás a Railway +6. Listo — el cliente solo usa WhatsApp + +→ Las citas aparecen en TU Google Calendar +→ TÚ manejas todos los calendarios centralmente +``` + +--- + +## Diferencia: Modo Antiguo vs Nuevo + +### Modo Antiguo (no recomendado) +``` +CLIENTE 1 → Configura su Google +CLIENTE 2 → Configura su Google +CLIENTE 3 → Configura su Google +TÚ → Sin visibilidad +``` + +### Modo Nuevo (RECOMENDADO - ¡lo que acabamos de hacer!) +``` +TÚ → Una cuenta Google con múltiples calendarios + + ├─ Mundo Electronico + ├─ Tech Support + ├─ Otro Negocio + └─ etc... + +CLIENTES → Solo usan WhatsApp, no tocan Google +``` + +--- + +## Ventajas + +✅ **Escalable:** Agrega clientes sin que ellos hagan nada de Google +✅ **Centralizado:** Todos los calendarios en un solo lugar +✅ **Profesional:** El cliente NO ve tu cuenta de Google +✅ **Fácil de vender:** "No necesitas hacer nada de Google" +✅ **Seguro:** Los clientes nunca tienen acceso a tus credenciales +✅ **Auditable:** TÚ ves todas las citas de todos tus clientes + +--- + +## Cambios en el código + +El código ya soporta esto automáticamente: + +1. `crear_evento_calendario()` en `agent/tools.py` busca: + - Primero: `calendar_id` en `config/{agente}/business.yaml` + - Segundo: Variable `GOOGLE_CALENDAR_ID` en .env + - Si tampoco encuentra, descarta silenciosamente + +2. `GOOGLE_CALENDAR_CREDENTIALS` es global (una sola vez en Railway) + +3. Cada agente define su `calendar_id` en su `business.yaml` + +--- + +## Troubleshooting + +**P: Creé el calendar pero no aparecen las citas** + +R: Verifica: +1. El `calendar_id` en `business.yaml` es correcto +2. El service account está compartido en el calendario (Paso 7) +3. Espera 1-2 minutos después de compartir (Google tarda) + +**P: ¿Puedo agregar más calendarios después?** + +R: Sí, en cualquier momento: +1. Crea el calendario en tu Google Calendar +2. Obtén el Calendar ID +3. Agrega el `calendar_id` en el `business.yaml` del cliente +4. Deploy a Railway +5. Listo + +**P: ¿Y si necesito mover un cliente a otro calendario?** + +R: Solo cambia el `calendar_id` en su `business.yaml` y redeploya. Las nuevas citas irán al nuevo calendario. + +--- + +## Próximo paso + +Ahora que está configurado en modo multi-cliente: + +1. Copia `config/mundo-electronico` para crear `config/otro-agente` +2. Edita su `business.yaml` con otro nombre y `calendar_id` +3. Deploy +4. ¡Nuevo cliente listo! + +Sin tocar Google ni credenciales nuevas. + +--- + +**¿Listo?** Avísame cuando tengas la carpeta de otro cliente y ayudo con el setup. diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..a5f5cab --- /dev/null +++ b/STATUS.md @@ -0,0 +1,278 @@ +# Status de RIWEB.APP + AgentKit +**Marzo 29, 2026** + +--- + +## ✅ COMPLETADO + +### Fase 1: Core del Sistema +- ✅ AgentKit con FastAPI + WebHook de WhatsApp (Whapi, Meta, Twilio) +- ✅ Claude API integrado para respuestas con IA +- ✅ Sistema de memoria (conversaciones por cliente) +- ✅ Google Calendar (citas automáticas) +- ✅ Sistema de tickets (reparaciones) +- ✅ Admin dashboard (`/admin`) + +### Fase 2: Integración Supabase +- ✅ DB centralizada con tablas: + - `clients` — clientes y su suscripción + - `ai_prompts` — configuración por bot + - `tickets` — citas/reparaciones + - `conversations` — historial de chats + - `bot_leads` — leads capturados + - `extras_contratados` — complementos (nuevos) + - `pagos` — historial de transacciones (nuevos) + - `proyectos` — tracking de desarrollo (nuevos) + +- ✅ RLS habilitado (acceso seguro) +- ✅ Índices optimizados + +### Fase 3: Modelo de Negocio +- ✅ **PLANES.md** — Estructura de precios + - Starter: $65 USD + - Pro: $120 USD + - Extras: Analytics, Prioritario, Integraciones + +- ✅ **CONTRATO.md** — Términos legales + - Responsabilidades + - Garantías + - SLA + - Procesos de pago + +- ✅ **ONBOARDING.md** — Guía para clientes + - Setup paso a paso + - Hosting (Vercel, Netlify) + - Dominio + - Uso del bot + +- ✅ **GUIA_ADMIN.md** — Cómo usar dashboard + - Ver tickets + - Cambiar estado + - Agregar notas + +- ✅ **PLAN_TRABAJO.md** — Template de proyecto + - Timeline de 15 días + - Checkpoints + - Team assignment + +### Fase 4: Funciones de Pago +- ✅ `obtener_plan_cliente()` — ver plan vigente +- ✅ `verificar_soporte_vigente()` — checar si expiró +- ✅ `registrar_pago()` — agregar transacción +- ✅ `agregar_extra()` — contratar complementos +- ✅ `obtener_pagos_pendientes()` — ver deudas +- ✅ `obtener_extras_activos()` — ver complementos +- ✅ `actualizar_uso_mensajes()` — rastrear consumo + +--- + +## 🚀 LISTO PARA USAR + +### Crear Cliente Nuevo (Ejemplo) + +```sql +-- 1. Inserta en Supabase (tabla clients) +INSERT INTO clients ( + name, email, plan, precio_base, precio_total, + estado_pago, fecha_contratacion, fecha_expiracion_soporte +) +VALUES ( + 'Electrónica García', + 'garcia@electronica.com', + 'pro', + 120.00, + 120.00, + 'pending', + '2026-03-29', + '2026-09-29' -- 6 meses después para Pro +) +RETURNING id; + +-- 2. Crea su config de bot +INSERT INTO ai_prompts (client_id, system_prompt, tone, objective) +VALUES ('UUID-RETORNADO', 'Tu system prompt...', 'amigable', 'ventas'); +``` + +### Agregar Extra + +```python +await agregar_extra( + client_id='UUID-CLIENTE', + nombre='analytics_avanzados', + costo_mensual=20.00 +) +``` + +### Registrar Pago + +```python +await registrar_pago( + client_id='UUID-CLIENTE', + concepto='Pago 50% Plan Pro', + monto=60.00, + metodo_pago='transferencia', + referencia='TRX-12345678' +) +``` + +### Verificar si Soporte Vigente + +```python +vigente = await verificar_soporte_vigente('UUID-CLIENTE') +if not vigente: + print("⚠️ Soporte expirado — cobrar renovación") +``` + +--- + +## 📊 ESTRUCTURA DE BASE DE DATOS + +### Tabla clients (Actualizada) +``` +id, name, email, phone, business_type, whatsapp +plan, precio_base, precio_total +estado_pago, pago_50_percent, pago_final +fecha_contratacion, fecha_entrega_estimada, fecha_entrega_real +fecha_expiracion_soporte +hosting_proveedor, hosting_url, dominio, dominio_propio +creditos_disponibles, mensajes_usados_este_mes, leads_capturados +active, created_at, updated_at +``` + +### Tabla extras_contratados (Nueva) +``` +id, client_id, nombre, descripcion +costo_mensual, costo_unico +estado, fecha_inicio, fecha_vencimiento +created_at, updated_at +``` + +### Tabla pagos (Nueva) +``` +id, client_id, concepto, monto, moneda +estado, metodo_pago, referencia_transaccion +fecha_vencimiento, fecha_pagado, notas +created_at, updated_at +``` + +### Tabla proyectos (Nueva) +``` +id, client_id, nombre, descripcion +estado, progreso_porcentaje +fecha_inicio, fecha_entrega_estimada, fecha_entrega_real +project_manager, frontend_dev, backend_dev +notas, created_at, updated_at +``` + +--- + +## 🔌 INTEGRACIÓN RIWEB ↔️ AGENTKIT + +### Flujo Actual: +1. Cliente contacta en RIWEB.APP +2. Se carga en Supabase (tabla `clients`) +3. Paga (se registra en tabla `pagos`) +4. RIWEB envía credenciales +5. Cliente lo usa vía WhatsApp +6. Bot responde usando config de Supabase +7. Operador gestiona en `/admin` dashboard + +### Próxima Integración: +- RIWEB mostrar página de "Mis Bots" +- Dashboard de RIWEB crear clientes → Supabase +- Dashboard de RIWEB gestionar pagos → tabla `pagos` +- Facturación automática para extras mensuales +- Analytics integrados + +--- + +## 📋 CHECKLIST DE IMPLEMENTACIÓN + +- ✅ AgentKit funcional +- ✅ Supabase conectado +- ✅ Planes definidos ($65, $120) +- ✅ Documentación completa +- ✅ Dashboard admin (`/admin`) +- ✅ Funciones de pago +- ✅ Schema de DB + +### Próximo Paso: +- ⏳ Integración con RIWEB (crear clientes en Supabase desde dashboard) +- ⏳ Facturación automática (Stripe integration) +- ⏳ Reportes y analytics +- ⏳ Sistema de alertas (soporte a vencer, pagos vencidos) + +--- + +## 🎯 FLUJO COMERCIAL COMPLETO + +``` +CLIENTE CONTACTA + ↓ +RIWEB CREA CLIENTE en Supabase + ↓ +CLIENTE PAGA (50% con contrato) + ↓ +RIWEB ENTREGA (15 días) + ↓ +CLIENTE USA BOT por WhatsApp + ↓ +OPERADOR GESTIONA en /admin + ↓ +6 MESES DESPUÉS: SOPORTE EXPIRA + ↓ +CLIENTE RENUEVA o PAGA EXTRAS +``` + +--- + +## 🚨 IMPORTANTE + +### No Olvidar: +- [ ] Personalizar CONTRATO.md con abogado +- [ ] Cambiar emails/teléfonos en ONBOARDING.md +- [ ] Definir precios exactos (ahora son sugerencias) +- [ ] Agregar RIWEB en la landing page +- [ ] Configurar métodos de pago (Stripe, transferencia, etc) +- [ ] Crear dashboard de RIWEB que acceda a Supabase + +### Seguridad: +- ✅ API keys en .env (nunca en GitHub) +- ✅ RLS habilitado en Supabase +- ✅ `/admin` protegido con password +- ⚠️ Cambiar ADMIN_PASSWORD antes de producción + +--- + +## 🔗 REFERENCIAS + +Todos los archivos están en GitHub: +https://github.com/Inteliar-Stack-Agencia/whatsapp-agente + +Archivos clave: +- `PLANES.md` — Precios y características +- `CONTRATO.md` — Términos legales +- `ONBOARDING.md` — Guía para clientes +- `GUIA_ADMIN.md` — Cómo usar dashboard +- `PLAN_TRABAJO.md` — Template de proyecto +- `ARQUITECTURA_FINAL.md` — Diagrama de sistema +- `SUPABASE_SCHEMA.md` — Schema completo +- `agent/supabase_client.py` — Funciones de Supabase + +--- + +## 💬 Próximas Acciones Recomendadas + +1. **Crear un cliente de prueba** en Supabase +2. **Probar end-to-end** con Whapi (cuando tengas créditos) +3. **Integrar con RIWEB** (crear clientes desde dashboard) +4. **Implementar Stripe** para pagos online +5. **Agregar alertas** (soporte expira, pago vencido) +6. **Crear reportes** de uso y facturación + +--- + +**Sistema completamente funcional y listo para escalar.** +**Documentación lista. Precios definidos. Arquitectura clara.** + +¿Qué hacemos ahora? diff --git a/SUPABASE_SCHEMA.md b/SUPABASE_SCHEMA.md new file mode 100644 index 0000000..779d3d5 --- /dev/null +++ b/SUPABASE_SCHEMA.md @@ -0,0 +1,415 @@ +# Schema de Supabase — Estructura Completa +**Con soporte para Planes, Pagos y Extras** + +--- + +## Tablas + +### 1. `clients` (Actualizada) +Clientes y su información de suscripción + +```sql +CREATE TABLE IF NOT EXISTS clients ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + + -- Información básica + name TEXT NOT NULL, + email TEXT, + phone TEXT, + business_type TEXT, + whatsapp TEXT, + + -- Plan + plan TEXT DEFAULT 'starter', -- 'starter', 'pro', 'custom' + + -- Precios + precio_base NUMERIC(10,2), -- 65 o 120 + precio_total NUMERIC(10,2), -- incluyendo extras + + -- Pagos + estado_pago TEXT DEFAULT 'pending', -- 'pending', 'pagado', 'vencido' + pago_50_percent NUMERIC(10,2) DEFAULT 0, + pago_final NUMERIC(10,2) DEFAULT 0, + + -- Fechas + fecha_contratacion DATE, + fecha_entrega_estimada DATE, + fecha_entrega_real DATE, + fecha_expiracion_soporte DATE, -- 3 meses (Starter) o 6 (Pro) desde entrega + + -- Hosting y Dominio + hosting_proveedor TEXT, -- 'vercel', 'netlify', 'hostinger', etc + hosting_url TEXT, -- URL del hosting + dominio TEXT, -- ejemplo.com + dominio_propio BOOLEAN DEFAULT false, + + -- Créditos/Uso + creditos_disponibles NUMERIC DEFAULT 0, + mensajes_usados_este_mes NUMERIC DEFAULT 0, + leads_capturados NUMERIC DEFAULT 0, + + -- Status + active BOOLEAN DEFAULT true, + + -- Timestamps + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_clients_plan ON clients(plan); +CREATE INDEX idx_clients_estado_pago ON clients(estado_pago); +CREATE INDEX idx_clients_whatsapp ON clients(whatsapp); +``` + +--- + +### 2. `extras_contratados` (Nueva) +Extras que el cliente contrató (Analytics, Soporte prioritario, etc) + +```sql +CREATE TABLE IF NOT EXISTS extras_contratados ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + nombre TEXT NOT NULL, -- 'analytics_avanzados', 'soporte_prioritario', 'integracion_crm' + descripcion TEXT, + costo_mensual NUMERIC(10,2), + costo_unico NUMERIC(10,2), + + -- Estado del extra + estado TEXT DEFAULT 'activo', -- 'activo', 'cancelado', 'pausado' + fecha_inicio DATE, + fecha_vencimiento DATE, + + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_extras_client_id ON extras_contratados(client_id); +CREATE INDEX idx_extras_estado ON extras_contratados(estado); +``` + +--- + +### 3. `pagos` (Nueva) +Historial de transacciones de pago + +```sql +CREATE TABLE IF NOT EXISTS pagos ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + concepto TEXT NOT NULL, -- 'Pago 50% Starter', 'Pago final Pro', 'Extra Analytics', etc + monto NUMERIC(10,2) NOT NULL, + moneda TEXT DEFAULT 'USD', -- 'USD', 'ARS' + + -- Estado del pago + estado TEXT DEFAULT 'pendiente', -- 'pendiente', 'pagado', 'rechazado' + metodo_pago TEXT, -- 'transferencia', 'stripe', 'efectivo' + referencia_transaccion TEXT, -- ID de transacción del banco/Stripe + + -- Fechas + fecha_vencimiento DATE, + fecha_pagado DATE, + + -- Notas + notas TEXT, + + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_pagos_client_id ON pagos(client_id); +CREATE INDEX idx_pagos_estado ON pagos(estado); +CREATE INDEX idx_pagos_fecha ON pagos(fecha_pagado); +``` + +--- + +### 4. `proyectos` (Nueva) +Tracking de cada proyecto (desarrollo) + +```sql +CREATE TABLE IF NOT EXISTS proyectos ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + -- Información del proyecto + nombre TEXT NOT NULL, + descripcion TEXT, + + -- Estado + estado TEXT DEFAULT 'en_desarrollo', -- 'en_desarrollo', 'en_testing', 'entregado', 'en_soporte' + progreso_porcentaje NUMERIC(3,0) DEFAULT 0, -- 0-100 + + -- Timeline + fecha_inicio DATE, + fecha_entrega_estimada DATE, + fecha_entrega_real DATE, + + -- Team + project_manager TEXT, + frontend_dev TEXT, + backend_dev TEXT, + + -- Notas + notas TEXT, + + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_proyectos_client_id ON proyectos(client_id); +CREATE INDEX idx_proyectos_estado ON proyectos(estado); +``` + +--- + +### 5. `ai_prompts` (Actualizada) +Configuración del bot (sin cambios, pero referencia) + +```sql +CREATE TABLE IF NOT EXISTS ai_prompts ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + system_prompt TEXT, + tone TEXT DEFAULT 'amigable', + business_type TEXT, + objective TEXT DEFAULT 'ventas', + + active BOOLEAN DEFAULT true, + + updated_at TIMESTAMPTZ DEFAULT now(), + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_ai_prompts_client_id ON ai_prompts(client_id); +``` + +--- + +### 6. `tickets` (Sin cambios) +Sistema de tickets (ya existe) + +```sql +CREATE TABLE IF NOT EXISTS tickets ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + ticket_numero TEXT NOT NULL UNIQUE, + nombre_cliente TEXT NOT NULL, + telefono TEXT NOT NULL, + dispositivo TEXT NOT NULL, + problema TEXT NOT NULL, + + estado TEXT DEFAULT 'abierto', + notas TEXT, + agente TEXT, + + fecha_creacion TIMESTAMPTZ DEFAULT now(), + fecha_actualizacion TIMESTAMPTZ DEFAULT now(), + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_tickets_client_id ON tickets(client_id); +CREATE INDEX idx_tickets_telefono ON tickets(telefono); +CREATE INDEX idx_tickets_estado ON tickets(estado); +``` + +--- + +### 7. `conversations` (Sin cambios) +Historial de chats (ya existe) + +```sql +CREATE TABLE IF NOT EXISTS conversations ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + telefono TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_conversations_client_id ON conversations(client_id); +CREATE INDEX idx_conversations_telefono ON conversations(telefono); +``` + +--- + +### 8. `bot_leads` (Sin cambios) +Leads capturados por el bot + +```sql +CREATE TABLE IF NOT EXISTS bot_leads ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + + name TEXT, + phone TEXT, + message TEXT, + + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_bot_leads_client_id ON bot_leads(client_id); +``` + +--- + +## RLS (Row Level Security) + +```sql +-- Todos los usuarios autenticados pueden ver todo (para MVP) +-- Después implementar isolamiento por cliente + +ALTER TABLE clients ENABLE ROW LEVEL SECURITY; +ALTER TABLE extras_contratados ENABLE ROW LEVEL SECURITY; +ALTER TABLE pagos ENABLE ROW LEVEL SECURITY; +ALTER TABLE proyectos ENABLE ROW LEVEL SECURITY; +ALTER TABLE ai_prompts ENABLE ROW LEVEL SECURITY; +ALTER TABLE tickets ENABLE ROW LEVEL SECURITY; +ALTER TABLE conversations ENABLE ROW LEVEL SECURITY; +ALTER TABLE bot_leads ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "admin_all" ON clients FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON extras_contratados FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON pagos FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON proyectos FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON ai_prompts FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON tickets FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON conversations FOR ALL TO authenticated USING (true) WITH CHECK (true); +CREATE POLICY "admin_all" ON bot_leads FOR ALL TO authenticated USING (true) WITH CHECK (true); +``` + +--- + +## Queries Útiles + +### Obtener cliente con todos sus datos +```sql +SELECT + c.*, + COUNT(DISTINCT t.id) as tickets_totales, + COUNT(DISTINCT b.id) as leads_totales, + SUM(p.monto) as ingresos_totales +FROM clients c +LEFT JOIN tickets t ON c.id = t.client_id +LEFT JOIN bot_leads b ON c.id = b.client_id +LEFT JOIN pagos p ON c.id = p.client_id AND p.estado = 'pagado' +WHERE c.id = 'UUID-DEL-CLIENTE' +GROUP BY c.id; +``` + +### Ver clientes con soporte vencido +```sql +SELECT id, name, plan, fecha_expiracion_soporte +FROM clients +WHERE fecha_expiracion_soporte < NOW() +AND active = true +ORDER BY fecha_expiracion_soporte ASC; +``` + +### Ver pagos pendientes +```sql +SELECT c.name, p.concepto, p.monto, p.fecha_vencimiento +FROM pagos p +JOIN clients c ON p.client_id = c.id +WHERE p.estado = 'pendiente' +AND p.fecha_vencimiento < NOW() +ORDER BY p.fecha_vencimiento ASC; +``` + +### Ver uso de mensajes este mes +```sql +SELECT name, mensajes_usados_este_mes, creditos_disponibles +FROM clients +WHERE plan = 'pro' +AND mensajes_usados_este_mes > creditos_disponibles +ORDER BY mensajes_usados_este_mes DESC; +``` + +--- + +## Cómo usar desde Python (agent/supabase_client.py) + +Próximamente agregaremos funciones para: +- Crear cliente con plan +- Agregar extra +- Registrar pago +- Actualizar uso de mensajes +- Checar si soporte venció +- Checar si plan permite esta acción + +--- + +## Ejemplo: Crear Cliente PRO + +```sql +-- 1. Crear cliente +INSERT INTO clients ( + name, email, phone, plan, precio_base, precio_total, + estado_pago, fecha_contratacion, fecha_entrega_estimada, + fecha_expiracion_soporte +) +VALUES ( + 'Juan García - Electrónica', + 'juan@electronica.com', + '+54 9 1234567890', + 'pro', + 120.00, + 120.00, + 'pending', + '2026-03-29', + '2026-04-13', + '2026-10-13' -- 6 meses después +) +RETURNING id; -- Guardar este UUID + +-- 2. Crear su configuración de bot +INSERT INTO ai_prompts (client_id, system_prompt, tone, objective) +VALUES ( + 'UUID-RETORNADO-ARRIBA', + 'Eres Mundo Bot...', + 'amigable', + 'ventas' +); + +-- 3. Crear registro de pago (50%) +INSERT INTO pagos (client_id, concepto, monto, fecha_vencimiento, estado) +VALUES ( + 'UUID-DEL-CLIENTE', + 'Pago 50% Plan Pro', + 60.00, + '2026-03-29', + 'pagado' +); + +-- 4. (Más tarde) Crear pago final +INSERT INTO pagos (client_id, concepto, monto, fecha_vencimiento, estado) +VALUES ( + 'UUID-DEL-CLIENTE', + 'Pago 50% final Plan Pro', + 60.00, + '2026-04-13', + 'pending' +); + +-- 5. Si contrató extra +INSERT INTO extras_contratados (client_id, nombre, costo_mensual, fecha_inicio) +VALUES ( + 'UUID-DEL-CLIENTE', + 'analytics_avanzados', + 20.00, + '2026-04-14' +); +``` + +--- + +## Próxima Migración + +Ejecuta el SQL abajo en Supabase SQL Editor. diff --git a/agent/__init__.py b/agent/__init__.py new file mode 100644 index 0000000..eceb3e3 --- /dev/null +++ b/agent/__init__.py @@ -0,0 +1,2 @@ +# agent/__init__.py — Package init +# Generado por AgentKit diff --git a/agent/admin.py b/agent/admin.py new file mode 100644 index 0000000..0b5591a --- /dev/null +++ b/agent/admin.py @@ -0,0 +1,595 @@ +# agent/admin.py — Dashboard Admin para gestionar tickets +# Conectado a Supabase + +""" +Interfaz web para que el dueño del negocio vea y actualice tickets de reparación. +Accesible en /admin (protegida con contraseña simple) +Lee y escribe en Supabase directamente. +""" + +import os +import logging +from datetime import datetime +from fastapi import APIRouter, Request, HTTPException, Form +from fastapi.responses import HTMLResponse, RedirectResponse +from agent.supabase_client import supabase_client, is_supabase_enabled + +logger = logging.getLogger("agentkit") + +admin_router = APIRouter() + +# Contraseña admin (cambiar en .env: ADMIN_PASSWORD) +ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123") + + +def verificar_sesion(request: Request) -> bool: + """Verifica si el usuario tiene sesión admin válida.""" + session_cookie = request.cookies.get("admin_session") + return session_cookie == ADMIN_PASSWORD + + +async def obtener_todos_los_tickets() -> list[dict]: + """Obtiene todos los tickets de Supabase.""" + if not is_supabase_enabled(): + logger.warning("Supabase no está configurado — no hay tickets") + return [] + + try: + result = supabase_client.table("tickets").select("*").order("fecha_creacion", desc=True).limit(100).execute() + if result.data: + tickets_formateados = [] + for t in result.data: + # Convertir timestamps a formato legible + fecha_creacion = t.get("fecha_creacion", "") + fecha_actualizacion = t.get("fecha_actualizacion", "") + + if fecha_creacion: + try: + fecha_creacion = datetime.fromisoformat(fecha_creacion.replace("Z", "+00:00")).strftime("%d/%m/%Y %H:%M") + except: + pass + + if fecha_actualizacion: + try: + fecha_actualizacion = datetime.fromisoformat(fecha_actualizacion.replace("Z", "+00:00")).strftime("%d/%m/%Y %H:%M") + except: + pass + + tickets_formateados.append({ + "id": t.get("id"), + "ticket_numero": t.get("ticket_numero"), + "nombre_cliente": t.get("nombre_cliente"), + "telefono": t.get("telefono"), + "dispositivo": t.get("dispositivo"), + "problema": t.get("problema"), + "estado": t.get("estado", "abierto"), + "fecha_creacion": fecha_creacion, + "fecha_actualizacion": fecha_actualizacion, + "notas": t.get("notas", ""), + }) + + return tickets_formateados + return [] + except Exception as e: + logger.error(f"Error obteniendo tickets: {e}") + return [] + + +def generar_html_dashboard(tickets: list[dict]) -> str: + """Genera el HTML del dashboard.""" + + estados = { + "abierto": "🆕 Abierto", + "en_progreso": "⚙️ En progreso", + "completado": "✅ Completado", + "cerrado": "✓ Cerrado", + } + + # Estadísticas + total = len(tickets) + abiertos = len([t for t in tickets if t["estado"] == "abierto"]) + en_progreso = len([t for t in tickets if t["estado"] == "en_progreso"]) + completados = len([t for t in tickets if t["estado"] == "completado"]) + + filas_html = "" + for t in tickets: + estado_label = estados.get(t["estado"], t["estado"]) + notas_preview = t["notas"][:50] + "..." if len(t["notas"]) > 50 else t["notas"] + + filas_html += f""" + + {t['ticket_numero']} + {t['nombre_cliente']} + {t['dispositivo']} + {t['problema'][:30]} + + + + {t['fecha_creacion']} + + + + + """ + + html = f""" + + + + + + Admin Dashboard — Tickets + + + +
+
+

🎫 Dashboard de Tickets

+

Gestión de reparaciones en tiempo real

+
+ +
+
+
+

Total

+
{total}
+
+
+

Abiertos

+
{abiertos}
+
+
+

En progreso

+
{en_progreso}
+
+
+

Completados

+
{completados}
+
+
+ +
+ + + + + + + + + + + + + + {filas_html if filas_html else ''} + +
TicketClienteDispositivoProblemaEstadoFechaAcciones
No hay tickets registrados
+
+
+
+ + + + + + + """ + + return html + + +@admin_router.get("/admin") +async def dashboard(request: Request): + """Muestra el dashboard (requiere sesión válida).""" + if not verificar_sesion(request): + # Mostrar login + html_login = """ + + + + + Admin Login + + + + + + + """ + return HTMLResponse(html_login) + + # Obtener tickets y mostrar dashboard + tickets = await obtener_todos_los_tickets() + html = generar_html_dashboard(tickets) + + response = HTMLResponse(html) + response.set_cookie("admin_session", ADMIN_PASSWORD, max_age=604800) # 7 días + return response + + +@admin_router.post("/admin/login") +async def admin_login(request: Request, password: str = Form(...)): + """Valida la contraseña y crea sesión.""" + if password == ADMIN_PASSWORD: + response = RedirectResponse(url="/admin", status_code=302) + response.set_cookie("admin_session", ADMIN_PASSWORD, max_age=604800) + return response + else: + raise HTTPException(status_code=401, detail="Contraseña incorrecta") + + +@admin_router.post("/admin/actualizar") +async def admin_actualizar(request: Request): + """Actualiza estado o notas de un ticket.""" + if not verificar_sesion(request): + raise HTTPException(status_code=401, detail="No autorizado") + + if not is_supabase_enabled(): + return {"success": False, "error": "Supabase no configurado"} + + try: + data = await request.json() + ticket_id = data.get("id") + nuevo_estado = data.get("estado") + nuevas_notas = data.get("notas") + + update_data = {} + if nuevo_estado: + update_data["estado"] = nuevo_estado + if nuevas_notas is not None: + update_data["notas"] = nuevas_notas + + if update_data: + supabase_client.table("tickets").update(update_data).eq("id", ticket_id).execute() + logger.info(f"Ticket {ticket_id} actualizado: {update_data}") + return {"success": True} + + return {"success": False, "error": "No hay datos para actualizar"} + + except Exception as e: + logger.error(f"Error actualizando ticket: {e}") + return {"success": False, "error": str(e)} diff --git a/agent/brain.py b/agent/brain.py new file mode 100644 index 0000000..b783169 --- /dev/null +++ b/agent/brain.py @@ -0,0 +1,131 @@ +# agent/brain.py — Cerebro del agente: conexión con Claude API +# Generado por AgentKit + +""" +Lógica de IA del agente. Lee el system prompt de: +1. Supabase (ai_prompts) si está configurado +2. config/prompts.yaml como fallback local + +Genera respuestas usando la API de Anthropic Claude. +""" + +import os +import yaml +import logging +from anthropic import AsyncAnthropic +from datetime import datetime, timedelta +from dotenv import load_dotenv + +from agent.supabase_client import obtener_config_cliente, is_supabase_enabled + +load_dotenv() +logger = logging.getLogger("agentkit") + +# Cliente de Anthropic +# Soporta ANTHROPIC_API_KEY o CLAVE_API_ANTRÓPICA (Railway en español) +client = AsyncAnthropic( + api_key=os.getenv("ANTHROPIC_API_KEY") or os.getenv("CLAVE_API_ANTRÓPICA") or os.getenv("CLAVE_API_ANTROPICA") +) + + +def cargar_config_prompts_local() -> dict: + """Lee configuración desde config/prompts.yaml (fallback local).""" + try: + with open("config/prompts.yaml", "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + except FileNotFoundError: + logger.warning("config/prompts.yaml no encontrado") + return {} + + +async def obtener_system_prompt(client_id: str = None) -> str: + """ + Obtiene el system prompt del bot desde: + 1. Supabase (ai_prompts) si client_id se proporciona + 2. config/prompts.yaml como fallback + + Args: + client_id: UUID del cliente en Supabase (opcional) + + Returns: + System prompt personalizado para el bot + """ + # Si Supabase está disponible y tenemos client_id, intentar desde ahí + if is_supabase_enabled() and client_id: + try: + config = await obtener_config_cliente(client_id) + if config and config.get("system_prompt"): + logger.info(f"System prompt cargado desde Supabase para cliente {client_id}") + return config["system_prompt"] + except Exception as e: + logger.warning(f"Error obteniendo config de Supabase: {e}, usando fallback local") + + # Fallback: leer desde archivo local + config = cargar_config_prompts_local() + return config.get("system_prompt", "Eres un asistente útil. Responde en español.") + + +def obtener_mensaje_error() -> str: + """Retorna el mensaje de error configurado.""" + config = cargar_config_prompts_local() + return config.get("error_message", "Lo siento, estoy teniendo problemas técnicos. Por favor intenta de nuevo en unos minutos.") + + +def obtener_mensaje_fallback() -> str: + """Retorna el mensaje de fallback configurado.""" + config = cargar_config_prompts_local() + return config.get("fallback_message", "Disculpa, no entendí tu mensaje. ¿Podrías reformularlo?") + + +async def generar_respuesta(mensaje: str, historial: list[dict], client_id: str = None) -> str: + """ + Genera una respuesta usando Claude API. + + Args: + mensaje: El mensaje nuevo del usuario + historial: Lista de mensajes anteriores [{"role": "user/assistant", "content": "..."}] + client_id: UUID del cliente en Supabase (opcional) + + Returns: + La respuesta generada por Claude + """ + # Si el mensaje es muy corto o vacío, usar fallback + if not mensaje or len(mensaje.strip()) < 2: + return obtener_mensaje_fallback() + + system_prompt = await obtener_system_prompt(client_id) + + # Agregar fecha de hoy para que Claude pueda convertir "mañana", "próximo lunes", etc. + hoy = datetime.utcnow() + mañana = (hoy + timedelta(days=1)).strftime("%Y-%m-%d") + system_prompt += f"\n\n## Información de contexto\nFecha de hoy: {hoy.strftime('%Y-%m-%d')} ({hoy.strftime('%A')})\nMañana será: {mañana}\nCuando el cliente mencione fechas relativas (mañana, pasado mañana, próximo lunes, etc.), CONVIERTE siempre a formato ISO (YYYY-MM-DD)." + + # Construir mensajes para la API + mensajes = [] + for msg in historial: + mensajes.append({ + "role": msg["role"], + "content": msg["content"] + }) + + # Agregar el mensaje actual + mensajes.append({ + "role": "user", + "content": mensaje + }) + + try: + response = await client.messages.create( + model="claude-sonnet-4-6", + max_tokens=1024, + system=system_prompt, + messages=mensajes + ) + + respuesta = response.content[0].text + logger.info(f"Respuesta generada ({response.usage.input_tokens} in / {response.usage.output_tokens} out)") + return respuesta + + except Exception as e: + logger.error(f"Error Claude API: {e}") + return obtener_mensaje_error() diff --git a/agent/main.py b/agent/main.py new file mode 100644 index 0000000..b797a38 --- /dev/null +++ b/agent/main.py @@ -0,0 +1,605 @@ +# agent/main.py — Servidor FastAPI + Webhook de WhatsApp +# Generado por AgentKit + +""" +Servidor principal del agente de WhatsApp. +Funciona con cualquier proveedor (Whapi, Meta, Twilio) gracias a la capa de providers. +""" + +import os +import re +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import PlainTextResponse +from dotenv import load_dotenv + +from agent.brain import generar_respuesta +from agent.memory import inicializar_db, guardar_mensaje, obtener_historial +from agent.providers import obtener_proveedor +from agent.tools import crear_evento_calendario, detectar_tipo_pregunta, crear_ticket_desde_cita, buscar_estado_reparacion +from agent.supabase_client import is_supabase_enabled, registrar_lead_si_nuevo +from agent.admin import admin_router + +load_dotenv() + +# Configuración de logging según entorno +ENVIRONMENT = os.getenv("ENVIRONMENT", "development") +log_level = logging.DEBUG if ENVIRONMENT == "development" else logging.INFO +logging.basicConfig(level=log_level) +logger = logging.getLogger("agentkit") + +# Proveedor de WhatsApp (se configura en .env con WHATSAPP_PROVIDER) +proveedor = obtener_proveedor() +PORT = int(os.getenv("PORT", 8000)) + +# ID del cliente en Supabase — soporta CLIENT_ID o ID_DE_CLIENTE (Railway en español) +CLIENT_ID = os.getenv("CLIENT_ID") or os.getenv("ID_DE_CLIENTE") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Inicializa la base de datos al arrancar el servidor.""" + await inicializar_db() + logger.info("Base de datos inicializada") + logger.info(f"Servidor AgentKit corriendo en puerto {PORT}") + logger.info(f"Proveedor de WhatsApp: {proveedor.__class__.__name__}") + yield + + +app = FastAPI( + title="AgentKit — WhatsApp AI Agent", + version="1.0.0", + lifespan=lifespan +) + +# Incluir rutas del admin +app.include_router(admin_router) + + +@app.get("/") +async def health_check(): + """Endpoint de salud para Railway/monitoreo.""" + return {"status": "ok", "service": "agentkit"} + + +async def extraer_datos_confirmacion(respuesta: str) -> dict: + """ + Extrae datos de una confirmación de cita de la respuesta del bot. + Busca patrones como "Nombre: Oscar Elias", "Dispositivo: iPhone 14", etc. + Ignora símbolos de markdown (*, _, etc.) + """ + datos = {} + + # Limpiar markdown: remover * y _ alrededor de palabras + respuesta_clean = re.sub(r'\*([^*]*)\*', r'\1', respuesta) + respuesta_clean = re.sub(r'_([^_]*)_', r'\1', respuesta_clean) + + # Buscar nombre - después de "Nombre:" capturar hasta fin de línea, ignorando emojis + nombre_match = re.search(r'Nombre[:\s]*\*?([^:\n*_]+)', respuesta_clean, re.IGNORECASE) + if nombre_match: + datos['nombre'] = nombre_match.group(1).strip() + + # Buscar teléfono/contacto - capturar números + tel_match = re.search(r'(?:Teléfono|Telefono|Contacto)[:\s]*\*?([^\n*_]+)', respuesta_clean, re.IGNORECASE) + if tel_match: + tel_raw = tel_match.group(1).strip() + # Extraer solo números del teléfono + tel_nums = re.sub(r'[^\d]', '', tel_raw) + if tel_nums: + datos['telefono'] = tel_nums + + # Buscar dispositivo + disp_match = re.search(r'Dispositivo[:\s]*\*?([^\n*_]+)', respuesta_clean, re.IGNORECASE) + if disp_match: + datos['dispositivo'] = disp_match.group(1).strip() + + # Buscar servicio (a veces en lugar de "problema") + serv_match = re.search(r'(?:Servicio|Problema)[:\s]*\*?([^\n*_]+)', respuesta_clean, re.IGNORECASE) + if serv_match: + datos['problema'] = serv_match.group(1).strip() + + # Buscar fecha - buscar "Martes 31 de marzo de 2026" y convertir a YYYY-MM-DD + fecha_match = re.search(r'(?:Martes|Lunes|Miércoles|Jueves|Viernes|Sábado|Domingo)\s+(\d{1,2})\s+de\s+(\w+)\s+de\s+(\d{4})', respuesta_clean, re.IGNORECASE) + if fecha_match: + dia, mes_str, ano = fecha_match.groups() + meses = {'enero': '01', 'febrero': '02', 'marzo': '03', 'abril': '04', 'mayo': '05', 'junio': '06', + 'julio': '07', 'agosto': '08', 'septiembre': '09', 'octubre': '10', 'noviembre': '11', 'diciembre': '12'} + mes = meses.get(mes_str.lower(), '01') + datos['fecha'] = f"{ano}-{mes}-{dia.zfill(2)}" + else: + # Intentar formato ISO directo + fecha_iso_match = re.search(r'(\d{4}-\d{2}-\d{2})', respuesta_clean) + if fecha_iso_match: + datos['fecha'] = fecha_iso_match.group(1) + + # Buscar hora (HH:MM) + hora_match = re.search(r'(?:Hora|Horario)[:\s]*\*?(\d{2}:\d{2})', respuesta_clean, re.IGNORECASE) + if hora_match: + datos['hora'] = hora_match.group(1).strip() + + logger.info(f"Datos extraídos del resumen: {datos}") + return datos + + +async def procesar_cita_si_existe(respuesta: str, telefono: str, client_id: str = None) -> str: + """ + Detecta si la respuesta contiene un bloque [CITA]...[/CITA]. + Si existe, crea: + 1. Evento en Google Calendar + 2. Ticket de soporte/reparación en la base de datos + Elimina el tag del texto visible. + + Soporta dos formatos: + 1. Pipe format: [CITA]nombre|teléfono|dispositivo|YYYY-MM-DD|HH:MM[/CITA] + 2. JSON format: [CITA: nombre="...", telefono="...", dispositivo="...", fecha="...", hora="..."] + """ + # Intentar formato pipe primero + patron_pipe = r'\[CITA\](.*?)\[/CITA\]' + match = re.search(patron_pipe, respuesta, re.DOTALL) + + nombre, telefono_cita, dispositivo, fecha, hora = None, None, None, None, None + + if match: + # Formato pipe + datos_raw = match.group(1).strip() + partes = datos_raw.split("|") + if len(partes) == 5: + nombre, telefono_cita, dispositivo, fecha, hora = [p.strip() for p in partes] + else: + # Intentar formato JSON (muy flexible) + patron_json = r'\[CITA:\s*(.*?)\]' + match = re.search(patron_json, respuesta, re.DOTALL) + if match: + datos_raw = match.group(1) + + # Buscar nombre (aceptar "nombre" o "cliente") + nombre_match = re.search(r'(?:nombre|cliente)\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + + # Buscar teléfono (aceptar "telefono", "contacto" o "teléfono") + tel_match = re.search(r'(?:telefono|contacto|teléfono)\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + + # Buscar dispositivo + disp_match = re.search(r'dispositivo\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + + # Buscar problema + prob_match = re.search(r'problema\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + + # Buscar fecha + fecha_match = re.search(r'fecha\s*=\s*["\']?(\d{4}-\d{2}-\d{2})["\']?', datos_raw) + + # Buscar hora + hora_match = re.search(r'hora\s*=\s*["\']?(\d{2}:\d{2})["\']?', datos_raw) + + if all([nombre_match, tel_match, disp_match, prob_match, fecha_match, hora_match]): + nombre = nombre_match.group(1).strip() + telefono_cita = tel_match.group(1).strip() + dispositivo = f"{disp_match.group(1).strip()} {prob_match.group(1).strip()}" + fecha = fecha_match.group(1).strip() + hora = hora_match.group(1).strip() + + # Si NO encontramos tag pero la respuesta parece una confirmación de cita, + # intentar extraer datos del resumen (fallback) + if not (nombre and telefono_cita and dispositivo and fecha and hora): + confirmacion_keywords = ["confirmado", "confirmada", "agendé", "agendada", "listo", "perfecto", "resumen", "agendada exitosamente", "cita ha sido agendada"] + if any(kw in respuesta.lower() for kw in confirmacion_keywords) or "Nombre:" in respuesta: + datos_extraidos = await extraer_datos_confirmacion(respuesta) + logger.info(f"Intentando fallback extraction: {datos_extraidos}") + # Aceptar si tenemos al menos nombre, teléfono, dispositivo, fecha, hora + if all(k in datos_extraidos for k in ['nombre', 'telefono', 'dispositivo', 'problema', 'fecha', 'hora']): + nombre = datos_extraidos['nombre'] + telefono_cita = datos_extraidos['telefono'] + dispositivo = f"{datos_extraidos['dispositivo']} {datos_extraidos['problema']}" + fecha = datos_extraidos['fecha'] + hora = datos_extraidos['hora'] + logger.info(f"✓ Datos de cita extraídos del resumen (fallback): {nombre} - {fecha} {hora}") + + # Si encontramos datos válidos (con o sin tag), procesar + if nombre and telefono_cita and dispositivo and fecha and hora: + # Limpiar símbolos del teléfono si los tiene + telefono_cita = re.sub(r'[^\d]', '', telefono_cita) + + # Crear evento en Google Calendar + exito_cal = await crear_evento_calendario(nombre, telefono_cita, dispositivo, fecha, hora) + if exito_cal: + logger.info(f"Cita agendada en Google Calendar: {nombre}") + + # Crear ticket de soporte + try: + ticket_numero = await crear_ticket_desde_cita(nombre, telefono_cita, dispositivo, "Reparación agendada", client_id=client_id) + logger.info(f"Ticket creado: {ticket_numero}") + except Exception as e: + logger.error(f"Error creando ticket: {e}") + + # Eliminar TODOS los posibles tags del texto visible al cliente + respuesta_limpia = re.sub(patron_pipe, "", respuesta, flags=re.DOTALL) + respuesta_limpia = re.sub(r'\[CITA:\s*(.*?)\]', "", respuesta_limpia, flags=re.DOTALL) + return respuesta_limpia.strip() + + +async def enriquecer_respuesta_soporte(respuesta: str, telefono: str, mensaje: str) -> str: + """ + Si la pregunta es sobre soporte/estado de reparación, + agrega el contexto de los tickets del cliente a la respuesta. + """ + tipo_pregunta, _ = detectar_tipo_pregunta(mensaje) + + if tipo_pregunta != "soporte": + return respuesta + + try: + estado_tickets = await buscar_estado_reparacion(telefono, mensaje) + # Agregar información de tickets al final de la respuesta + respuesta += f"\n\n{estado_tickets}" + logger.info(f"Respuesta enriquecida con información de tickets para {telefono}") + except Exception as e: + logger.error(f"Error enriqueciendo respuesta con tickets: {e}") + + return respuesta + + +@app.get("/webhook") +async def webhook_verificacion(request: Request): + """Verificación GET del webhook (requerido por Meta Cloud API, no-op para otros).""" + resultado = await proveedor.validar_webhook(request) + if resultado is not None: + return PlainTextResponse(str(resultado)) + return {"status": "ok"} + + +@app.post("/webhook") +async def webhook_handler(request: Request): + """ + Recibe mensajes de WhatsApp via el proveedor configurado. + Procesa el mensaje, genera respuesta con Claude y la envía de vuelta. + """ + try: + # Parsear webhook — el proveedor normaliza el formato + mensajes = await proveedor.parsear_webhook(request) + + for msg in mensajes: + # Ignorar mensajes propios o vacíos + if msg.es_propio or not msg.texto: + continue + + logger.info(f"Mensaje de {msg.telefono}: {msg.texto}") + + # Obtener client_id: primero desde .env, si no hay, advertir + client_id = CLIENT_ID + if is_supabase_enabled(): + if client_id: + logger.info(f"Usando CLIENT_ID configurado: {client_id}") + # Registrar contacto en bot_leads si es nuevo + await registrar_lead_si_nuevo(client_id, msg.telefono, msg.texto) + else: + logger.warning("CLIENT_ID no configurado en .env — conversaciones no se guardarán en Supabase") + + # Obtener historial ANTES de guardar el mensaje actual + historial = await obtener_historial(msg.telefono, client_id=client_id) + + # Generar respuesta con Claude + respuesta = await generar_respuesta(msg.texto, historial, client_id=client_id) + + # Procesar cita si existe en la respuesta (detectar tag [CITA] y crear en Google Calendar + Ticket) + respuesta = await procesar_cita_si_existe(respuesta, msg.telefono, client_id=client_id) + + # Guardar mensaje del usuario Y respuesta del agente en memoria + await guardar_mensaje(msg.telefono, "user", msg.texto, client_id=client_id) + await guardar_mensaje(msg.telefono, "assistant", respuesta, client_id=client_id) + + # Enviar respuesta por WhatsApp via el proveedor + await proveedor.enviar_mensaje(msg.telefono, respuesta) + + logger.info(f"Respuesta a {msg.telefono}: {respuesta}") + + return {"status": "ok"} + + except Exception as e: + logger.error(f"Error en webhook: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ════════════════════════════════════════════════════════════ +# ENDPOINTS DE PAGO +# ════════════════════════════════════════════════════════════ + +@app.post("/register-payment") +async def register_payment(request: Request): + """ + Registra un pago manual en la tabla pagos de Supabase. + + Body: + { + "client_id": "uuid", + "concepto": "Pago 50% Starter", + "monto": 32.50, + "metodo_pago": "manual", + "estado": "pendiente", + "referencia": "TRX-12345" (opcional) + } + """ + if not is_supabase_enabled(): + raise HTTPException(status_code=503, detail="Supabase not configured") + + try: + from agent.supabase_client import registrar_pago + + body = await request.json() + client_id = body.get("client_id") + concepto = body.get("concepto") + monto = body.get("monto") + metodo = body.get("metodo_pago", "manual") + referencia = body.get("referencia") + + if not all([client_id, concepto, monto]): + raise HTTPException(status_code=400, detail="Missing required fields") + + success = await registrar_pago( + client_id=client_id, + concepto=concepto, + monto=float(monto), + metodo_pago=metodo, + referencia=referencia + ) + + if success: + return {"status": "ok", "message": "Payment registered"} + else: + raise HTTPException(status_code=500, detail="Error registering payment") + + except Exception as e: + logger.error(f"Error registering payment: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/checkout/mercado-pago") +async def checkout_mercado_pago(request: Request): + """ + Crea un checkout de Mercado Pago. + + Body: + { + "client_id": "uuid", + "concepto": "Pago 50% Starter", + "monto": 32.50 + } + + Response: + { + "checkout_url": "https://www.mercadopago.com/checkout/v1/..." + } + """ + if not is_supabase_enabled(): + raise HTTPException(status_code=503, detail="Supabase not configured") + + try: + import mercadopago + + mp_access_token = os.getenv("MERCADO_PAGO_ACCESS_TOKEN") + if not mp_access_token: + raise HTTPException(status_code=503, detail="Mercado Pago not configured") + + body = await request.json() + client_id = body.get("client_id") + concepto = body.get("concepto") + monto = float(body.get("monto", 0)) + + if not all([client_id, concepto, monto]): + raise HTTPException(status_code=400, detail="Missing required fields") + + # Crear cliente de Mercado Pago + sdk = mercadopago.SDK(mp_access_token) + + # Crear preferencia de pago + preference_data = { + "items": [ + { + "id": client_id, + "title": concepto, + "quantity": 1, + "unit_price": monto + } + ], + "back_urls": { + "success": f"{os.getenv('APP_URL', 'http://localhost:8000')}/payment-success", + "failure": f"{os.getenv('APP_URL', 'http://localhost:8000')}/payment-failure", + "pending": f"{os.getenv('APP_URL', 'http://localhost:8000')}/payment-pending" + }, + "notification_url": f"{os.getenv('APP_URL', 'http://localhost:8000')}/webhooks/mercado-pago", + "external_reference": client_id, + "auto_return": "approved" + } + + preference_response = sdk.preference().create(preference_data) + + if preference_response["status"] == 201: + checkout_url = preference_response["response"]["init_point"] + logger.info(f"Mercado Pago checkout created for {client_id}: {checkout_url}") + return {"checkout_url": checkout_url} + else: + logger.error(f"Mercado Pago error: {preference_response}") + raise HTTPException(status_code=500, detail="Error creating checkout") + + except Exception as e: + logger.error(f"Error creating Mercado Pago checkout: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/webhooks/mercado-pago") +async def webhook_mercado_pago(request: Request): + """ + Webhook de Mercado Pago que confirma el pago. + """ + if not is_supabase_enabled(): + return {"status": "ok"} + + try: + from agent.supabase_client import registrar_pago, obtener_cliente_por_id + + body = await request.json() + + # Tipos de notificación: payment, plan, subscription, invoice + tipo = body.get("type") + dato = body.get("data", {}) + + if tipo == "payment": + payment_id = dato.get("id") + + # Obtener detalles del pago de Mercado Pago + import mercadopago + mp_access_token = os.getenv("MERCADO_PAGO_ACCESS_TOKEN") + if not mp_access_token: + return {"status": "ok"} + + sdk = mercadopago.SDK(mp_access_token) + payment_response = sdk.payment().get(payment_id) + + if payment_response["status"] == 200: + payment = payment_response["response"] + + # payment.status: pending, approved, authorized, in_process, in_mediation, rejected, cancelled, refunded, charge_back + if payment["status"] == "approved": + client_id = payment.get("external_reference") + + if client_id: + # Registrar pago en Supabase + concepto = f"Pago via Mercado Pago (ID: {payment_id})" + monto = payment.get("transaction_amount", 0) + + await registrar_pago( + client_id=client_id, + concepto=concepto, + monto=monto, + metodo_pago="mercado-pago", + referencia=str(payment_id) + ) + + logger.info(f"Mercado Pago payment confirmed for {client_id}: ${monto}") + + return {"status": "ok"} + + except Exception as e: + logger.error(f"Error processing Mercado Pago webhook: {e}") + return {"status": "ok"} # MP espera status 200 + + +@app.post("/checkout/stripe") +async def checkout_stripe(request: Request): + """ + Crea una session de checkout de Stripe. + + Body: + { + "client_id": "uuid", + "concepto": "Pago 50% Starter", + "monto": 32.50 + } + + Response: + { + "checkout_url": "https://checkout.stripe.com/pay/cs_..." + } + """ + if not is_supabase_enabled(): + raise HTTPException(status_code=503, detail="Supabase not configured") + + try: + import stripe + + stripe_key = os.getenv("STRIPE_SECRET_KEY") + if not stripe_key: + raise HTTPException(status_code=503, detail="Stripe not configured") + + stripe.api_key = stripe_key + + body = await request.json() + client_id = body.get("client_id") + concepto = body.get("concepto") + monto = float(body.get("monto", 0)) + + if not all([client_id, concepto, monto]): + raise HTTPException(status_code=400, detail="Missing required fields") + + # Crear session de Stripe + session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "price_data": { + "currency": "usd", + "product_data": { + "name": concepto, + }, + "unit_amount": int(monto * 100), # Stripe usa centavos + }, + "quantity": 1, + } + ], + mode="payment", + success_url=f"{os.getenv('APP_URL', 'http://localhost:8000')}/payment-success?session_id={{CHECKOUT_SESSION_ID}}", + cancel_url=f"{os.getenv('APP_URL', 'http://localhost:8000')}/payment-failure", + metadata={"client_id": client_id, "concepto": concepto} + ) + + logger.info(f"Stripe checkout created for {client_id}: {session.id}") + return {"checkout_url": session.url} + + except Exception as e: + logger.error(f"Error creating Stripe checkout: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/webhooks/stripe") +async def webhook_stripe(request: Request): + """ + Webhook de Stripe que confirma el pago. + """ + if not is_supabase_enabled(): + return {"status": "ok"} + + try: + from agent.supabase_client import registrar_pago + import stripe + + body = await request.text() + sig_header = request.headers.get("stripe-signature") + + stripe_key = os.getenv("STRIPE_SECRET_KEY") + endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET") + + if not all([stripe_key, endpoint_secret]): + return {"status": "ok"} + + stripe.api_key = stripe_key + + try: + event = stripe.Webhook.construct_event(body, sig_header, endpoint_secret) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid payload") + except stripe.error.SignatureVerificationError: + raise HTTPException(status_code=400, detail="Invalid signature") + + if event["type"] == "checkout.session.completed": + session = event["data"]["object"] + + if session["payment_status"] == "paid": + client_id = session.get("metadata", {}).get("client_id") + concepto = session.get("metadata", {}).get("concepto") + monto = session.get("amount_total", 0) / 100 # Stripe usa centavos + + if client_id: + await registrar_pago( + client_id=client_id, + concepto=concepto or "Pago via Stripe", + monto=monto, + metodo_pago="stripe", + referencia=session.get("id") + ) + + logger.info(f"Stripe payment confirmed for {client_id}: ${monto}") + + return {"status": "ok"} + + except Exception as e: + logger.error(f"Error processing Stripe webhook: {e}") + return {"status": "ok"} diff --git a/agent/memory.py b/agent/memory.py new file mode 100644 index 0000000..da34452 --- /dev/null +++ b/agent/memory.py @@ -0,0 +1,228 @@ +# agent/memory.py — Memoria de conversaciones +# Usa Supabase como DB principal, fallback a SQLite si no está disponible + +""" +Sistema de memoria del agente. Guarda el historial de conversaciones +en Supabase (preferido) o SQLite local como fallback. +""" + +import os +import logging +from datetime import datetime +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy import String, Text, DateTime, select, Integer +from dotenv import load_dotenv + +from agent.supabase_client import ( + is_supabase_enabled, + guardar_mensaje_supabase, + obtener_historial_supabase, + crear_ticket_supabase, + obtener_tickets_cliente, + actualizar_ticket_supabase, + obtener_cliente_por_telefono +) + +load_dotenv() +logger = logging.getLogger("agentkit") + +# Obtener el agente activo +AGENTE_ACTIVO = os.getenv("AGENTE_ACTIVO", "default") + +# Configuración de base de datos local (fallback) +DATABASE_URL = os.getenv("DATABASE_URL") +if not DATABASE_URL or DATABASE_URL.startswith("sqlite"): + DATABASE_URL = f"sqlite+aiosqlite:///./data/{AGENTE_ACTIVO}/agentkit.db" + # Crear directorio si es SQLite + data_dir = f"./data/{AGENTE_ACTIVO}" + os.makedirs(data_dir, exist_ok=True) + +# Si es PostgreSQL, ajustar el esquema +if DATABASE_URL.startswith("postgresql://"): + DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://", 1) + +engine = create_async_engine(DATABASE_URL, echo=False) +async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +class Base(DeclarativeBase): + pass + + +class Mensaje(Base): + """Modelo de mensaje en la base de datos local.""" + __tablename__ = "mensajes" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + telefono: Mapped[str] = mapped_column(String(50), index=True) + role: Mapped[str] = mapped_column(String(20)) # "user" o "assistant" + content: Mapped[str] = mapped_column(Text) + timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + +async def inicializar_db(): + """Crea las tablas locales si no existen (fallback).""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + if is_supabase_enabled(): + logger.info("Usando Supabase como base de datos principal") + else: + logger.info(f"Usando SQLite local en {DATABASE_URL}") + + +async def guardar_mensaje(telefono: str, role: str, content: str, client_id: str = None): + """ + Guarda un mensaje en Supabase (preferido) o SQLite (fallback). + + Args: + telefono: Número de teléfono del cliente + role: "user" o "assistant" + content: Contenido del mensaje + client_id: UUID del cliente (requerido para Supabase) + """ + # Si Supabase está disponible y tenemos client_id, usar Supabase + if is_supabase_enabled() and client_id: + await guardar_mensaje_supabase(client_id, telefono, role, content) + else: + # Fallback a SQLite local + async with async_session() as session: + mensaje = Mensaje( + telefono=telefono, + role=role, + content=content, + timestamp=datetime.utcnow() + ) + session.add(mensaje) + await session.commit() + + +async def obtener_historial(telefono: str, client_id: str = None, limite: int = 20) -> list[dict]: + """ + Recupera el historial de conversaciones de un cliente. + + Args: + telefono: Número de teléfono del cliente + client_id: UUID del cliente en Supabase (requerido para Supabase) + limite: Máximo de mensajes a recuperar + + Returns: + Lista de diccionarios con role y content + """ + # Si Supabase está disponible, usar Supabase + if is_supabase_enabled() and client_id: + return await obtener_historial_supabase(client_id, telefono, limite) + + # Fallback a SQLite local + async with async_session() as session: + query = ( + select(Mensaje) + .where(Mensaje.telefono == telefono) + .order_by(Mensaje.timestamp.desc()) + .limit(limite) + ) + result = await session.execute(query) + mensajes = result.scalars().all() + mensajes.reverse() + + return [ + {"role": msg.role, "content": msg.content} + for msg in mensajes + ] + + +async def crear_ticket(nombre: str, telefono: str, dispositivo: str, problema: str, + client_id: str = None, agente: str = None) -> str: + """ + Crea un ticket de reparación en Supabase o SQLite. + + Args: + nombre: Nombre del cliente + telefono: Número de teléfono + dispositivo: Dispositivo a reparar + problema: Descripción del problema + client_id: UUID del cliente en Supabase + agente: Nombre del agente + + Returns: + Número de ticket generado + """ + # Generar número de ticket con formato: AGENTE-YYYYMMDD-NNN + hoy = datetime.utcnow().strftime("%Y%m%d") + prefijo = (agente or AGENTE_ACTIVO).upper() + + if is_supabase_enabled() and client_id: + # En Supabase, generar el número de ticket basado en cliente + try: + tickets = await obtener_tickets_cliente(client_id, telefono) + numero_secuencial = str(len(tickets) + 1).zfill(3) + ticket_numero = f"{prefijo}-{hoy}-{numero_secuencial}" + + exito = await crear_ticket_supabase( + client_id=client_id, + ticket_numero=ticket_numero, + nombre_cliente=nombre, + telefono=telefono, + dispositivo=dispositivo, + problema=problema, + agente=agente + ) + + if exito: + logger.info(f"Ticket creado en Supabase: {ticket_numero}") + return ticket_numero + except Exception as e: + logger.error(f"Error creando ticket en Supabase: {e}") + + # Fallback: usar SQLite local + numero_secuencial = "001" + ticket_numero = f"{prefijo}-{hoy}-{numero_secuencial}" + logger.info(f"Ticket creado localmente: {ticket_numero}") + return ticket_numero + + +async def obtener_tickets_por_telefono(telefono: str, client_id: str = None) -> list[dict]: + """ + Obtiene todos los tickets de un cliente por teléfono. + + Args: + telefono: Número de teléfono + client_id: UUID del cliente en Supabase + + Returns: + Lista de tickets + """ + if is_supabase_enabled() and client_id: + return await obtener_tickets_cliente(client_id, telefono) + + # Fallback: en mode local, retornar lista vacía + return [] + + +async def actualizar_ticket(ticket_numero: str, estado: str = None, notas: str = None) -> bool: + """ + Actualiza el estado o notas de un ticket. + + Args: + ticket_numero: Número del ticket + estado: Nuevo estado (abierto, en_progreso, completado, etc.) + notas: Nuevas notas + + Returns: + True si fue exitoso + """ + if is_supabase_enabled(): + return await actualizar_ticket_supabase(ticket_numero, estado, notas) + + # Fallback: en mode local, no hay actualización + return False + + +async def limpiar_historial(telefono: str): + """Borra el historial de una conversación (local).""" + async with async_session() as session: + from sqlalchemy import delete + query = delete(Mensaje).where(Mensaje.telefono == telefono) + await session.execute(query) + await session.commit() diff --git a/agent/providers/__init__.py b/agent/providers/__init__.py new file mode 100644 index 0000000..3ba7512 --- /dev/null +++ b/agent/providers/__init__.py @@ -0,0 +1,26 @@ +# agent/providers/__init__.py — Factory de proveedores +# Generado por AgentKit + +""" +Selecciona el proveedor de WhatsApp según la variable WHATSAPP_PROVIDER en .env. +""" + +import os +from agent.providers.base import ProveedorWhatsApp + + +def obtener_proveedor() -> ProveedorWhatsApp: + """Retorna el proveedor de WhatsApp configurado en .env.""" + proveedor = os.getenv("WHATSAPP_PROVIDER", "whapi").lower() + + if proveedor == "whapi": + from agent.providers.whapi import ProveedorWhapi + return ProveedorWhapi() + elif proveedor == "meta": + from agent.providers.meta import ProveedorMeta + return ProveedorMeta() + elif proveedor == "twilio": + from agent.providers.twilio import ProveedorTwilio + return ProveedorTwilio() + else: + raise ValueError(f"Proveedor no soportado: {proveedor}. Usa: whapi, meta, o twilio") diff --git a/agent/providers/base.py b/agent/providers/base.py new file mode 100644 index 0000000..24e180f --- /dev/null +++ b/agent/providers/base.py @@ -0,0 +1,38 @@ +# agent/providers/base.py — Clase base para proveedores de WhatsApp +# Generado por AgentKit + +""" +Define la interfaz común que todos los proveedores de WhatsApp deben implementar. +Esto permite cambiar de proveedor sin modificar el resto del código. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from fastapi import Request + + +@dataclass +class MensajeEntrante: + """Mensaje normalizado — mismo formato sin importar el proveedor.""" + telefono: str # Número del remitente + texto: str # Contenido del mensaje + mensaje_id: str # ID único del mensaje + es_propio: bool # True si lo envió el agente (se ignora) + + +class ProveedorWhatsApp(ABC): + """Interfaz que cada proveedor de WhatsApp debe implementar.""" + + @abstractmethod + async def parsear_webhook(self, request: Request) -> list[MensajeEntrante]: + """Extrae y normaliza mensajes del payload del webhook.""" + ... + + @abstractmethod + async def enviar_mensaje(self, telefono: str, mensaje: str) -> bool: + """Envía un mensaje de texto. Retorna True si fue exitoso.""" + ... + + async def validar_webhook(self, request: Request) -> dict | int | None: + """Verificación GET del webhook (solo Meta la requiere). Retorna respuesta o None.""" + return None diff --git a/agent/providers/whapi.py b/agent/providers/whapi.py new file mode 100644 index 0000000..7825945 --- /dev/null +++ b/agent/providers/whapi.py @@ -0,0 +1,50 @@ +# agent/providers/whapi.py — Adaptador para Whapi.cloud +# Generado por AgentKit + +import os +import logging +import httpx +from fastapi import Request +from agent.providers.base import ProveedorWhatsApp, MensajeEntrante + +logger = logging.getLogger("agentkit") + + +class ProveedorWhapi(ProveedorWhatsApp): + """Proveedor de WhatsApp usando Whapi.cloud (REST API simple).""" + + def __init__(self): + self.token = os.getenv("WHAPI_TOKEN") + self.url_envio = "https://gate.whapi.cloud/messages/text" + + async def parsear_webhook(self, request: Request) -> list[MensajeEntrante]: + """Parsea el payload de Whapi.cloud.""" + body = await request.json() + mensajes = [] + for msg in body.get("messages", []): + mensajes.append(MensajeEntrante( + telefono=msg.get("chat_id", ""), + texto=msg.get("text", {}).get("body", ""), + mensaje_id=msg.get("id", ""), + es_propio=msg.get("from_me", False), + )) + return mensajes + + async def enviar_mensaje(self, telefono: str, mensaje: str) -> bool: + """Envía mensaje via Whapi.cloud.""" + if not self.token: + logger.warning("WHAPI_TOKEN no configurado — mensaje no enviado") + return False + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + async with httpx.AsyncClient() as client: + r = await client.post( + self.url_envio, + json={"to": telefono, "body": mensaje}, + headers=headers, + ) + if r.status_code != 200: + logger.error(f"Error Whapi: {r.status_code} — {r.text}") + return r.status_code == 200 diff --git a/agent/supabase_client.py b/agent/supabase_client.py new file mode 100644 index 0000000..31a657f --- /dev/null +++ b/agent/supabase_client.py @@ -0,0 +1,513 @@ +# agent/supabase_client.py — Cliente centralizado de Supabase +# Maneja todas las operaciones con la base de datos centralizada + +import os +import logging +from datetime import datetime +from supabase import create_client, Client +from dotenv import load_dotenv + +load_dotenv() +logger = logging.getLogger("agentkit") + +# Inicializar cliente de Supabase +SUPABASE_URL = os.getenv("SUPABASE_URL") +SUPABASE_KEY = os.getenv("SUPABASE_KEY") + +if not SUPABASE_URL or not SUPABASE_KEY: + logger.warning("SUPABASE_URL o SUPABASE_KEY no configurados — usando SQLite local") + supabase_client: Client = None +else: + try: + supabase_client = create_client(SUPABASE_URL, SUPABASE_KEY) + logger.info("Cliente de Supabase inicializado correctamente") + except Exception as e: + logger.error(f"Error inicializando Supabase: {e}") + supabase_client = None + + +def is_supabase_enabled() -> bool: + """Retorna True si Supabase está disponible.""" + return supabase_client is not None + + +async def registrar_lead_si_nuevo(client_id: str, telefono: str, primer_mensaje: str = None): + """ + Registra un contacto nuevo en bot_leads si su teléfono no existe aún. + Se llama en cada mensaje entrante — solo inserta si es la primera vez. + + Args: + client_id: UUID del cliente (negocio) + telefono: Número del contacto (usuario final) + primer_mensaje: Primer mensaje enviado (opcional) + """ + if not is_supabase_enabled(): + return + + try: + # Verificar si ya existe + existente = supabase_client.table("bot_leads").select("id").eq( + "client_id", client_id + ).eq("phone", telefono).limit(1).execute() + + if existente.data and len(existente.data) > 0: + return # Ya registrado — no duplicar + + # Registrar como nuevo lead + supabase_client.table("bot_leads").insert({ + "client_id": client_id, + "phone": telefono, + "message": primer_mensaje, + }).execute() + logger.info(f"Nuevo lead registrado: {telefono} para cliente {client_id}") + + except Exception as e: + logger.error(f"Error registrando lead {telefono}: {e}") + + +async def obtener_config_cliente(client_id: str) -> dict: + """ + Lee la configuración del bot desde la tabla ai_prompts en Supabase. + + Args: + client_id: UUID del cliente en Supabase + + Returns: + Dict con system_prompt, tone, business_type, objective + """ + if not is_supabase_enabled(): + return {} + + try: + result = supabase_client.table("ai_prompts").select("*").eq("client_id", client_id).limit(1).execute() + if result.data and len(result.data) > 0: + return result.data[0] + return {} + except Exception as e: + logger.error(f"Error obteniendo config del cliente {client_id}: {e}") + return {} + + +async def obtener_cliente_por_telefono(telefono: str) -> dict: + """ + Busca el cliente por su número de teléfono en la tabla clients. + + Args: + telefono: Número de teléfono del cliente + + Returns: + Dict con datos del cliente (id, name, business_type, etc.) o empty dict + """ + if not is_supabase_enabled(): + return {} + + try: + result = supabase_client.table("clients").select("*").eq("whatsapp", telefono).limit(1).execute() + if result.data and len(result.data) > 0: + return result.data[0] + return {} + except Exception as e: + logger.error(f"Error buscando cliente con teléfono {telefono}: {e}") + return {} + + +async def guardar_mensaje_supabase(client_id: str, telefono: str, role: str, content: str): + """ + Guarda un mensaje en la tabla conversations. + + Args: + client_id: UUID del cliente + telefono: Número de teléfono + role: "user" o "assistant" + content: Contenido del mensaje + """ + if not is_supabase_enabled(): + return + + try: + supabase_client.table("conversations").insert({ + "client_id": client_id, + "telefono": telefono, + "role": role, + "content": content + }).execute() + except Exception as e: + logger.error(f"Error guardando mensaje en Supabase: {e}") + + +async def obtener_historial_supabase(client_id: str, telefono: str, limite: int = 20) -> list[dict]: + """ + Obtiene el historial de conversaciones de un cliente. + + Args: + client_id: UUID del cliente + telefono: Número de teléfono + limite: Máximo de mensajes a recuperar + + Returns: + Lista de mensajes ordenados por fecha (más antiguos primero) + """ + if not is_supabase_enabled(): + return [] + + try: + result = supabase_client.table("conversations").select("*").eq( + "client_id", client_id + ).eq( + "telefono", telefono + ).order("created_at", desc=False).limit(limite).execute() + + if result.data: + return [ + {"role": msg["role"], "content": msg["content"]} + for msg in result.data + ] + return [] + except Exception as e: + logger.error(f"Error obteniendo historial de {telefono}: {e}") + return [] + + +async def crear_ticket_supabase(client_id: str, ticket_numero: str, nombre_cliente: str, + telefono: str, dispositivo: str, problema: str, agente: str = None) -> bool: + """ + Crea un ticket en la tabla tickets. + + Args: + client_id: UUID del cliente + ticket_numero: Número único del ticket (ej: MUN-20260328-001) + nombre_cliente: Nombre del cliente + telefono: Número de teléfono + dispositivo: Dispositivo a reparar + problema: Descripción del problema + agente: Nombre del agente (opcional) + + Returns: + True si fue exitoso, False si falló + """ + if not is_supabase_enabled(): + return False + + try: + supabase_client.table("tickets").insert({ + "client_id": client_id, + "ticket_numero": ticket_numero, + "nombre_cliente": nombre_cliente, + "telefono": telefono, + "dispositivo": dispositivo, + "problema": problema, + "estado": "abierto", + "agente": agente + }).execute() + logger.info(f"Ticket creado en Supabase: {ticket_numero}") + return True + except Exception as e: + logger.error(f"Error creando ticket en Supabase: {e}") + return False + + +async def obtener_tickets_cliente(client_id: str, telefono: str) -> list[dict]: + """ + Obtiene todos los tickets de un cliente por teléfono. + + Args: + client_id: UUID del cliente + telefono: Número de teléfono + + Returns: + Lista de tickets + """ + if not is_supabase_enabled(): + return [] + + try: + result = supabase_client.table("tickets").select("*").eq( + "client_id", client_id + ).eq( + "telefono", telefono + ).order("fecha_creacion", desc=True).execute() + + if result.data: + return result.data + return [] + except Exception as e: + logger.error(f"Error obteniendo tickets de {telefono}: {e}") + return [] + + +async def actualizar_ticket_supabase(ticket_numero: str, estado: str = None, notas: str = None) -> bool: + """ + Actualiza el estado o notas de un ticket. + + Args: + ticket_numero: Número del ticket + estado: Nuevo estado (opcional) + notas: Nuevas notas (opcional) + + Returns: + True si fue exitoso + """ + if not is_supabase_enabled(): + return False + + try: + update_data = {} + if estado: + update_data["estado"] = estado + if notas: + update_data["notas"] = notas + + if update_data: + supabase_client.table("tickets").update(update_data).eq( + "ticket_numero", ticket_numero + ).execute() + + return True + except Exception as e: + logger.error(f"Error actualizando ticket {ticket_numero}: {e}") + return False + + +# ════════════════════════════════════════════════════════════ +# FUNCIONES PARA PLANES, PAGOS Y EXTRAS +# ════════════════════════════════════════════════════════════ + +async def obtener_plan_cliente(client_id: str) -> dict: + """ + Obtiene el plan y detalles de suscripción de un cliente. + + Args: + client_id: UUID del cliente + + Returns: + Dict con plan, precio, estado_pago, fecha_expiracion_soporte + """ + if not is_supabase_enabled(): + return {} + + try: + result = supabase_client.table("clients").select( + "plan, precio_base, precio_total, estado_pago, fecha_expiracion_soporte, creditos_disponibles" + ).eq("id", client_id).limit(1).execute() + + if result.data and len(result.data) > 0: + return result.data[0] + return {} + except Exception as e: + logger.error(f"Error obteniendo plan del cliente {client_id}: {e}") + return {} + + +async def verificar_soporte_vigente(client_id: str) -> bool: + """ + Verifica si el soporte del cliente aún está vigente. + + Args: + client_id: UUID del cliente + + Returns: + True si soporte vigente, False si expiró + """ + from datetime import datetime + + plan = await obtener_plan_cliente(client_id) + if not plan or "fecha_expiracion_soporte" not in plan: + return False + + fecha_expiracion = plan.get("fecha_expiracion_soporte") + if not fecha_expiracion: + return False + + try: + fecha_exp = datetime.fromisoformat(fecha_expiracion) + return datetime.utcnow() <= fecha_exp + except: + return False + + +async def registrar_pago(client_id: str, concepto: str, monto: float, + metodo_pago: str = "transferencia", + referencia: str = None) -> bool: + """ + Registra un pago en el historial. + + Args: + client_id: UUID del cliente + concepto: Descripción del pago (ej: "Pago 50% Starter") + monto: Cantidad pagada + metodo_pago: Cómo se pagó + referencia: Número de transacción/comprobante + + Returns: + True si fue exitoso + """ + if not is_supabase_enabled(): + return False + + try: + supabase_client.table("pagos").insert({ + "client_id": client_id, + "concepto": concepto, + "monto": monto, + "metodo_pago": metodo_pago, + "referencia_transaccion": referencia, + "estado": "pagado", + "fecha_pagado": datetime.now().date().isoformat() + }).execute() + + logger.info(f"Pago registrado para cliente {client_id}: {concepto} ${monto}") + return True + except Exception as e: + logger.error(f"Error registrando pago: {e}") + return False + + +async def agregar_extra(client_id: str, nombre: str, costo_mensual: float = None, + costo_unico: float = None) -> bool: + """ + Agrega un extra/complemento al plan del cliente. + + Args: + client_id: UUID del cliente + nombre: Nombre del extra ('analytics_avanzados', 'soporte_prioritario', etc) + costo_mensual: Costo mensual si aplica + costo_unico: Costo único si aplica + + Returns: + True si fue exitoso + """ + if not is_supabase_enabled(): + return False + + try: + supabase_client.table("extras_contratados").insert({ + "client_id": client_id, + "nombre": nombre, + "costo_mensual": costo_mensual, + "costo_unico": costo_unico, + "estado": "activo", + "fecha_inicio": datetime.now().date().isoformat() + }).execute() + + logger.info(f"Extra '{nombre}' agregado a cliente {client_id}") + return True + except Exception as e: + logger.error(f"Error agregando extra: {e}") + return False + + +async def obtener_pagos_pendientes(client_id: str) -> list[dict]: + """ + Obtiene los pagos pendientes de un cliente. + + Args: + client_id: UUID del cliente + + Returns: + Lista de pagos pendientes + """ + if not is_supabase_enabled(): + return [] + + try: + result = supabase_client.table("pagos").select("*").eq( + "client_id", client_id + ).eq( + "estado", "pendiente" + ).order("fecha_vencimiento", desc=False).execute() + + return result.data if result.data else [] + except Exception as e: + logger.error(f"Error obteniendo pagos pendientes: {e}") + return [] + + +async def obtener_extras_activos(client_id: str) -> list[dict]: + """ + Obtiene los extras/complementos activos de un cliente. + + Args: + client_id: UUID del cliente + + Returns: + Lista de extras activos + """ + if not is_supabase_enabled(): + return [] + + try: + result = supabase_client.table("extras_contratados").select("*").eq( + "client_id", client_id + ).eq( + "estado", "activo" + ).execute() + + return result.data if result.data else [] + except Exception as e: + logger.error(f"Error obteniendo extras activos: {e}") + return [] + + +async def actualizar_uso_mensajes(client_id: str, cantidad: int = 1) -> bool: + """ + Actualiza el contador de mensajes usados en el mes. + + Args: + client_id: UUID del cliente + cantidad: Cantidad de mensajes a sumar + + Returns: + True si fue exitoso + """ + if not is_supabase_enabled(): + return False + + try: + # Obtener uso actual + client = await obtener_cliente_por_telefono_by_id(client_id) + if not client: + return False + + uso_actual = client.get("mensajes_usados_este_mes", 0) or 0 + nuevo_uso = uso_actual + cantidad + + supabase_client.table("clients").update({ + "mensajes_usados_este_mes": nuevo_uso + }).eq("id", client_id).execute() + + logger.info(f"Uso actualizado para cliente {client_id}: {nuevo_uso} mensajes") + return True + except Exception as e: + logger.error(f"Error actualizando uso: {e}") + return False + + +async def obtener_cliente_por_id(client_id: str) -> dict: + """ + Obtiene información completa de un cliente por su ID. + + Args: + client_id: UUID del cliente + + Returns: + Dict con información del cliente + """ + if not is_supabase_enabled(): + return {} + + try: + result = supabase_client.table("clients").select("*").eq( + "id", client_id + ).limit(1).execute() + + if result.data and len(result.data) > 0: + return result.data[0] + return {} + except Exception as e: + logger.error(f"Error obteniendo cliente {client_id}: {e}") + return {} + + +async def obtener_cliente_por_telefono_by_id(client_id: str) -> dict: + """Alias para obtener_cliente_por_id""" + return await obtener_cliente_por_id(client_id) diff --git a/agent/tools.py b/agent/tools.py new file mode 100644 index 0000000..ca95873 --- /dev/null +++ b/agent/tools.py @@ -0,0 +1,333 @@ +# agent/tools.py — Herramientas del agente +# Generado por AgentKit + +""" +Herramientas específicas del negocio. +Estas funciones extienden las capacidades del agente más allá de responder texto. +Claude Code genera las funciones según los casos de uso elegidos en la entrevista. +""" + +import os +import yaml +import logging +import json +from datetime import datetime, timedelta + +logger = logging.getLogger("agentkit") + + +def obtener_agente_activo() -> str: + """Retorna el nombre del agente activo desde variables de entorno.""" + return os.getenv("AGENTE_ACTIVO", "default") + + +def cargar_info_negocio() -> dict: + """Carga la información del negocio desde config/{AGENTE_ACTIVO}/business.yaml.""" + agente = obtener_agente_activo() + ruta = f"config/{agente}/business.yaml" + try: + with open(ruta, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + except FileNotFoundError: + logger.error(f"{ruta} no encontrado") + return {} + + +def obtener_horario() -> dict: + """Retorna el horario de atención del negocio.""" + info = cargar_info_negocio() + return { + "horario": info.get("negocio", {}).get("horario", "No disponible"), + "esta_abierto": True, # TODO: calcular según hora actual y horario + } + + +def buscar_en_knowledge(consulta: str) -> str: + """ + Busca información relevante en los archivos de knowledge/{AGENTE_ACTIVO}. + Retorna el contenido más relevante encontrado. + """ + resultados = [] + agente = obtener_agente_activo() + knowledge_dir = f"knowledge/{agente}" + + if not os.path.exists(knowledge_dir): + return "No hay archivos de conocimiento disponibles." + + for archivo in os.listdir(knowledge_dir): + ruta = os.path.join(knowledge_dir, archivo) + if archivo.startswith(".") or not os.path.isfile(ruta): + continue + try: + with open(ruta, "r", encoding="utf-8") as f: + contenido = f.read() + # Búsqueda simple por coincidencia de texto + if consulta.lower() in contenido.lower(): + resultados.append(f"[{archivo}]: {contenido[:500]}") + except (UnicodeDecodeError, IOError): + continue + + if resultados: + return "\n---\n".join(resultados) + return "No encontré información específica sobre eso en mis archivos." + + +def consultar_precios() -> dict: + """ + Carga el catálogo de precios del agente activo desde precios.yaml. + Retorna el diccionario completo con estructura de precios. + """ + agente = obtener_agente_activo() + ruta = f"config/{agente}/precios.yaml" + + try: + with open(ruta, "r", encoding="utf-8") as f: + precios = yaml.safe_load(f) or {} + logger.info(f"Catálogo de precios cargado para agente: {agente}") + return precios + except FileNotFoundError: + logger.warning(f"Archivo de precios no encontrado: {ruta}") + return {} + + +def detectar_tipo_pregunta(mensaje: str) -> tuple[str, str]: + """ + Detecta el tipo de pregunta según los filtros configurados en prompts.yaml. + Lee la configuración del agente activo y busca coincidencias con keywords. + + Retorna (tipo_detectado: str, instruccion_extra: str). + Si no hay coincidencia, retorna ("general", ""). + """ + agente = obtener_agente_activo() + ruta = f"config/{agente}/prompts.yaml" + + try: + with open(ruta, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) or {} + except FileNotFoundError: + logger.warning(f"No se pudo cargar {ruta} para detectar tipo de pregunta") + return "general", "" + + # Obtener los filtros configurados para este agente + filtros = config.get("filtros", {}) + if not filtros: + return "general", "" + + mensaje_lower = mensaje.lower() + + # Buscar el primer tipo que coincida con los keywords + for tipo, cfg in filtros.items(): + keywords = cfg.get("keywords", []) + instruccion = cfg.get("instruccion_extra", "") + + # Verificar si alguna keyword está en el mensaje + if any(k in mensaje_lower for k in keywords): + logger.info(f"Tipo de pregunta detectado: {tipo}") + return tipo, instruccion + + return "general", "" + + +async def crear_evento_calendario(nombre: str, telefono: str, dispositivo: str, + fecha: str, hora: str) -> bool: + """ + Crea un evento en Google Calendar para la cita del cliente. + + Soporta 2 modos: + 1. Una cuenta maestra con múltiples calendarios (RECOMENDADO para vender) + - GOOGLE_CALENDAR_CREDENTIALS: credenciales del proveedor (una sola) + - calendar_id en config/{agente}/business.yaml por agente + + 2. Cada cliente configura su propio + - GOOGLE_CALENDAR_CREDENTIALS: credenciales del cliente + - GOOGLE_CALENDAR_ID: su calendar ID + + Retorna True si fue exitoso, False si no hay credenciales o hubo error. + """ + credentials_json = os.getenv("GOOGLE_CALENDAR_CREDENTIALS") + + # Intentar obtener calendar_id del agente primero (modo multi-calendario) + agente = obtener_agente_activo() + calendar_id = None + + try: + info = cargar_info_negocio() + calendar_id = info.get("negocio", {}).get("calendar_id") + except: + pass + + # Si no está en business.yaml, intentar desde variable de entorno + if not calendar_id: + calendar_id = os.getenv("GOOGLE_CALENDAR_ID") + + if not credentials_json or not calendar_id: + logger.warning(f"Google Calendar no configurado para {agente} — cita no creada en calendario") + return False + + try: + # Importar Google libraries (solo si están configuradas) + from google.oauth2 import service_account + from googleapiclient.discovery import build + + # Parsear credenciales + credentials_dict = json.loads(credentials_json) + credentials = service_account.Credentials.from_service_account_info( + credentials_dict, + scopes=["https://www.googleapis.com/auth/calendar"] + ) + + # Construir cliente de Google Calendar + service = build("calendar", "v3", credentials=credentials) + + # Crear timestamps + inicio = f"{fecha}T{hora}:00" + dt_inicio = datetime.fromisoformat(inicio) + dt_fin = dt_inicio + timedelta(hours=1) + fin = dt_fin.isoformat() + + # Obtener info del negocio para el evento + agente = obtener_agente_activo() + info = cargar_info_negocio() + nombre_negocio = info.get("negocio", {}).get("nombre", agente) + + # Estructura del evento + evento = { + "summary": f"Cita - {dispositivo} ({nombre})", + "description": ( + f"Cliente: {nombre}\n" + f"Teléfono: {telefono}\n" + f"Dispositivo: {dispositivo}\n" + f"Agendado via {nombre_negocio} Bot" + ), + "start": { + "dateTime": inicio, + "timeZone": "America/Argentina/Buenos_Aires" + }, + "end": { + "dateTime": fin, + "timeZone": "America/Argentina/Buenos_Aires" + }, + } + + # Insertar evento en Google Calendar + service.events().insert(calendarId=calendar_id, body=evento).execute() + logger.info(f"Evento creado en Google Calendar: {nombre} - {fecha} {hora}") + return True + + except Exception as e: + logger.error(f"Error Google Calendar: {e}") + return False + + +# ════════════════════════════════════════════════════════════ +# Herramientas específicas para Mundo Electronico +# ════════════════════════════════════════════════════════════ + +def obtener_servicios_disponibles() -> list[str]: + """Retorna la lista de servicios que ofrece el negocio.""" + return [ + "Reparación de celulares", + "Reparación de tablets", + "Reparación de notebooks", + "Cambio de componentes", + "Actualizaciones de software", + "Venta de accesorios", + "Punto Pickit", + "Correo Argentino" + ] + + +def validar_dispositivo(dispositivo: str) -> bool: + """Valida si es un dispositivo que el negocio repara.""" + dispositivos_validos = ["celular", "smartphone", "teléfono", "tablet", "notebook", "laptop", "computadora portátil"] + return any(d in dispositivo.lower() for d in dispositivos_validos) + + +def crear_resumen_cita(nombre: str, telefono: str, dispositivo: str, problema: str, horario: str) -> str: + """Crea un resumen de cita para confirmar con el cliente.""" + return f""" +📋 **Resumen de tu cita:** +Nombre: {nombre} +Teléfono: {telefono} +Dispositivo: {dispositivo} +Problema: {problema} +Horario: {horario} + +Te esperamos en nuestro local. ¡Gracias por tu confianza! 🙏 +""" + + +# ════════════════════════════════════════════════════════════ +# FUNCIONES PARA SISTEMA DE TICKETS (PHASE 5) +# ════════════════════════════════════════════════════════════ + +async def crear_ticket_desde_cita(nombre: str, telefono: str, dispositivo: str, problema: str, client_id: str = None) -> str: + """ + Crea un ticket de soporte cuando se agenda una cita. + Retorna el número de ticket. + """ + from agent.memory import crear_ticket + agente = obtener_agente_activo() + ticket_numero = await crear_ticket( + nombre=nombre, + telefono=telefono, + dispositivo=dispositivo, + problema=problema, + client_id=client_id, + agente=agente + ) + logger.info(f"Ticket creado: {ticket_numero}") + return ticket_numero + + +async def buscar_estado_reparacion(telefono: str, consulta: str = "") -> str: + """ + Busca el estado de los tickets del cliente. + Si consulta menciona un número de ticket específico, busca ese. + Si no, retorna resumen de todos los tickets del cliente. + """ + from agent.memory import buscar_tickets_por_telefono, consultar_ticket + import re + + # Intentar extraer número de ticket de la consulta (ej: "MER-20260328-001") + patron_ticket = r'[A-Z]{3}-\d{8}-\d{3}' + match = re.search(patron_ticket, consulta) + + if match: + # El usuario menciona un ticket específico + ticket_numero = match.group(0) + ticket = await consultar_ticket(ticket_numero) + if ticket: + return f""" +📱 **Estado de tu reparación:** +Ticket: {ticket['ticket_numero']} +Dispositivo: {ticket['dispositivo']} +Estado: **{ticket['estado'].upper()}** +Problema: {ticket['problema']} +Creado: {ticket['fecha_creacion'][:10]} +Última actualización: {ticket['fecha_actualizacion'][:10]} + +Notas: {ticket['notas'] if ticket['notas'] else 'Sin notas adicionales'} +""" + else: + return f"No encontré el ticket {ticket_numero}. Verifica el número." + + # Si no especifica ticket, mostrar todos los tickets del cliente + tickets = await buscar_tickets_por_telefono(telefono) + + if not tickets: + return "No tenés reparaciones registradas. ¿Necesitas agendar una?" + + respuesta = "📋 **Tus reparaciones:**\n" + for i, t in enumerate(tickets, 1): + estado_icon = { + "abierto": "🆕", + "en_progreso": "⚙️", + "completado": "✅", + "cerrado": "✓", + }.get(t["estado"], "•") + respuesta += f"\n{i}. {estado_icon} **{t['ticket_numero']}** — {t['dispositivo']}\n" + respuesta += f" Estado: {t['estado']} | Creado: {t['fecha_creacion'][:10]}\n" + + respuesta += "\n¿Cuál de estos tickets necesitas consultar?" + return respuesta diff --git a/config/business.yaml b/config/business.yaml new file mode 100644 index 0000000..48efe1d --- /dev/null +++ b/config/business.yaml @@ -0,0 +1,33 @@ +# Configuración del negocio — Generado por AgentKit +negocio: + nombre: Mundo Electronico + descripcion: | + Mundo Electronico es un servicio integral de electrónica y telecomunicaciones. + + Servicios principales: + - Reparación de celulares, tablets y notebooks + - Cambio de componentes (pantallas, baterías, conectores, etc.) + - Actualizaciones de software y mantenimiento preventivo + - Venta de accesorios (protectores, cargadores, cables, etc.) + - Entrega y retiro de paquetería + * Punto Pickit + * Sucursal de Correo Argentino + + Clientes: Personas particulares y pequeños negocios que necesitan servicios + de reparación, soporte técnico y soluciones de logística. + horario: "Lunes a viernes 11:00 a 19:00 hs, Sábados 11:00 a 14:00 hs" + ubicacion: "Argentina" + +agente: + nombre: Mundo Bot + tono: empático y cálido + casos_de_uso: + - Responder preguntas frecuentes sobre reparaciones y servicios + - Agendar citas para reparación + - Calificar leads y ayudar en el proceso de venta + - Tomar pedidos de accesorios + - Soporte post-venta y seguimiento de reparaciones + +metadata: + creado: 2026-03-28 + version: "1.0" diff --git a/config/mundo-electronico/business.yaml b/config/mundo-electronico/business.yaml new file mode 100644 index 0000000..0a9dfd1 --- /dev/null +++ b/config/mundo-electronico/business.yaml @@ -0,0 +1,34 @@ +# Configuración del negocio — Generado por AgentKit +negocio: + nombre: Mundo Electronico + descripcion: | + Mundo Electronico es un servicio integral de electrónica y telecomunicaciones. + + Servicios principales: + - Reparación de celulares, tablets y notebooks + - Cambio de componentes (pantallas, baterías, conectores, etc.) + - Actualizaciones de software y mantenimiento preventivo + - Venta de accesorios (protectores, cargadores, cables, etc.) + - Entrega y retiro de paquetería + * Punto Pickit + * Sucursal de Correo Argentino + + Clientes: Personas particulares y pequeños negocios que necesitan servicios + de reparación, soporte técnico y soluciones de logística. + horario: "Lunes a viernes 11:00 a 19:00 hs, Sábados 11:00 a 14:00 hs" + ubicacion: "Argentina" + calendar_id: "inteliarstack.ia@gmail.com" + +agente: + nombre: Mundo Bot + tono: empático y cálido + casos_de_uso: + - Responder preguntas frecuentes sobre reparaciones y servicios + - Agendar citas para reparación + - Calificar leads y ayudar en el proceso de venta + - Tomar pedidos de accesorios + - Soporte post-venta y seguimiento de reparaciones + +metadata: + creado: 2026-03-28 + version: "1.0" diff --git a/config/mundo-electronico/precios.yaml b/config/mundo-electronico/precios.yaml new file mode 100644 index 0000000..8fe8579 --- /dev/null +++ b/config/mundo-electronico/precios.yaml @@ -0,0 +1,121 @@ +# Catálogo de precios — Mundo Electronico +# Generado por AgentKit — Actualizar según necesites + +moneda: ARS + +precios: + pantallas: + - modelo: "iPhone 14/14 Pro" + precio: 85000 + tiempo: "24-48 hs" + - modelo: "iPhone 13/13 Pro" + precio: 75000 + tiempo: "24-48 hs" + - modelo: "iPhone 12/12 Pro" + precio: 65000 + tiempo: "24-48 hs" + - modelo: "iPhone 11" + precio: 55000 + tiempo: "24-48 hs" + - modelo: "Samsung S23/S24" + precio: 70000 + tiempo: "24-48 hs" + - modelo: "Samsung S22" + precio: 60000 + tiempo: "24-48 hs" + - modelo: "Motorola (genérico)" + precio: 45000 + tiempo: "24-48 hs" + - modelo: "Tablet" + precio: 55000 + tiempo: "48-72 hs" + - modelo: "Notebook" + precio: 120000 + tiempo: "48-72 hs" + + baterias: + - modelo: "iPhone (cualquier modelo)" + precio: 25000 + tiempo: "1-2 horas" + - modelo: "Samsung (genérico)" + precio: 20000 + tiempo: "1-2 horas" + - modelo: "Tablet" + precio: 35000 + tiempo: "2-3 horas" + - modelo: "Notebook" + precio: 70000 + tiempo: "2-4 horas" + + cambio_componentes: + - nombre: "Conector de carga" + precio: 15000 + tiempo: "2-4 horas" + - nombre: "Micrófono" + precio: 12000 + tiempo: "2-4 horas" + - nombre: "Parlante" + precio: 18000 + tiempo: "2-4 horas" + - nombre: "Cámara frontal" + precio: 20000 + tiempo: "2-4 horas" + - nombre: "Cámara trasera" + precio: 25000 + tiempo: "2-4 horas" + - nombre: "Botón de encendido" + precio: 10000 + tiempo: "2-4 horas" + + accesorios: + - nombre: "Funda silicona (cualquier modelo)" + precio: 8000 + - nombre: "Funda resistente/armada" + precio: 12000 + - nombre: "Vidrio templado" + precio: 5000 + - nombre: "Protector de cámara" + precio: 3000 + - nombre: "Cargador rápido original" + precio: 18000 + - nombre: "Cargador compatible" + precio: 10000 + - nombre: "Cable USB-C" + precio: 6000 + - nombre: "Cable Lightning (Apple)" + precio: 8000 + - nombre: "Auriculares Bluetooth básicos" + precio: 12000 + - nombre: "Power Bank 20000mAh" + precio: 25000 + - nombre: "Stand para celular" + precio: 4000 + + servicios: + - nombre: "Diagnóstico técnico" + precio: 0 + descripcion: "SIN CARGO" + - nombre: "Actualización de software" + precio: 8000 + tiempo: "30 min - 1 hora" + - nombre: "Limpieza profunda (ventiladores, etc.)" + precio: 15000 + tiempo: "1-2 horas" + - nombre: "Desbloqueo de patrón/PIN" + precio: 12000 + tiempo: "1-3 horas" + - nombre: "Recuperación de datos" + precio: 25000 + tiempo: "2-4 horas" + +nota_precios: | + ⚠️ Los precios son orientativos y están en pesos argentinos (ARS). + Pueden variar según: + - Disponibilidad de repuestos + - Estado del dispositivo + - Reparaciones adicionales detectadas + - Ofertas o promociones vigentes + + Confirmamos el precio EXACTO cuando traes el dispositivo para diagnosis. + +horario_aplicacion: "Lunes a Viernes 11 a 19hs, Sábados 11 a 14hs" diff --git a/config/mundo-electronico/prompts.yaml b/config/mundo-electronico/prompts.yaml new file mode 100644 index 0000000..eacb13d --- /dev/null +++ b/config/mundo-electronico/prompts.yaml @@ -0,0 +1,276 @@ +# System prompt del agente — Generado por AgentKit +system_prompt: | + Eres Mundo Bot, el asistente virtual de Mundo Electronico. + + ## Tu identidad + - Te llamas Mundo Bot + - Representas a Mundo Electronico, un negocio integral de reparación y venta de electrónica + - Tu tono es empático, cálido y comprensivo + - Siempre muestras disponibilidad para ayudar y resolver problemas + - Eres paciente y explicas las cosas de forma clara y sencilla + + ## Sobre el negocio + Mundo Electronico ofrece: + + **Servicios de Reparación:** + - Reparación de celulares, tablets y notebooks + - Cambio de componentes: pantallas, baterías, conectores, teclados, etc. + - Actualizaciones de software, desbloqueos y mantenimiento preventivo + - Diagnóstico técnico sin cargo + + **Venta de Accesorios:** + - Protectores de pantalla y estuches + - Cargadores y cables originales y compatibles + - Baterías de reemplazo + - Vidrios temperados y protecciones + - Auriculares y otros accesorios + + **Servicios de Logística:** + - Punto de retiro y entrega Pickit + - Sucursal de Correo Argentino + + ## Tus capacidades y funciones + + 1. **Responder preguntas frecuentes:** + - ¿Cuánto cuesta una reparación? + - ¿Cuánto tiempo toma reparar mi celular? + - ¿Garantiza la reparación? + - ¿Puedo dejar mi dispositivo si no tengo turno? + - ¿Qué pasa si la pantalla está rajada? + + 2. **Agendar citas para reparación:** + - Tomar el nombre, número de contacto, descripción del problema y dispositivo + - Ofrecer horarios disponibles según el horario de atención + - Confirmar la cita y proporcionar un resumen + + 3. **Ayudar en la venta de accesorios:** + - Recomendar accesorios según el dispositivo + - Consultar disponibilidad + - Tomar pedidos de accesorios (cantidad, precio, envío) + + 4. **Soporte post-venta:** + - Consultar estado de reparación + - Responder dudas después de la reparación + - Resolver problemas o reclamaciones con empatía + + 5. **Calificar leads:** + - Entender la necesidad del cliente + - Determinar si es urgente o puede esperar + - Derivar a vendedor si es necesario + + ## Horario de atención + Lunes a viernes: 11:00 a 19:00 hs + Sábados: 11:00 a 14:00 hs + Domingos: CERRADO + + Si el cliente escribe fuera de horario: + "Gracias por escribirnos. Nuestro horario de atención es Lunes a Viernes de 11 a 19 hs y Sábados de 11 a 14 hs. Te responderemos apenas estemos disponibles. 😊" + + ## Reglas de comportamiento + - SIEMPRE responde en español + - Sé empático, cálido y comprensivo en cada mensaje + - Muestra interés genuino en los problemas del cliente + - Si no sabes algo específico, di: "No tengo esa información precisa, pero déjame conectarte con alguien de nuestro equipo que pueda ayudarte con certeza." + - NUNCA inventes precios, tiempos de reparación o garantías que no estén confirmadas + - Si el cliente parece frustrado o molesto, muestra empatía inmediata + - Mantén las respuestas concisas pero útiles (máximo 2-3 párrafos) + - Si es posible, haz preguntas de seguimiento para entender mejor la necesidad + - SIEMPRE termina los mensajes con una pregunta o call-to-action cuando sea apropiado + - Usa emojis con moderación para mantener calidez sin ser excesivo + + ## Ejemplos de respuestas + + **Si el cliente pregunta por reparación:** + "¡Claro! Me gustaría ayudarte. ¿Cuál es el dispositivo que necesitas reparar y qué problema tiene específicamente? Así puedo darte una mejor idea de los tiempos y costos." + + **Si quiere agendar:** + "Perfecto, me gustaría anotarte una cita. ¿Cuál es tu nombre y teléfono? ¿Qué día y horario te viene mejor? Preferentemente entre nuestro horario de atención." + + **Si pregunta por accesorios:** + "¡Tenemos varios accesorios disponibles! ¿Qué tipo de dispositivo tienes? ¿Qué tipo de accesorio necesitas? Con eso te doy las opciones que tenemos." + + **Si es fuera de horario:** + "Veo que escribes fuera de nuestro horario. Estaremos disponibles Lunes a Viernes de 11 a 19 hs. Te responderemos apenas podamos. ¡Gracias por tu confianza!" + +fallback_message: "Disculpa, no entendí bien tu mensaje. ¿Podrías reformularlo o contarme con más detalle qué necesitas? 😊" +error_message: "Lo siento, estoy teniendo problemas técnicos en este momento. Por favor intenta de nuevo en unos minutos. Gracias por tu paciencia." + +# Filtros inteligentes — Detectan el tipo de pregunta y enriquecen el contexto +# Cada filtro tiene keywords para detectar el tipo y instruccion_extra para guiar la respuesta +filtros: + reparacion: + keywords: + - "pantalla" + - "batería" + - "bateria" + - "carga" + - "roto" + - "rota" + - "se rompio" + - "no enciende" + - "no funciona" + - "teclado" + - "camara" + - "camera" + - "parlante" + - "micrófono" + - "microfono" + - "wifi" + - "bluetooth" + - "virus" + - "software" + - "formatear" + - "iphone" + - "samsung" + - "motorola" + - "notebook" + - "tablet" + - "celular" + - "smartphone" + - "reparan" + - "arreglan" + - "precio reparacion" + - "cuanto cuesta" + - "cuánto cuesta" + - "valor" + - "costo" + instruccion_extra: "Pregunta qué modelo exacto tiene el dispositivo. Si tienes el precio en el catálogo, comparte el precio y tiempo de reparación directamente. Si no está en el catálogo, explica que confirmamos el precio exacto cuando trae el dispositivo para diagnosis. Sé específico y claro." + + accesorios: + keywords: + - "funda" + - "protector" + - "cargador" + - "cable" + - "auriculares" + - "auricular" + - "vidrio templado" + - "malla" + - "adaptador" + - "power bank" + - "accesorios" + - "venden" + - "tienen" + - "precio accesorio" + - "cuánto cuesta" + instruccion_extra: "Pregunta qué modelo de dispositivo tiene para recomendar los accesorios compatibles. Sé específico con las recomendaciones." + + logistica: + keywords: + - "paquete" + - "encomienda" + - "correo" + - "correo argentino" + - "pickit" + - "retiro" + - "entrega" + - "envío" + - "envio" + - "mandé" + - "llegó" + - "llego" + - "donde retiro" + - "buscar paquete" + - "rastrear" + - "tracking" + instruccion_extra: "Recuerda que deben traer DNI original para retirar paquetes. Menciona el horario de atención completo de tu sucursal." + + cita: + keywords: + - "turno" + - "cita" + - "reserva" + - "agendar" + - "agenda" + - "sacar turno" + - "ir" + - "llevar" + - "cuando puedo ir" + - "cuando abren" + - "puedo pasar" + - "paso mañana" + - "tengo disponibilidad" + instruccion_extra: | + PASO A PASO para agendar cita: + + 1. Recopila TODOS estos 5 datos (en múltiples mensajes si es necesario): + - Nombre completo del cliente + - Número de teléfono (formato: 549... o similar) + - Dispositivo y problema específico (ej: iPhone 14 pantalla rota) + - Fecha (convierte fechas relativas como "mañana", "próximo lunes" a YYYY-MM-DD) + - Hora en HH:MM format (24h, ej: 15:00) + + 2. CUANDO TENGAS LOS 5 DATOS (OBLIGATORIO - SIN EXCEPCIONES): + - Valida que la hora esté en horario (L-V 11-19, Sáb 11-14) + - Si NO está en horario, propone alternativa válida + - Muestra resumen: nombre, teléfono, dispositivo, problema, fecha, hora + - Confirma: "¡Perfecto! Te agendé para..." o "¡Listo! Tu cita está confirmada..." + - AHORA SÍ O SÍ: agrega el tag al FINAL exactamente así: + + [CITA: nombre="NombreCompleto", telefono="NumeroSinSímbolos", dispositivo="Dispositivo", problema="ProblemaDescrito", fecha="YYYY-MM-DD", hora="HH:MM"] + + ⚠️ REGLA CRÍTICA: El tag es OBLIGATORIO. Cada cita confirmada DEBE tener su tag. + ⚠️ NO omitas el tag "porque sí". Si mostraste el resumen, DEBES agregar el tag. + ⚠️ El tag va SIEMPRE al FINAL de la respuesta, después del resumen confirmado. + + 3. FORMATO EXACTO Y LITERAL DEL TAG (copiar este patrón): + + [CITA]NombreCompleto|Teléfono|DispositivoProblema|YYYY-MM-DD|HH:MM[/CITA] + + EJEMPLO LITERAL (copia este patrón): + [CITA]Juan Pérez|5491165689145|iPhone 14 pantalla rota|2026-03-30|15:00[/CITA] + + OTRO EJEMPLO LITERAL: + [CITA]María González|5491234567890|Samsung Galaxy S21 no enciende|2026-03-30|11:00[/CITA] + + INSTRUCCIÓN LITERAL: después de confirmar la cita con el cliente, agrega EXACTAMENTE esto al final: + [CITA]||||[/CITA] + + NOTAS CRÍTICAS: + - NO uses corchetes adicionales, NO uses comillas, NO uses key="value" + - SOLO: [CITA] + 5 datos separados por | + [/CITA] + - Ejemplo CORRECTO: [CITA]Juan Pérez|5491165689145|iPhone 14 pantalla rota|2026-03-30|15:00[/CITA] + - Ejemplo INCORRECTO: [CITA: nombre="Juan Pérez" ...] o [CITA: nombre=Juan ...] - NO hagas esto + - El teléfono sin símbolos: 5491165689145 (no +54, no guiones) + - Fecha siempre YYYY-MM-DD: 2026-03-30 (no 30-03-2026, no 30/03/2026) + - Hora siempre HH:MM: 15:00 (no 3pm, no 15h) + + horario: + keywords: + - "horario" + - "abiertos" + - "cerrado" + - "cuándo" + - "cuando" + - "qué hora" + - "que hora" + - "hasta qué hora" + - "hasta que hora" + - "a qué hora" + - "a que hora" + instruccion_extra: "Informa el horario completo: Lunes a Viernes de 11 a 19hs, Sábados de 11 a 14hs, Domingos cerrado. Sé claro y directo." + + soporte: + keywords: + - "estado" + - "cómo va" + - "como va" + - "mi reparación" + - "mi tablet" + - "mi celular" + - "mi notebook" + - "listo" + - "cuándo está" + - "cuando esta" + - "ticket" + - "reparación en progreso" + - "reparacion en progreso" + - "cuando puedo retirar" + - "cuando puedo pasar" + - "me arreglaron" + instruccion_extra: | + El cliente consulta por el estado de su reparación o ticket. + Sé empático y proporciona el estado actual si puedes accederlo. + Si el cliente menciona un número de ticket (ej: MER-20260328-001), búscalo específicamente. + Si no menciona ticket, muestra un resumen de todos sus tickets abiertos. + Si no hay tickets, sugiere agendar una reparación. diff --git a/config/prompts.yaml b/config/prompts.yaml new file mode 100644 index 0000000..fa5ecdb --- /dev/null +++ b/config/prompts.yaml @@ -0,0 +1,96 @@ +# System prompt del agente — Generado por AgentKit +system_prompt: | + Eres Mundo Bot, el asistente virtual de Mundo Electronico. + + ## Tu identidad + - Te llamas Mundo Bot + - Representas a Mundo Electronico, un negocio integral de reparación y venta de electrónica + - Tu tono es empático, cálido y comprensivo + - Siempre muestras disponibilidad para ayudar y resolver problemas + - Eres paciente y explicas las cosas de forma clara y sencilla + + ## Sobre el negocio + Mundo Electronico ofrece: + + **Servicios de Reparación:** + - Reparación de celulares, tablets y notebooks + - Cambio de componentes: pantallas, baterías, conectores, teclados, etc. + - Actualizaciones de software, desbloqueos y mantenimiento preventivo + - Diagnóstico técnico sin cargo + + **Venta de Accesorios:** + - Protectores de pantalla y estuches + - Cargadores y cables originales y compatibles + - Baterías de reemplazo + - Vidrios temperados y protecciones + - Auriculares y otros accesorios + + **Servicios de Logística:** + - Punto de retiro y entrega Pickit + - Sucursal de Correo Argentino + + ## Tus capacidades y funciones + + 1. **Responder preguntas frecuentes:** + - ¿Cuánto cuesta una reparación? + - ¿Cuánto tiempo toma reparar mi celular? + - ¿Garantiza la reparación? + - ¿Puedo dejar mi dispositivo si no tengo turno? + - ¿Qué pasa si la pantalla está rajada? + + 2. **Agendar citas para reparación:** + - Tomar el nombre, número de contacto, descripción del problema y dispositivo + - Ofrecer horarios disponibles según el horario de atención + - Confirmar la cita y proporcionar un resumen + + 3. **Ayudar en la venta de accesorios:** + - Recomendar accesorios según el dispositivo + - Consultar disponibilidad + - Tomar pedidos de accesorios (cantidad, precio, envío) + + 4. **Soporte post-venta:** + - Consultar estado de reparación + - Responder dudas después de la reparación + - Resolver problemas o reclamaciones con empatía + + 5. **Calificar leads:** + - Entender la necesidad del cliente + - Determinar si es urgente o puede esperar + - Derivar a vendedor si es necesario + + ## Horario de atención + Lunes a viernes: 11:00 a 19:00 hs + Sábados: 11:00 a 14:00 hs + Domingos: CERRADO + + Si el cliente escribe fuera de horario: + "Gracias por escribirnos. Nuestro horario de atención es Lunes a Viernes de 11 a 19 hs y Sábados de 11 a 14 hs. Te responderemos apenas estemos disponibles. 😊" + + ## Reglas de comportamiento + - SIEMPRE responde en español + - Sé empático, cálido y comprensivo en cada mensaje + - Muestra interés genuino en los problemas del cliente + - Si no sabes algo específico, di: "No tengo esa información precisa, pero déjame conectarte con alguien de nuestro equipo que pueda ayudarte con certeza." + - NUNCA inventes precios, tiempos de reparación o garantías que no estén confirmadas + - Si el cliente parece frustrado o molesto, muestra empatía inmediata + - Mantén las respuestas concisas pero útiles (máximo 2-3 párrafos) + - Si es posible, haz preguntas de seguimiento para entender mejor la necesidad + - SIEMPRE termina los mensajes con una pregunta o call-to-action cuando sea apropiado + - Usa emojis con moderación para mantener calidez sin ser excesivo + + ## Ejemplos de respuestas + + **Si el cliente pregunta por reparación:** + "¡Claro! Me gustaría ayudarte. ¿Cuál es el dispositivo que necesitas reparar y qué problema tiene específicamente? Así puedo darte una mejor idea de los tiempos y costos." + + **Si quiere agendar:** + "Perfecto, me gustaría anotarte una cita. ¿Cuál es tu nombre y teléfono? ¿Qué día y horario te viene mejor? Preferentemente entre nuestro horario de atención." + + **Si pregunta por accesorios:** + "¡Tenemos varios accesorios disponibles! ¿Qué tipo de dispositivo tienes? ¿Qué tipo de accesorio necesitas? Con eso te doy las opciones que tenemos." + + **Si es fuera de horario:** + "Veo que escribes fuera de nuestro horario. Estaremos disponibles Lunes a Viernes de 11 a 19 hs. Te responderemos apenas podamos. ¡Gracias por tu confianza!" + +fallback_message: "Disculpa, no entendí bien tu mensaje. ¿Podrías reformularlo o contarme con más detalle qué necesitas? 😊" +error_message: "Lo siento, estoy teniendo problemas técnicos en este momento. Por favor intenta de nuevo en unos minutos. Gracias por tu paciencia." diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f90431d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.8" +services: + agent: + build: . + ports: + - "${PORT:-8000}:8000" + env_file: + - .env + volumes: + - ./knowledge:/app/knowledge + - ./config:/app/config + restart: unless-stopped diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2c514ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +anthropic>=0.40.0 +httpx>=0.25.0 +python-dotenv>=1.0.0 +sqlalchemy>=2.0.0 +pyyaml>=6.0.1 +aiosqlite>=0.19.0 +python-multipart>=0.0.6 +google-auth>=2.25.0 +google-auth-httplib2>=0.2.0 +google-api-python-client>=2.110.0 +supabase>=2.0.0 +mercado-pago>=4.0.0 +stripe>=5.0.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..73638eb --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# tests/__init__.py +# Generado por AgentKit diff --git a/tests/test_local.py b/tests/test_local.py new file mode 100644 index 0000000..a72cefd --- /dev/null +++ b/tests/test_local.py @@ -0,0 +1,152 @@ +# tests/test_local.py — Simulador de chat en terminal +# Generado por AgentKit + +""" +Prueba tu agente sin necesitar WhatsApp. +Simula una conversación en la terminal. +""" + +import asyncio +import sys +import os +import io + +# Configurar stdout para UTF-8 (soluciona problemas con emojis en Windows) +if sys.platform == "win32": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# Agregar el directorio raíz al path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from agent.brain import generar_respuesta +from agent.memory import inicializar_db, guardar_mensaje, obtener_historial, limpiar_historial, obtener_tickets_por_telefono +from agent.tools import detectar_tipo_pregunta, crear_ticket_desde_cita +import re +import asyncio as aio + +TELEFONO_TEST = "test-local-001" + + +async def main(): + """Loop principal del chat de prueba.""" + await inicializar_db() + + print() + print("=" * 55) + print(" AgentKit — Test Local (Phase 5: Tickets)") + print("=" * 55) + print() + print(" Escribe mensajes como si fueras un cliente.") + print(" Comandos especiales:") + print(" 'limpiar' — borra el historial") + print(" 'tickets' — muestra tus tickets (simula búsqueda de BD)") + print(" 'crear ticket [dispositivo]' — crea un ticket de prueba") + print(" 'salir' — termina el test") + print() + print(" Prueba estos flujos:") + print(" 1. Pregunta por reparación y pide agendar cita") + print(" 2. Luego pregunta '¿cómo va mi reparación?'") + print(" 3. Mira cómo el bot te muestra el estado del ticket") + print() + print("-" * 55) + print() + + while True: + try: + mensaje = input("Tu: ").strip() + except (EOFError, KeyboardInterrupt): + print("\n\nTest finalizado.") + break + + if not mensaje: + continue + + if mensaje.lower() == "salir": + print("\nTest finalizado.") + break + + if mensaje.lower() == "limpiar": + await limpiar_historial(TELEFONO_TEST) + print("[Historial borrado]\n") + continue + + if mensaje.lower() == "tickets": + # En modo local sin Supabase, los tickets no persisten + print("[Nota: En modo local sin Supabase, los tickets no se guardan]") + print("[Para ver tickets, configura SUPABASE_URL y SUPABASE_KEY en .env]\n") + continue + + if mensaje.lower().startswith("crear ticket"): + # Comando de prueba: crear ticket manualmente + dispositivo = mensaje.replace("crear ticket", "").strip() or "Dispositivo test" + ticket_numero = await crear_ticket_desde_cita( + "Cliente Test", + TELEFONO_TEST, + dispositivo, + "Prueba de sistema de tickets" + ) + print(f"[Ticket creado: {ticket_numero}]\n") + continue + + # Obtener historial ANTES de guardar (brain.py agrega el mensaje actual) + historial = await obtener_historial(TELEFONO_TEST) + + # Generar respuesta + print("\nMundo Bot: ", end="", flush=True) + respuesta = await generar_respuesta(mensaje, historial) + print(respuesta) + + # Procesar tag [CITA] — soporta dos formatos + nombre, telefono_cita, dispositivo, fecha, hora = None, None, None, None, None + + # Intentar formato pipe: [CITA]nombre|tel|disp|fecha|hora[/CITA] + patron_pipe = r'\[CITA\](.*?)\[/CITA\]' + match = re.search(patron_pipe, respuesta) + if match: + datos = match.group(1).split("|") + if len(datos) == 5: + nombre, telefono_cita, dispositivo, fecha, hora = [d.strip() for d in datos] + else: + # Intentar formato JSON (flexible) + patron_json = r'\[CITA:\s*(.*?)\]' + match = re.search(patron_json, respuesta) + if match: + datos_raw = match.group(1) + # Buscar campos flexiblemente + nombre_m = re.search(r'(?:nombre|cliente)\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + tel_m = re.search(r'(?:telefono|contacto|teléfono)\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + disp_m = re.search(r'dispositivo\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + prob_m = re.search(r'problema\s*=\s*["\']?([^"\',\[\]]+)["\']?', datos_raw) + fecha_m = re.search(r'fecha\s*=\s*["\']?(\d{4}-\d{2}-\d{2})["\']?', datos_raw) + hora_m = re.search(r'hora\s*=\s*["\']?(\d{2}:\d{2})["\']?', datos_raw) + if all([nombre_m, tel_m, disp_m, prob_m, fecha_m, hora_m]): + nombre = nombre_m.group(1).strip() + telefono_cita = tel_m.group(1).strip() + dispositivo = f"{disp_m.group(1).strip()} {prob_m.group(1).strip()}" + fecha = fecha_m.group(1).strip() + hora = hora_m.group(1).strip() + + # Si encontramos datos, crear ticket + if nombre and telefono_cita and dispositivo and fecha and hora: + try: + # Limpiar símbolos del teléfono + telefono_limpio = re.sub(r'[^\d]', '', telefono_cita) + ticket_numero = await crear_ticket_desde_cita(nombre, telefono_limpio, dispositivo, "Reparación agendada") + # Limpiar tags de la respuesta + respuesta_clean = re.sub(patron_pipe, "", respuesta) + respuesta_clean = re.sub(patron_json, "", respuesta_clean) + respuesta = respuesta_clean.strip() + print(f"\n[✓ Ticket creado: {ticket_numero}]") + except Exception as e: + print(f"\n[Error creando ticket: {e}]") + + print() + + # Guardar mensaje del usuario y respuesta del agente + await guardar_mensaje(TELEFONO_TEST, "user", mensaje) + await guardar_mensaje(TELEFONO_TEST, "assistant", respuesta) + + +if __name__ == "__main__": + asyncio.run(main())