diff --git a/docs/companybrain-mvp-plan.md b/docs/companybrain-mvp-plan.md new file mode 100644 index 0000000..b272954 --- /dev/null +++ b/docs/companybrain-mvp-plan.md @@ -0,0 +1,736 @@ +# CompanyBrain — Plan MVP + +> **Objetivo:** Evolucionar Compass en CompanyBrain, el cerebro operativo AI-first para PyMEs LATAM. +> Generado: 2026-05-18 + +--- + +## Resumen ejecutivo + +Compass ya es una base sólida: Q&A sobre documentos con PageIndex, persistencia SQLite, un monolito FastAPI limpio y un frontend React 19 parcialmente construido. Este plan lo evoluciona sistemáticamente sin romper el comportamiento existente. Cada fase produce un incremento que el fundador puede usar en su propia empresa — esa es la puerta de validación antes de escalar. + +**Deuda técnica reconocida (no se planifica alrededor de ella):** +- I/O bloqueante en handlers async (`upload`, `index_document`) — solución eventual: `asyncio.to_thread` +- Tabla `messages` crece sin límite — necesita política de retención antes de Fase 3 +- `mark_indexed()` en `database.py` es código muerto — eliminar en Fase 1 +- Sin cobertura de tests ni linting — agregar `ruff` + `pytest-cov` a CI en Fase 1 + +--- + +## Nuevo esquema de base de datos + +Todas las tablas coexisten en `data/compass.db`. `init_db()` crece con `CREATE TABLE IF NOT EXISTS`. Cambios de columnas en tablas existentes requieren `ALTER TABLE` explícito. + +```sql +-- EXISTENTE (sin cambios) +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + indexed BOOLEAN DEFAULT FALSE +); + +-- FASE 1: Entidades de memoria estructurada + +CREATE TABLE IF NOT EXISTS people ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + role TEXT, + email TEXT, + phone TEXT, + notes TEXT, + source_doc TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_people_name ON people(name); + +CREATE TABLE IF NOT EXISTS clients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + contact_person TEXT, + email TEXT, + phone TEXT, + status TEXT DEFAULT 'active', -- 'active' | 'inactive' | 'prospect' + last_contact_at DATETIME, + notes TEXT, + source_doc TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_clients_status ON clients(status); +CREATE INDEX IF NOT EXISTS idx_clients_last_contact ON clients(last_contact_at); + +CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + client_id INTEGER REFERENCES clients(id), + status TEXT DEFAULT 'active', -- 'active' | 'completed' | 'paused' | 'cancelled' + description TEXT, + started_at DATETIME, + deadline_at DATETIME, + last_update_at DATETIME, + source_doc TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status); +CREATE INDEX IF NOT EXISTS idx_projects_client_id ON projects(client_id); +CREATE INDEX IF NOT EXISTS idx_projects_last_update ON projects(last_update_at); + +CREATE TABLE IF NOT EXISTS decisions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + context TEXT, + outcome TEXT NOT NULL, + made_by TEXT, + made_at DATETIME, + impact TEXT, -- 'high' | 'medium' | 'low' + source_doc TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_decisions_made_at ON decisions(made_at); + +-- FASE 2: Sistema de conectores + +CREATE TABLE IF NOT EXISTS connectors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, -- 'whatsapp' | 'gmail' | 'gcal' | 'slack' + name TEXT NOT NULL, + config TEXT NOT NULL, -- JSON (campos sensibles documentados como pendientes de cifrado) + enabled BOOLEAN DEFAULT TRUE, + last_synced_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS connector_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + connector_id INTEGER NOT NULL REFERENCES connectors(id), + external_id TEXT, -- ID del proveedor para deduplicación + event_type TEXT NOT NULL, -- 'message_in' | 'message_out' | 'event_created' | etc. + payload TEXT NOT NULL, -- JSON completo del proveedor + processed BOOLEAN DEFAULT FALSE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_connector_events_connector_id ON connector_events(connector_id); +CREATE INDEX IF NOT EXISTS idx_connector_events_external_id ON connector_events(external_id); +CREATE INDEX IF NOT EXISTS idx_connector_events_processed ON connector_events(processed); + +-- FASE 3: Sistema de acciones agénticas + +CREATE TABLE IF NOT EXISTS actions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, -- 'draft_email' | 'create_calendar_event' | 'create_task' + payload TEXT NOT NULL, -- JSON: parámetros completos de la acción + status TEXT DEFAULT 'pending', -- 'pending' | 'approved' | 'rejected' | 'executed' | 'failed' + proposed_by TEXT DEFAULT 'companybrain', + approved_by TEXT, + session_id TEXT, + connector_id INTEGER REFERENCES connectors(id), + result TEXT, -- JSON: resultado de ejecución o error + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_actions_status ON actions(status); +CREATE INDEX IF NOT EXISTS idx_actions_session_id ON actions(session_id); + +-- FASE 4: Sistema de alertas y resúmenes + +CREATE TABLE IF NOT EXISTS alert_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + rule_type TEXT NOT NULL, -- 'client_no_contact' | 'project_no_update' | 'contract_expiry' + threshold_days INTEGER NOT NULL, + enabled BOOLEAN DEFAULT TRUE, + delivery TEXT DEFAULT 'web', -- 'web' | 'whatsapp' | 'both' + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS alert_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rule_id INTEGER NOT NULL REFERENCES alert_rules(id), + entity_type TEXT NOT NULL, -- 'client' | 'project' + entity_id INTEGER NOT NULL, + message TEXT NOT NULL, + delivered BOOLEAN DEFAULT FALSE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_alert_events_delivered ON alert_events(delivered); +CREATE INDEX IF NOT EXISTS idx_alert_events_rule_id ON alert_events(rule_id); +``` + +--- + +## Nuevos endpoints de API + +Todos los routes nuevos siguen el patrón existente: `@limiter.limit(RATE_LIMIT)`, dependencia de auth, tests en `test_*.py`. + +### Fase 1 — API de Entidades + +``` +GET /entities/people → listar personas +POST /entities/people → crear persona manualmente +GET /entities/people/{id} → detalle de persona +PATCH /entities/people/{id} → actualizar persona + +GET /entities/clients → listar clientes (?status=active) +POST /entities/clients → crear cliente manualmente +GET /entities/clients/{id} → detalle de cliente +PATCH /entities/clients/{id} → actualizar cliente + +GET /entities/projects → listar proyectos (?status=active&client_id=) +POST /entities/projects → crear proyecto manualmente +GET /entities/projects/{id} → detalle de proyecto +PATCH /entities/projects/{id} → actualizar proyecto + +GET /entities/decisions → listar decisiones (?limit=20&offset=0) +POST /entities/decisions → crear decisión manualmente + +POST /documents/{doc_id}/extract → disparar extracción de entidades de un doc +GET /documents/{doc_id}/entities → listar entidades extraídas de un doc +``` + +### Fase 2 — API de Conectores + +``` +GET /connectors → listar conectores +POST /connectors → registrar conector (tipo + config) +DELETE /connectors/{id} → eliminar conector +PATCH /connectors/{id}/toggle → habilitar/deshabilitar + +POST /webhooks/whatsapp → webhook entrante de WhatsApp (auth propio) +GET /webhooks/whatsapp → verificación de webhook (GET challenge) +POST /connectors/{id}/sync → disparar sync manual (Gmail/GCal) +GET /connectors/{id}/events → listar eventos crudos del conector +``` + +### Fase 3 — API de Acciones + +``` +GET /actions → listar acciones (?status=pending) +GET /actions/{id} → detalle de acción +POST /actions/{id}/approve → aprobar y ejecutar acción +POST /actions/{id}/reject → rechazar acción +``` + +### Fase 4 — API de Alertas + +``` +GET /alerts/rules → listar reglas de alerta +POST /alerts/rules → crear regla +PATCH /alerts/rules/{id} → actualizar regla +DELETE /alerts/rules/{id} → eliminar regla + +GET /alerts/events → listar alertas disparadas (?delivered=false) +POST /alerts/events/{id}/dismiss → marcar como entregada + +GET /digest/preview → previsualizar resumen (sin enviar) +POST /digest/send → generar y entregar resumen +``` + +--- + +## Fase 1 — Base Completa + +**Complejidad: Media.** Sin dependencias externas nuevas. Mayor riesgo: calidad del prompt de extracción de entidades. + +### 1.1 Migraciones de base de datos + +Extender `backend/database.py`: +- Agregar tablas Fase 1 en `init_db()` +- Agregar funciones CRUD por entidad +- Eliminar `mark_indexed()` (código muerto) +- Agregar `prune_messages(session_id, keep_last=100)` + +Firmas a implementar: + +```python +def create_person(name, role, email, phone, notes, source_doc) -> int +def list_people(limit=50) -> list[dict] +def get_person(person_id) -> dict | None +def update_person(person_id, **fields) -> bool + +def create_client(name, contact_person, email, phone, status, notes, source_doc) -> int +def list_clients(status=None, limit=50) -> list[dict] +def get_client(client_id) -> dict | None +def update_client(client_id, **fields) -> bool + +def create_project(name, client_id, status, description, started_at, deadline_at, source_doc) -> int +def list_projects(status=None, client_id=None, limit=50) -> list[dict] +def get_project(project_id) -> dict | None +def update_project(project_id, **fields) -> bool + +def create_decision(title, context, outcome, made_by, made_at, impact, source_doc) -> int +def list_decisions(limit=20, offset=0) -> list[dict] +``` + +### 1.2 Pipeline de extracción de entidades + +Crear `backend/extractor.py`: + +```python +EXTRACTION_PROMPT = """Eres un asistente que extrae entidades estructuradas de documentos empresariales. + +Dado el siguiente contenido, extrae las entidades en formato JSON: + +{ + "people": [{"name":"...","role":"...","email":"...","phone":"...","notes":"..."}], + "clients": [{"name":"...","contact_person":"...","email":"...","notes":"..."}], + "projects": [{"name":"...","client":"...","status":"...","description":"...","deadline":"..."}], + "decisions": [{"title":"...","context":"...","outcome":"...","made_by":"...","made_at":"YYYY-MM-DD","impact":"high|medium|low"}] +} + +Solo incluye entidades claramente mencionadas. Responde SOLO con el JSON. + +DOCUMENTO ({doc_name}): +{content}""" + +async def extract_entities_from_doc(doc_id, doc_name, indexer, model) -> dict +async def extract_all_documents(indexer, model) -> dict +``` + +**Nota importante:** `_extract_summaries()` de `chat.py` debe moverse a `backend/utils.py` y ser importado por ambos `chat.py` y `extractor.py`. Hacer esto primero. + +**Fallback de extracción:** Si el LLM no produce JSON válido, reintentar con prompt más estricto ("responde ÚNICAMENTE con JSON válido, sin texto adicional"). Si falla dos veces, loguear y omitir ese documento sin fallar el request. + +### 1.3 Routers de entidades + +Crear `backend/routers/__init__.py` y `backend/routers/entities.py`: + +```python +# backend/routers/entities.py +router = APIRouter(prefix="/entities", tags=["entities"]) + +class PersonCreate(BaseModel): + name: str + role: str | None = None + email: str | None = None + phone: str | None = None + notes: str | None = None + +# Incluir en main.py: +# from .routers.entities import router as entities_router +# app.include_router(entities_router) +``` + +### 1.4 Completar frontend + +**Nuevas páginas:** +- `frontend/src/pages/Entities.tsx` — dashboard de Personas, Clientes, Proyectos y Decisiones en tabs. Solo lectura para MVP, con botón "Agregar" por tipo. +- `frontend/src/pages/EntityDetail.tsx` — detalle de un cliente o proyecto con decisiones relacionadas. + +**Nuevos componentes:** +- `frontend/src/components/EntityCard.tsx` — tarjeta reutilizable siguiendo el sistema de diseño Nocturnal Architect existente. +- `frontend/src/components/EntityBadge.tsx` — pill de estado (active/inactive/pending) usando tokens de color existentes. + +**Archivos a actualizar:** +- `frontend/src/lib/api.ts` — agregar funciones: `getPeople()`, `getClients()`, `getProjects()`, `getDecisions()`, `createPerson()`, `updateClient()`, etc. +- `frontend/src/components/Sidebar.tsx` — agregar nav item `/entities` con icono `Users` de lucide. +- `frontend/src/App.tsx` — agregar rutas `/entities` y `/entities/:type/:id`. +- `frontend/src/pages/Chat.tsx` — traducir sugerencias hardcodeadas al español. + +### 1.5 Rebrand a CompanyBrain + +Cambios de string (no reestructuración): +- `frontend/src/components/Sidebar.tsx` — `"Compass"` → `"CompanyBrain"`, tagline → `"Tu cerebro empresarial"` +- `backend/chat.py` — primera línea del SYSTEM_PROMPT: `"Eres CompanyBrain, el cerebro operativo de la empresa."` +- `backend/main.py` — `title="CompanyBrain"`, `version="0.3.0"` +- `frontend/index.html` — `CompanyBrain` + +### Archivos nuevos en Fase 1 + +``` +backend/extractor.py +backend/utils.py (mover _extract_summaries desde chat.py) +backend/routers/__init__.py +backend/routers/entities.py +frontend/src/pages/Entities.tsx +frontend/src/pages/EntityDetail.tsx +frontend/src/components/EntityCard.tsx +frontend/src/components/EntityBadge.tsx +tests/test_extractor.py +tests/test_entities_api.py +``` + +--- + +## Fase 2 — Integraciones en Vivo + +**Complejidad: Alta.** APIs externas, flujos OAuth, ingesta de webhooks, procesamiento async. + +### Arquitectura de conectores + +Crear paquete `backend/connectors/`: + +``` +backend/connectors/ +├── __init__.py +├── base.py ← clase abstracta BaseConnector +├── registry.py ← CONNECTOR_CLASSES dict + get_connector() +├── whatsapp.py ← Meta Cloud API +├── gmail.py ← Google Gmail API (service account) +└── gcal.py ← Google Calendar API (service account) +``` + +**`backend/connectors/base.py`:** + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass + +@dataclass +class ConnectorConfig: + connector_id: int + connector_type: str + name: str + config: dict + +@dataclass +class InboundEvent: + external_id: str + event_type: str # 'message_in' | 'email_received' | 'calendar_event' + sender: str | None + content: str + raw_payload: dict + +class BaseConnector(ABC): + def __init__(self, cfg: ConnectorConfig): self.cfg = cfg + + @abstractmethod + async def verify_webhook(self, headers: dict, body: bytes) -> bool: ... + + @abstractmethod + async def parse_inbound(self, payload: dict) -> list[InboundEvent]: ... + + @abstractmethod + async def send_message(self, recipient: str, text: str) -> dict: ... + + @abstractmethod + async def health_check(self) -> bool: ... + + # Opcionales — no todos los conectores implementan estos + async def create_draft_email(self, to, subject, body) -> dict: + raise NotImplementedError + + async def create_calendar_event(self, title, start, end, description, attendees) -> dict: + raise NotImplementedError + + async def list_recent_emails(self, max_results=10) -> list[dict]: + raise NotImplementedError + + async def list_upcoming_events(self, days_ahead=7) -> list[dict]: + raise NotImplementedError +``` + +### WhatsApp — Meta Cloud API + +Usar Meta Cloud API directa (no Twilio — más barato, mejores rate limits para LATAM). + +Config requerida en `connectors.config` (JSON): +```json +{ + "phone_number_id": "...", + "access_token": "...", + "webhook_verify_token": "...", + "app_secret": "..." +} +``` + +**Routing de mensajes WhatsApp al pipeline de chat:** + +`session_id = "wa_" + normalized_phone` (ej: `wa_573001234567`). Esto reutiliza la tabla `messages` y `get_session_messages()` sin cambios. Cada contacto de WhatsApp tiene su historial de conversación continuo. + +### Google Workspace — Service Account + +Usar Service Account con delegación de dominio (no OAuth de 3 patas). Requiere solo configuración del admin de Google Workspace una vez. Asume que el usuario tiene Google Workspace (no Gmail personal) — esto aplica para la agencia target. + +Config requerida: +```json +{ + "service_account_file": "./config/service-account.json", + "delegated_email": "owner@empresa.com", + "calendar_id": "primary" +} +``` + +**Nuevas dependencias pip para Fase 2:** +``` +google-auth==2.29.0 +google-api-python-client==2.126.0 +``` + +### Servicio de sync — `backend/sync.py` + +Polling en background para conectores que no hacen push (Gmail, GCal). Corre como tareas asyncio iniciadas en el lifespan de FastAPI. + +```python +# Variable de entorno: COMPANYBRAIN_SYNC_INTERVAL_MINUTES=5 (default) +async def start_sync_loop(app: FastAPI) -> None: ... +async def sync_connector(connector_cfg, app: FastAPI) -> None: ... +``` + +### Archivos nuevos en Fase 2 + +``` +backend/connectors/__init__.py +backend/connectors/base.py +backend/connectors/registry.py +backend/connectors/whatsapp.py +backend/connectors/gmail.py +backend/connectors/gcal.py +backend/routers/webhooks.py +backend/routers/connectors.py +backend/sync.py +frontend/src/pages/Connectors.tsx +tests/test_connectors.py +tests/test_webhooks.py +``` + +--- + +## Fase 3 — Acciones Agénticas + +**Complejidad: Media.** El desafío mayor es el UX de confianza, no la implementación. + +### Definiciones de tools — `backend/tools.py` + +```python +TOOLS = [ + { + "type": "function", + "function": { + "name": "draft_email", + "description": "Redactar un borrador de email para revisión humana antes de enviar.", + "parameters": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "subject": {"type": "string"}, + "body": {"type": "string"}, + "context": {"type": "string"} + }, + "required": ["to", "subject", "body"] + } + } + }, + { + "type": "function", + "function": { + "name": "create_calendar_event", + "description": "Crear un evento en Google Calendar, requiere aprobación humana.", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "start_datetime": {"type": "string", "description": "ISO 8601"}, + "end_datetime": {"type": "string", "description": "ISO 8601"}, + "attendees": {"type": "array", "items": {"type": "string"}}, + "description": {"type": "string"}, + "context": {"type": "string"} + }, + "required": ["title", "start_datetime", "end_datetime"] + } + } + }, + { + "type": "function", + "function": { + "name": "create_task", + "description": "Registrar una tarea o acción de seguimiento.", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "assigned_to": {"type": "string"}, + "due_date": {"type": "string", "description": "ISO 8601 date"}, + "related_client": {"type": "string"}, + "related_project": {"type": "string"}, + "context": {"type": "string"} + }, + "required": ["title"] + } + } + } +] +``` + +### Pipeline de chat modificado + +Cuando la pregunta implica una acción, el LLM puede devolver `tool_calls`. El pipeline: +1. Detecta `tool_calls` en la respuesta del LLM +2. Guarda una `action` pendiente en DB (NO ejecuta) +3. Devuelve la propuesta junto con la respuesta + +**Nuevo shape de respuesta de `POST /chat`:** +```json +{ + "answer": "Puedo preparar ese email. Lo he propuesto para tu revisión.", + "sources": [...], + "suggestion": null, + "session_id": "...", + "proposed_actions": [ + { + "action_id": 42, + "type": "draft_email", + "summary": "Email a camila@empresa.co sobre ClienteX", + "status": "pending" + } + ] +} +``` + +**Fallback para modelos sin function calling:** Si el modelo no soporta `tool_calls`, parsear la respuesta buscando un marcador `🔧 ACCIÓN:` similar al patrón existente de `💡 SUGERENCIA:`. + +### UX de aprobación en el chat + +El mensaje del asistente renderiza un `ActionCard` inline — widget compacto con tipo, resumen, y dos botones: "Aprobar" (primary) y "Rechazar" (ghost). Aprobar llama a `POST /actions/{id}/approve`. Todo ocurre sin salir del chat. + +### Archivos nuevos en Fase 3 + +``` +backend/tools.py +backend/executor.py +backend/routers/actions.py +frontend/src/components/ActionCard.tsx +frontend/src/pages/Actions.tsx +tests/test_actions.py +``` + +--- + +## Fase 4 — Inteligencia Proactiva + +**Complejidad: Baja-Media.** Scheduling es sencillo; la calidad de alertas depende del tuning de prompts. + +### Scheduler — `backend/scheduler.py` + +Tareas asyncio con sleep loops, iniciadas en el lifespan de FastAPI. Sin Celery ni APScheduler (overkill para MVP single-user, añade complejidad operacional). + +``` +Jobs: + check_alerts() → cada 1 hora + generate_digest() → diario a las 8:00 AM hora local +``` + +### Reglas de alerta — `backend/alerts.py` + +```python +async def check_client_no_contact(threshold_days, conn) -> list[dict]: + # SELECT * FROM clients WHERE status='active' + # AND (last_contact_at < datetime('now', '-N days') OR last_contact_at IS NULL) + +async def check_project_no_update(threshold_days, conn) -> list[dict]: + # SELECT * FROM projects WHERE status='active' + # AND last_update_at < datetime('now', '-N days') + +async def check_contract_expiry(threshold_days, conn) -> list[dict]: + # SELECT * FROM projects WHERE deadline_at BETWEEN datetime('now') + # AND datetime('now', '+N days') + +RULE_HANDLERS = { + "client_no_contact": check_client_no_contact, + "project_no_update": check_project_no_update, + "contract_expiry": check_contract_expiry, +} +``` + +### Digest diario + +El resumen usa el pipeline LLM existente con un prompt en español. El contexto viene de las tablas de entidades (estado real-time), no de documentos indexados. + +### Entrega + +- **Web:** `GET /alerts/events?delivered=false` — el frontend hace polling cada 60s y muestra badge de notificaciones en sidebar. +- **WhatsApp:** Si hay conector registrado, `connector.send_message()` al teléfono del dueño configurado en `COMPANYBRAIN_OWNER_PHONE`. + +### Variables de entorno nuevas para Fase 4 + +```env +COMPANYBRAIN_DIGEST_HOUR=8 # hora (0-23) para digest diario +COMPANYBRAIN_ALERT_CHECK_INTERVAL=60 # minutos entre checks de alertas +COMPANYBRAIN_OWNER_PHONE= # número WhatsApp para digest +``` + +### Archivos nuevos en Fase 4 + +``` +backend/scheduler.py +backend/alerts.py +backend/routers/alerts.py +frontend/src/pages/Alerts.tsx +tests/test_alerts.py +tests/test_scheduler.py +``` + +--- + +## Resumen de archivos por fase + +### Qué no cambia +- `backend/indexer.py` — sin cambios +- `backend/watcher.py` — sin cambios en Fases 1-3 +- `frontend/src/index.css` — tokens de diseño intactos +- `frontend/src/components/Toast.tsx`, `TopBar.tsx` +- `vite.config.ts` +- `pageindex/` — submodulo intocable + +### Qué se modifica (cambios aditivos) +- `backend/database.py` — tablas nuevas, funciones CRUD, eliminar `mark_indexed()` +- `backend/main.py` — incluir routers, hooks en lifespan, metadata +- `backend/chat.py` — manejo de tool_calls (Fase 3), extraer `_extract_summaries` a `utils.py` +- `frontend/src/App.tsx` — rutas nuevas +- `frontend/src/components/Sidebar.tsx` — nav items nuevos +- `frontend/src/lib/api.ts` — funciones API nuevas +- `requirements.txt` — deps por fase +- `.env.example` — variables nuevas con comentarios + +--- + +## Decisiones arquitectónicas clave + +| Decisión | Razonamiento | Trade-off | +|---|---|---| +| SQLite sin framework de migraciones | Cero complejidad ops para fundador solo | Requiere disciplina estricta con `ALTER TABLE` | +| Meta Cloud API sobre Twilio para WhatsApp | Gratis (solo pago por conversación), mejores rate limits LATAM | Proceso de aprobación de Meta (1-2 días) | +| Service Account Google sobre OAuth 3-patas | Setup único por admin, sin pantalla de consentimiento | Requiere Google Workspace (no Gmail personal) | +| asyncio scheduler sobre Celery | Cero infraestructura extra, deployment simple | Riesgo de muerte silente (igual que el watcher actual) | +| Extracción de entidades en post-index | Queries rápidas, UI de entidades sin preguntar al LLM | Calidad congelada al momento de indexar; re-extracción manual disponible | +| Aprobación de acciones en el chat | UX de menor fricción, sin cambio de contexto | Acciones pendientes de sesiones anteriores requieren ir a `/actions` | +| session_id WhatsApp = teléfono | Reutiliza tabla `messages` y funciones existentes sin cambios | Historial mezclado si dos personas usan el mismo teléfono | + +--- + +## Riesgos principales + +| Riesgo | Probabilidad | Mitigación | +|---|---|---| +| Modelo Ollama no produce JSON válido para extracción | Alta | Fallback con prompt más estricto + retry; usar `gemma4:e4b` o `llama3.2` según capacidad | +| Modelo Ollama no soporta function calling | Media | Fallback con marcador `🔧 ACCIÓN:` en el response, igual que `💡 SUGERENCIA:` | +| Aprobación de WhatsApp Business API Meta tarda | Media | Documentar en setup; arrancar con web UI primero, agregar WA después | +| Calidad de alertas (demasiados falsos positivos) | Media | Empezar con threshold_days alto (30+ días); hacer configurable desde UI | +| Scheduler asyncio muere silenciosamente | Baja | Agregar heartbeat log; supervisión similar a la del watcher | + +--- + +## Variables de entorno completas (todas las fases) + +```env +# Existentes +OLLAMA_API_BASE=http://localhost:11434 +COMPASS_MODEL=ollama/gemma4:e4b +COMPASS_DOCS_PATH=./data/docs +COMPASS_WORKSPACE=./data/index +COMPASS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 +COMPASS_API_KEY= +COMPASS_RATE_LIMIT=60/minute + +# Fase 2 — Sync +COMPANYBRAIN_SYNC_INTERVAL_MINUTES=5 # intervalo de polling para Gmail/GCal + +# Fase 4 — Alertas y digest +COMPANYBRAIN_DIGEST_HOUR=8 # hora para digest diario (0-23) +COMPANYBRAIN_ALERT_CHECK_INTERVAL=60 # minutos entre evaluaciones de alertas +COMPANYBRAIN_OWNER_PHONE= # ej: +573001234567 (WhatsApp del dueño para digest) +``` diff --git a/docs/data-architecture.md b/docs/data-architecture.md new file mode 100644 index 0000000..c3f932a --- /dev/null +++ b/docs/data-architecture.md @@ -0,0 +1,842 @@ +# CompanyBrain — Arquitectura de datos a escala + +> Diseño de la capa de métricas y el meta-árbol de empresa. +> Complementa `companybrain-mvp-plan.md` con el modelo de datos para razonamiento agéntico a escala. + +--- + +## El problema a resolver + +El sistema actual en `chat.py` tiene dos limitaciones que se vuelven críticas con el crecimiento: + +**Limitación 1 — O(n) en contexto** + +```python +# chat.py — _extract_summaries() +# Carga TODOS los resúmenes de TODOS los nodos de TODOS los documentos seleccionados. +# Con 5 documentos pequeños: ~2k tokens. Con 500 documentos: imposible. +def _extract_summaries(structure_json: str) -> str: + def _collect(nodes, parts): + for node in nodes: + if node.get("summary"): + parts.append(node["summary"]) # todo entra al contexto + if node.get("nodes"): + _collect(node["nodes"], parts) # recursivo sin límite +``` + +**Limitación 2 — Solo documentos como fuente de verdad** + +Un historial de ventas no es un documento. Un patrón estacional no es un archivo Markdown. Los datos numéricos estructurados requieren consultas precisas, no recuperación semántica. Un agente que intenta "recordar" números que leyó en un documento comete errores. Un agente que consulta una tabla con la query correcta siempre es exacto. + +La solución combina dos piezas: +1. **Capa de métricas** — almacenamiento y consulta de datos estructurados temporales. +2. **Meta-árbol de empresa** — árbol jerárquico de resúmenes que cubre toda la empresa, navegable en O(log n). + +Estas dos piezas se conectan mediante **herramientas de agente** que saben exactamente a dónde ir según el tipo de dato. + +--- + +## Los tres planos de datos + +Antes del diseño detallado, la taxonomía fundamental. Cada tipo de dato tiene un hogar natural: + +| Tipo de dato | Ejemplos | Almacenamiento | Acceso del agente | +|---|---|---|---| +| **Estructurado temporal** | ventas, usuarios, sesiones, facturación | Tablas de series de tiempo en SQLite | SQL via herramienta | +| **Conocimiento no estructurado** | documentos, SOPs, decisiones, actas | PageIndex (árbol por documento) | Navegación de meta-árbol | +| **Entidades relacionadas** | clientes, proyectos, personas | SQLite (tablas del MVP plan) | SQL via herramienta | +| **Conversaciones** | chat, WhatsApp, email | SQLite `messages` + resúmenes indexados | Meta-árbol (nodo de conversaciones) | +| **Tendencias externas** | mercado, competidores, búsquedas | Ingestión periódica → PageIndex | Meta-árbol (nodo de tendencias) | + +La regla de oro: **el agente nunca adivina un número — lo consulta. Nunca recuerda contexto — lo navega.** + +--- + +## Parte 1 — Capa de métricas + +### Schema: tablas de series de tiempo + +```sql +-- Eventos crudos — la fuente de verdad, append-only +CREATE TABLE IF NOT EXISTS metric_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + occurred_at DATETIME NOT NULL, + metric_type TEXT NOT NULL, -- ver tipos abajo + value REAL NOT NULL, + currency TEXT, -- 'COP' | 'USD' | 'EUR' (null si no aplica) + dimension_1 TEXT, -- primera dimensión de segmentación + dimension_2 TEXT, -- segunda dimensión (opcional) + entity_id INTEGER, -- FK lógica: client_id, project_id, user_id + entity_type TEXT, -- 'client' | 'project' | 'user' | 'product' + source TEXT, -- 'manual' | 'alegra' | 'hubspot' | 'stripe' | 'whatsapp' + external_id TEXT, -- ID del sistema fuente (para dedup) + metadata TEXT, -- JSON: datos adicionales sin schema fijo + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Índices para los patrones de query más comunes +CREATE INDEX IF NOT EXISTS idx_metric_events_type_date + ON metric_events(metric_type, occurred_at); + +CREATE INDEX IF NOT EXISTS idx_metric_events_entity + ON metric_events(entity_type, entity_id, occurred_at); + +CREATE INDEX IF NOT EXISTS idx_metric_events_external + ON metric_events(source, external_id); -- dedup por fuente + + +-- Agregaciones diarias precomputadas — evita full scans en queries de dashboard +CREATE TABLE IF NOT EXISTS metric_daily ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + metric_type TEXT NOT NULL, + dimension TEXT, -- null = total, o valor de segmentación + total REAL NOT NULL, + count INTEGER NOT NULL, + min_val REAL, + max_val REAL, + avg_val REAL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_metric_daily_unique + ON metric_daily(date, metric_type, COALESCE(dimension, '')); + + +-- Snapshots periódicos de métricas clave — lectura ultrarrápida para el agente +CREATE TABLE IF NOT EXISTS metric_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_date DATE NOT NULL, + metric_type TEXT NOT NULL, + period TEXT NOT NULL, -- 'day' | 'week' | 'month' | 'quarter' | 'year' + value REAL NOT NULL, + change_pct REAL, -- % vs período anterior (null en primer snapshot) + summary_text TEXT, -- ej: "Ventas Q4 2024: $610k (+45% vs Q4 2023)" + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_metric_snapshots_unique + ON metric_snapshots(snapshot_date, metric_type, period); +``` + +**Tipos de métricas (`metric_type`):** + +``` +# Revenue +sale_amount — transacción de venta completada +invoice_issued — factura emitida +invoice_paid — pago recibido +invoice_overdue — factura vencida sin pago + +# Usuarios / Clientes +client_acquired — nuevo cliente +client_churned — cliente que cancela +client_contact — interacción con cliente (reunión, email, llamada) +proposal_sent — propuesta enviada +proposal_won — propuesta aceptada +proposal_lost — propuesta rechazada + +# Proyectos +project_started — inicio de proyecto +project_completed — cierre de proyecto +project_milestone — hito alcanzado +project_delayed — retraso detectado + +# Equipo (si aplica) +employee_hours — horas trabajadas por persona/proyecto +``` + +**Convención de dimensiones:** + +`dimension_1` y `dimension_2` llevan los valores de segmentación más importantes: + +``` +Para sale_amount: + dimension_1 = segmento ('enterprise' | 'smb' | 'startup') + dimension_2 = canal ('referral' | 'inbound' | 'outbound' | 'partner') + +Para client_contact: + dimension_1 = tipo ('meeting' | 'email' | 'whatsapp' | 'call') + dimension_2 = dirección ('inbound' | 'outbound') +``` + +--- + +### Herramientas de consulta para el agente + +Estas funciones son las "tools" que el agente llama directamente. No son búsquedas semánticas — son queries SQL con parámetros validados. + +```python +# backend/metrics.py + +def query_metric( + metric_type: str, + start_date: str, # ISO 8601 date + end_date: str, + dimension: str | None = None, + entity_id: int | None = None, + entity_type: str | None = None, + granularity: str = "month", # 'day' | 'week' | 'month' | 'quarter' | 'year' +) -> list[dict]: + """ + Returns aggregated metric data for the given period. + Uses metric_daily if granularity allows, otherwise queries metric_events. + + Example return: + [ + {"period": "2024-Q4", "total": 610000, "count": 47, "change_pct": 45.2}, + {"period": "2024-Q3", "total": 210000, "count": 31, "change_pct": 12.1}, + ] + """ + +def detect_seasonal_pattern( + metric_type: str, + years_back: int = 3, + dimension: str | None = None, +) -> dict: + """ + Computes the seasonal index for each quarter/month by comparing + each period against the annual average across all years. + + Example return: + { + "peak_period": "Q4", + "peak_multiplier": 2.9, + "trough_period": "Q1", + "trough_multiplier": 0.6, + "by_quarter": {"Q1": 0.6, "Q2": 0.9, "Q3": 1.0, "Q4": 2.9}, + "confidence": "high", # 'high' si >=3 años, 'low' si <3 + "years_analyzed": 3 + } + +def compute_growth( + metric_type: str, + current_period: str, # ej: "2025-Q1" + vs_period: str, # ej: "2024-Q1" (YoY) o "2024-Q4" (QoQ) + dimension: str | None = None, +) -> dict: + """ + Example return: + { + "current": 195000, + "previous": 142000, + "change": 53000, + "change_pct": 37.3, + "trend": "accelerating" # vs the prior comparison period's change_pct + } + +def get_metric_snapshot( + metric_type: str, + period: str = "month", + limit: int = 12, +) -> list[dict]: + """ + Fast read from metric_snapshots. Used for dashboard and digest generation. + Returns most recent N snapshots with pre-computed change_pct. + """ + +def ingest_metric_event( + metric_type: str, + value: float, + occurred_at: str, + dimension_1: str | None = None, + dimension_2: str | None = None, + entity_id: int | None = None, + entity_type: str | None = None, + source: str = "manual", + external_id: str | None = None, + metadata: dict | None = None, +) -> int: + """ + Insert a raw event and queue a metric_daily recompute for that day. + Returns event id. + """ +``` + +**Definición de las tools para el agente** (en `backend/tools.py`): + +```python +METRIC_TOOLS = [ + { + "type": "function", + "function": { + "name": "query_metric", + "description": "Consulta datos históricos de una métrica del negocio por período, segmento o entidad.", + "parameters": { + "type": "object", + "properties": { + "metric_type": { + "type": "string", + "description": "Tipo de métrica: sale_amount, client_acquired, proposal_won, etc." + }, + "start_date": {"type": "string", "description": "Fecha inicio ISO 8601 (YYYY-MM-DD)"}, + "end_date": {"type": "string", "description": "Fecha fin ISO 8601 (YYYY-MM-DD)"}, + "granularity": { + "type": "string", + "enum": ["day", "week", "month", "quarter", "year"], + "description": "Nivel de agregación" + }, + "dimension": {"type": "string", "description": "Filtro de segmento (enterprise, smb, etc.)"} + }, + "required": ["metric_type", "start_date", "end_date"] + } + } + }, + { + "type": "function", + "function": { + "name": "detect_seasonal_pattern", + "description": "Detecta el patrón estacional de una métrica analizando los últimos N años.", + "parameters": { + "type": "object", + "properties": { + "metric_type": {"type": "string"}, + "years_back": {"type": "integer", "default": 3} + }, + "required": ["metric_type"] + } + } + }, + { + "type": "function", + "function": { + "name": "compute_growth", + "description": "Calcula el crecimiento de una métrica entre dos períodos.", + "parameters": { + "type": "object", + "properties": { + "metric_type": {"type": "string"}, + "current_period": {"type": "string", "description": "Ej: '2025-Q1'"}, + "vs_period": {"type": "string", "description": "Ej: '2024-Q1' (YoY) o '2024-Q4' (QoQ)"}, + "dimension": {"type": "string"} + }, + "required": ["metric_type", "current_period", "vs_period"] + } + } + } +] +``` + +--- + +### Proceso de ingestión + +Los datos llegan desde tres vías: + +**Vía 1 — Manual** (siempre disponible) +`POST /metrics/events` — el usuario registra ventas, contactos, hitos directamente desde la UI. + +**Vía 2 — Conectores** (Fase 2 del MVP plan) +Cuando Gmail sincroniza una factura pagada, o HubSpot envía un deal cerrado, el conector llama a `ingest_metric_event()` con `source="alegra"` / `source="hubspot"`. El `external_id` previene duplicados. + +**Vía 3 — Extracción desde documentos** +Cuando se indexa un documento que contiene cifras históricas (reporte anual, resumen de ventas), el pipeline de extracción de entidades (`extractor.py`) también extrae métricas pasadas y las ingesta con `source="document_extraction"`. + +**Recompute de agregaciones** (background job en `scheduler.py`): +- Al insertar un `metric_event`, se marca el día como "dirty" en una tabla `recompute_queue`. +- El scheduler corre cada 5 minutos y procesa la cola: recomputa `metric_daily` para los días marcados. +- `metric_snapshots` se actualiza cada hora para períodos recientes, diariamente para histórico. + +--- + +## Parte 2 — Meta-árbol de empresa + +### El insight central + +`_route_documents()` en `chat.py` ya hace algo parecido a lo que necesitamos: usa los resúmenes del nivel raíz de cada documento para decidir a cuál ir. El meta-árbol generaliza esta idea a **toda la empresa**, con múltiples niveles de navegación. + +En lugar de: *"¿cuál de mis 5 documentos es relevante?"* +El meta-árbol responde: *"¿en qué dominio de la empresa está la respuesta, en qué subdominio, y qué tipo de dato necesito?"* + +La diferencia a escala: con 500 documentos, cargar todos sus resúmenes en el routing prompt es inviable. Con el meta-árbol, el agente siempre ve solo ~10-20 nodos a la vez y navega hacia abajo selectivamente. + +### Schema + +```sql +-- Nodos del árbol de conocimiento de la empresa +CREATE TABLE IF NOT EXISTS knowledge_tree ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_id INTEGER REFERENCES knowledge_tree(id), + node_type TEXT NOT NULL, + -- 'root' | 'domain' | 'subdomain' | 'document' | 'metric_summary' + -- | 'entity_summary' | 'conversation_summary' + name TEXT NOT NULL, + description TEXT, -- etiqueta humana, sin estructura + summary TEXT, -- resumen generado por LLM (~150 palabras) + summary_token_count INTEGER, -- para monitorear crecimiento de contexto + summary_updated_at DATETIME, + is_stale BOOLEAN DEFAULT FALSE, -- true cuando hijos son más nuevos que summary + doc_id TEXT, -- si node_type='document', doc_id de PageIndex + metric_type TEXT, -- si node_type='metric_summary' + entity_type TEXT, -- si node_type='entity_summary' + time_range_start DATE, -- para nodos temporales (sales/2024, etc.) + time_range_end DATE, + depth INTEGER NOT NULL DEFAULT 0, -- 0=root, 1=domain, 2=subdomain... + sort_order INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_tree_parent + ON knowledge_tree(parent_id); + +CREATE INDEX IF NOT EXISTS idx_knowledge_tree_type + ON knowledge_tree(node_type); + +CREATE INDEX IF NOT EXISTS idx_knowledge_tree_doc + ON knowledge_tree(doc_id) + WHERE doc_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_knowledge_tree_stale + ON knowledge_tree(is_stale) + WHERE is_stale = TRUE; +``` + +### Estructura canónica del árbol + +El árbol se inicializa con esta estructura fija al crear la empresa. Los nodos hoja se van poblando automáticamente. + +``` +[root] TechFlow Agency +│ summary: "Agencia digital 5 personas, $450k ARR, 23 clientes activos, +│ 8 proyectos en curso. Q4 es temporada alta (2.9x baseline). +│ Segmento enterprise creciendo, trabajo AI/diseño lidera ingresos." +│ +├── [domain] Comercial +│ │ summary: "23 clientes activos, $450k ARR, +38% YoY. +│ │ Top 5 enterprise = 60% revenue. 4 propuestas activas." +│ │ +│ ├── [subdomain] Ventas +│ │ │ summary: "Registro histórico en 2024 ($610k Q4). Ciclo +│ │ │ de ventas promedio 18 días. Canal referidos = 70%." +│ │ ├── [metric_summary] Ventas / 2025 ← nodo auto-generado +│ │ ├── [metric_summary] Ventas / 2024 +│ │ └── [metric_summary] Ventas / 2023 +│ │ +│ ├── [subdomain] Clientes +│ │ │ summary: "23 activos, 4 en riesgo de churn (sin contacto >30 días). +│ │ │ LTV promedio $19k. Mejor segmento: enterprise tech." +│ │ └── [entity_summary] Clientes activos ← regenerado cada 6h +│ │ +│ ├── [subdomain] Propuestas +│ │ └── [metric_summary] Propuestas / 2025 +│ │ +│ └── [subdomain] Tendencias de mercado +│ ├── [document] análisis-mercado-2026.md ← hoja PageIndex +│ └── [document] trends-Q1-2026.md +│ +├── [domain] Proyectos +│ │ summary: "8 activos, 2 con riesgo de deadline. +│ │ Capacidad actual al 85%. Entrega promedio 6 semanas." +│ ├── [subdomain] Activos +│ │ └── [entity_summary] Proyectos activos +│ └── [subdomain] Completados +│ └── [entity_summary] Proyectos 2024 +│ +├── [domain] Conocimiento operativo +│ │ summary: "Documentados: onboarding, servicios, procesos. +│ │ 3 documentos indexados. Último update: hace 2 semanas." +│ ├── [document] servicios.md +│ ├── [document] onboarding-clientes.md +│ ├── [document] onboarding-equipo.md +│ └── [document] proveedores.md +│ +├── [domain] Decisiones +│ │ summary: "12 decisiones en 2025. Pricing subió 15% en Q1. +│ │ 3 decisiones reversadas. Última: cambio stack React+Vite." +│ ├── [subdomain] Pricing +│ │ └── [document] decisiones.md ← mismos docs, clasificados por dominio +│ └── [subdomain] Equipo y herramientas +│ └── [document] decisiones.md +│ +└── [domain] Métricas recientes + summary: "Última semana: 2 ventas ($18k), 1 nuevo cliente, + 3 contactos con clientes, 1 propuesta enviada." + └── [metric_summary] Actividad / últimos-7-días +``` + +**Observación clave:** un mismo documento puede aparecer en múltiples nodos del árbol (ej: `decisiones.md` bajo Pricing Y bajo Equipo). El árbol organiza por **dominio de negocio**, no por archivo. El `doc_id` apunta al mismo árbol de PageIndex en ambos casos. + +### Mecanismo de actualización incremental + +Cuando cambia algo (nuevo documento indexado, nueva métrica ingresada, nueva entidad creada): + +``` +Evento disparador → actualizar nodo hoja → propagar hacia arriba + +1. Ocurre un evento: + - Se indexa "propuesta-clienteX.pdf" → nodo tipo 'document' creado bajo Comercial/Propuestas + - Entra una venta de $25,000 → nodo 'metric_summary' Ventas/2025 marcado stale + - Se registra contacto con cliente ABC → nodo 'entity_summary' Clientes marcado stale + +2. El nodo afectado se marca is_stale = TRUE + +3. El scheduler (cada hora por default, configurable) corre refresh_stale_summaries(): + a. Encuentra todos los nodos stale + b. Para cada uno, regenera el summary usando LLM: + - Reúne los summaries de todos sus hijos directos + - Prompt: "Dado estos resúmenes de subsecciones, genera un resumen de 150 palabras + del estado actual de {domain}. Incluye números clave, tendencias y alertas." + - Guarda el nuevo summary, marca is_stale = FALSE + c. Marca el padre como is_stale = TRUE (la propagación sube un nivel) + d. Repite hasta llegar al root + +4. El root siempre refleja el estado actual de la empresa + (con un lag máximo igual al intervalo del scheduler) +``` + +**Costo del refresh:** un nodo stale = 1 llamada LLM con contexto pequeño (~500 tokens entrada, ~200 salida). Para un árbol de profundidad 4 con factor de ramificación 5, un cambio en hoja = máximo 4 refreshes (uno por nivel). Muy barato. + +```python +# backend/knowledge_tree.py + +async def refresh_stale_summaries(model: str, max_nodes: int = 20) -> int: + """ + Procesa hasta max_nodes nodos stale, empezando por los más profundos + (bottom-up para que el padre ya tenga hijos frescos al ser procesado). + Retorna el número de nodos actualizados. + """ + +async def _regenerate_summary(node_id: int, model: str) -> str: + """ + Reúne los summaries de los hijos del nodo y llama al LLM para condensar. + Para nodos tipo 'document': usa la raíz del árbol PageIndex correspondiente. + Para nodos tipo 'metric_summary': usa get_metric_snapshot() formateado. + Para nodos tipo 'entity_summary': usa una query SQL sobre las tablas de entidades. + """ + +def get_company_map(depth: int = 1) -> dict: + """ + Retorna el árbol desde la raíz hasta `depth` niveles. + depth=1: root + dominios (siempre en contexto, ~800 tokens) + depth=2: root + dominios + subdominios (~2500 tokens) + Usado por el agente al inicio de cada conversación. + """ + +def get_node_children(node_id: int) -> list[dict]: + """ + Retorna los hijos directos de un nodo con sus summaries. + El agente llama esto para navegar un nivel hacia abajo. + """ + +def mark_subtree_stale(node_id: int) -> None: + """ + Marca un nodo y todos sus ancestros como stale. + Llamado por indexer.py al indexar un nuevo documento, + por metrics.py al ingestar un evento, por database.py al crear/actualizar entidades. + """ +``` + +--- + +## Parte 3 — Protocolo de navegación del agente + +Esta es la pieza que une las dos partes anteriores. Define cómo el agente usa el meta-árbol y las herramientas de métricas para responder preguntas complejas. + +### El contexto siempre cargado + +En toda conversación, el `SYSTEM_PROMPT` incluye el company map (profundidad 1): + +```python +# chat.py — modificación al SYSTEM_PROMPT + +company_map = get_company_map(depth=1) +# ~800 tokens, siempre fresco (máx 1h de lag) + +SYSTEM_PROMPT = f"""Eres CompanyBrain, el cerebro operativo de la empresa. + +MAPA DE CONOCIMIENTO DE LA EMPRESA: +{format_company_map(company_map)} + +Para responder, puedes: +1. Usar el contexto de documentos proporcionado (para preguntas sobre procesos, decisiones, equipo) +2. Llamar herramientas para consultar datos numéricos precisos (ventas, crecimiento, patrones) +3. Navegar el mapa para identificar qué subdominios explorar + +Cita siempre la fuente: [documento — sección] para conocimiento, [métrica — período] para datos. +... +""" +``` + +El mapa de la empresa se ve así en el prompt (texto, no JSON): + +``` +CONOCIMIENTO DISPONIBLE: +• Comercial (23 clientes, $450k ARR, +38% YoY. 4 propuestas activas.) +• Proyectos (8 activos, 2 en riesgo de deadline. Capacidad al 85%.) +• Conocimiento operativo (servicios, onboarding, proveedores — 4 docs) +• Decisiones (12 en 2025. Pricing +15% Q1. Último: stack React+Vite.) +• Métricas recientes (última semana: 2 ventas $18k, 1 nuevo cliente) +``` + +Esto son ~150 tokens. El agente sabe qué dominios existen y puede decidir si necesita profundizar. + +### Los tres modos de recuperación + +**Modo A — Respuesta directa desde contexto** +Para preguntas sobre procesos, políticas, equipo: el flujo actual de `chat.py` es correcto. El router selecciona documentos relevantes, `_extract_summaries()` los carga. + +Cambio: en lugar de cargar TODOS los summaries de un árbol, se carga solo hasta un nivel de profundidad configurable. Si el agente necesita más detalle, navega explícitamente. + +**Modo B — Consulta de datos estructurados** +Para preguntas sobre números, métricas, tendencias: el agente llama herramientas directamente. + +```python +# El agente detecta que necesita datos numéricos y llama: +result = query_metric("sale_amount", "2023-01-01", "2025-12-31", granularity="quarter") +# Recibe datos exactos, no texto aproximado +``` + +**Modo C — Navegación profunda del árbol** +Para preguntas complejas que cruzan dominios: el agente navega el árbol un nivel a la vez. + +```python +# El agente llama la herramienta navigate_tree cuando el mapa indica +# que la respuesta está en un subdominio específico: +children = get_node_children(node_id=domain_comercial_id) +# Recibe summaries de: Ventas, Clientes, Propuestas, Tendencias +# Decide en cuál subdirección profundizar +``` + +### Herramienta de navegación para el agente + +```python +NAVIGATION_TOOLS = [ + { + "type": "function", + "function": { + "name": "navigate_knowledge_tree", + "description": "Navega el árbol de conocimiento de la empresa para encontrar información específica en un dominio o subdominio.", + "parameters": { + "type": "object", + "properties": { + "node_id": { + "type": "integer", + "description": "ID del nodo a expandir. Usa el ID del dominio relevante del mapa de empresa." + }, + "expand_documents": { + "type": "boolean", + "default": False, + "description": "Si true, para nodos tipo 'document' también retorna el contexto completo del documento." + } + }, + "required": ["node_id"] + } + } + } +] +``` + +--- + +## El workflow completo: decisión basada en 4 fuentes + +**Pregunta:** *"¿Debemos lanzar una nueva línea de servicio premium en Q4 de este año?"* + +### Paso 1 — Planificación (1 llamada LLM pequeña, ~1s) + +El agente recibe la pregunta + el company map. Genera un plan: + +``` +Necesito: + [A] Historial de ventas por trimestre, últimos 3 años → query_metric() + [B] Crecimiento de clientes por segmento → compute_growth() + [C] Patrón estacional de ingresos → detect_seasonal_pattern() + [D] Tendencias de mercado → navigate_knowledge_tree(domain=Comercial/Tendencias) + [E] Decisiones pasadas sobre lanzamientos → navigate_knowledge_tree(domain=Decisiones) +``` + +### Paso 2 — Recuperación paralela (~2-3s, sin LLM) + +Las 5 consultas corren en paralelo (asyncio.gather): + +```python +sales_history, client_growth, seasonal, _ , _ = await asyncio.gather( + query_metric("sale_amount", "2022-01-01", "2025-04-30", granularity="quarter"), + compute_growth("client_acquired", "2025-Q1", "2024-Q1", dimension="enterprise"), + detect_seasonal_pattern("sale_amount", years_back=3), + navigate_knowledge_tree(node_id=tendencias_node_id), + navigate_knowledge_tree(node_id=decisiones_node_id), +) +``` + +**Resultados concretos:** + +```python +sales_history = [ + {"period": "2022-Q4", "total": 280000, "count": 21}, + {"period": "2023-Q4", "total": 420000, "count": 34, "change_pct": 50}, + {"period": "2024-Q4", "total": 610000, "count": 47, "change_pct": 45.2}, + {"period": "2024-Q1", "total": 180000, ...}, + ... +] + +client_growth = { + "current": 8, "previous": 4, + "change_pct": 100, # enterprise duplicó en YoY + "trend": "accelerating" +} + +seasonal = { + "peak_period": "Q4", + "peak_multiplier": 2.9, + "trough_period": "Q1", + "trough_multiplier": 0.58, + "confidence": "high", + "years_analyzed": 3 +} + +trends_context = """ +[análisis-mercado-2026.md — Tendencias] +Categoría AI-tools: búsquedas +65% YoY. Empresas aumentando presupuesto en herramientas +digitales. Segmento enterprise prioriza soluciones integradas sobre puntuales. + +[trends-Q1-2026.md — Resumen ejecutivo] +SaaS tradicional -12% en nuevas adquisiciones. Servicios managed/consultivos +28%. +Oportunidad: empresas buscan acompañamiento, no solo software. +""" + +decisions_context = """ +[decisiones.md — Decisiones de pricing] +Mar-2024: Lanzamos Plan Pro para enterprise. Resultado a 90 días: +22% conversión +enterprise, sin impacto en SMB. Decisión: mantener diferenciación por segmento. +""" +``` + +### Paso 3 — Ensamblaje de contexto (~50ms) + +Los resultados se convierten en un bloque de contexto compacto y estructurado, no en texto libre: + +``` +DATOS RECUPERADOS PARA ANÁLISIS: + +VENTAS HISTÓRICAS (Q4 últimos 3 años): + Q4 2022: $280k | Q4 2023: $420k (+50%) | Q4 2024: $610k (+45%) + Tendencia Q4: crecimiento sostenido, acelerando. + +CRECIMIENTO CLIENTES ENTERPRISE (YoY Q1): + 4 → 8 clientes (+100%). Tendencia: acelerando. + +PATRÓN ESTACIONAL (3 años de datos): + Q4 es 2.9x el baseline trimestral. Q1 cae a 0.58x (-42% vs promedio). + Riesgo: cualquier lanzamiento en Q4 enfrenta cliff fuerte en enero. + +TENDENCIAS DE MERCADO: + + AI-tools: +65% en búsquedas [análisis-mercado-2026.md] + + Servicios managed/consultivos: +28% [trends-Q1-2026.md] + - SaaS tradicional: -12% en adquisición nueva + +DECISIONES HISTÓRICAS RELEVANTES: + Plan Pro (Mar-2024): +22% conversión enterprise. SMB no respondió. [decisiones.md] + +PREGUNTA: ¿Lanzar línea premium en Q4? +``` + +Total de tokens en este contexto: ~600. Mucho menos que cargar todos los documentos completos. + +### Paso 4 — Razonamiento final (1 llamada LLM principal, ~5-8s) + +El LLM razona sobre datos concretos y citables: + +``` +RECOMENDACIÓN: Lanzar en Q4, orientado exclusivamente a Enterprise. + +FUNDAMENTOS: +• El patrón 2.9x de Q4 maximiza la exposición del lanzamiento [métrica estacional, 3 años] +• Enterprise duplicó YoY y respondió bien a Plan Pro en 2024 [decisiones.md + métricas] +• Tendencia managed services +28% valida una propuesta premium de alto acompañamiento + [análisis-mercado-2026.md] +• SMB nunca respondió a premium — no enfocar recursos ahí [decisiones.md] + +RIESGO PRINCIPAL: +El cliff de enero (caída a 0.58x del promedio) golpea duro a nuevos servicios sin +base de clientes recurrentes. Diseñar el producto con contrato mínimo anual +para mitigar abandono post-Q4 es crítico. + +ACCIÓN SUGERIDA: +• Lanzar 15 octubre, solo tier Enterprise, contrato mínimo 12 meses +• Precio: escalar desde el Plan Pro existente (+40-60%) +• Canales: referidos (70% del pipeline actual) + outreach directo top 20 prospects +• Métrica de éxito a 90 días: 3 contratos cerrados antes de diciembre +``` + +**Latencia total estimada:** 8-12 segundos. Comparable a una búsqueda en Google para una pregunta que normalmente requeriría 2 horas de análisis manual. + +--- + +## Implementación: qué construir y en qué orden + +Este diseño introduce dos nuevos módulos al codebase: + +### `backend/metrics.py` — Capa de métricas + +``` +Construir: + ingest_metric_event() — insert en metric_events + queue recompute + query_metric() — select con agregación desde metric_daily + detect_seasonal_pattern() — análisis estadístico sobre 3+ años + compute_growth() — delta entre dos períodos + get_metric_snapshot() — read rápido de metric_snapshots + recompute_daily() — job que procesa la cola de recomputes + +Tests en tests/test_metrics.py: + - ingestión y dedup por external_id + - query por período y dimensión + - detección de patrón estacional (datos mock de 3 años) + - cómputo de crecimiento YoY y QoQ +``` + +### `backend/knowledge_tree.py` — Meta-árbol + +``` +Construir: + init_tree() — crea la estructura canónica de dominios al inicializar la empresa + get_company_map(depth) — retorna árbol desde root hasta depth niveles + get_node_children(node_id) — retorna hijos directos con summaries + mark_subtree_stale(node_id) — marca nodo + ancestros como stale + refresh_stale_summaries() — regenera summaries de nodos stale (bottom-up) + _regenerate_summary() — 1 llamada LLM para condensar summaries de hijos + add_document_node() — llamado por indexer.py al indexar un doc nuevo + add_metric_summary_node() — llamado al crear un período nuevo de métricas + +Tests en tests/test_knowledge_tree.py: + - estructura inicial correcta + - propagación de staleness hacia arriba + - refresh bottom-up en el orden correcto + - get_company_map retorna el depth correcto +``` + +### Modificaciones a archivos existentes + +**`backend/database.py`:** Agregar tablas `metric_events`, `metric_daily`, `metric_snapshots`, `knowledge_tree` en `init_db()`. + +**`backend/indexer.py`:** Al final de `index_document()`, llamar a `knowledge_tree.add_document_node()` y `knowledge_tree.mark_subtree_stale()`. + +**`backend/chat.py`:** Modificaciones en `process_chat()`: +1. Al inicio, cargar `get_company_map(depth=1)` e incluirlo en el system prompt. +2. Agregar `METRIC_TOOLS` y `NAVIGATION_TOOLS` a la llamada LiteLLM. +3. Si la respuesta contiene `tool_calls`, ejecutar las herramientas y hacer una segunda llamada con los resultados. + +**`backend/scheduler.py`** (Fase 4 del MVP plan): agregar `refresh_stale_summaries()` al ciclo de jobs. + +**`backend/main.py`:** Endpoint `POST /metrics/events` para ingestión manual. + +### Variables de entorno nuevas + +```env +COMPANYBRAIN_TREE_REFRESH_INTERVAL=60 # minutos entre refreshes del meta-árbol +COMPANYBRAIN_METRICS_RECOMPUTE_INTERVAL=5 # minutos entre recomputes de agregaciones +COMPANYBRAIN_MAP_DEPTH=1 # profundidad del company map siempre en contexto +``` + +--- + +## Restricciones de diseño importantes + +**SQLite es suficiente para el MVP.** Con índices correctos, SQLite maneja cómodamente 10 millones de filas en `metric_events` para una empresa de 50 personas en 5 años. El límite práctico de SQLite (escrituras concurrentes) no es problema para un sistema single-tenant. Si la empresa crece a múltiples usuarios simultáneos escribiendo métricas, migrar `metric_events` a PostgreSQL es quirúrgico — las queries no cambian. + +**Los summaries del árbol son aproximados, no autoritativos.** El agente siempre cita la fuente original (el documento PageIndex o la métrica consultada), nunca el summary del árbol. El árbol es para navegación, no para respuestas. + +**El refresh del árbol tiene lag intencional.** No necesita ser tiempo real. Un lag de 1 hora en el summary del root es aceptable para un negocio de 5-20 personas. El agente siempre puede usar las herramientas directas (`query_metric`) para datos exactos cuando la pregunta lo requiere. + +**La profundidad del árbol no debe superar 5.** Más profundidad aumenta la latencia de refresh y la complejidad de navegación sin beneficio proporcional. Si hay más granularidad, agregar dimensiones al nodo existente en lugar de crear subnodos. diff --git a/docs/design-prompt.md b/docs/design-prompt.md new file mode 100644 index 0000000..4cb9bf4 --- /dev/null +++ b/docs/design-prompt.md @@ -0,0 +1,439 @@ +# CompanyBrain — Design Prompt +> For AI design tools (v0, Galileo, Figma AI, Anima, etc.) +> This prompt defines a complete design system and detailed prototype for CompanyBrain. + +--- + +## Product identity + +**CompanyBrain** is an AI-first operational brain for small businesses. It knows everything about a company — its people, clients, projects, decisions, communications — and answers questions, surfaces insights, and takes actions. It feels like the most intelligent member of the team: always available, never forgets, anticipates what you need. + +The product has two entry points: +- **Web app** — the command center. Dense, data-rich, built for focus. +- **WhatsApp** — frictionless access on the go. Ask anything in natural language and get a cited, structured answer. + +The visual language must communicate: **intelligence, trust, precision, and calm authority**. Not a chatbot. Not a dashboard. A brain. + +--- + +## Part 1 — Design System + +### 1.1 Philosophy: The Nocturnal Architect + +The design system is called **The Nocturnal Architect**. Core beliefs: + +- **Information is a precious material.** Treat it with editorial restraint. +- **Light is intelligence.** In a deep dark environment, only meaningful content is illuminated. The AI's responses glow; chrome is invisible. +- **No lines.** Hierarchy through tonal depth, never borders. Space is the separator. +- **Data visualization as first-class UI.** Charts, timelines, entity graphs, and status indicators are not decorations — they are the primary interface. + +### 1.2 Color tokens + +``` +VOID #0e0e13 — the infinite background, used sparingly +BACKGROUND #131318 — main canvas +SURFACE #1b1b20 — primary workbench layer +SURFACE_HIGH #2a292f — cards, active elements, hover states +SURFACE_HIGHEST #35343a — selected state, popovers + +OUTLINE #424753 — ghost borders at 15–20% opacity only +OUTLINE_MUTED #8c909e — metadata separators + +ON_SURFACE #e4e1e9 — primary text (never pure white #ffffff) +ON_DIM #c2c6d5 — secondary text, descriptions +ON_MUTED #909094 — tertiary text, timestamps, metadata + +PRIMARY #acc7ff — blue, AI responses, links, key actions +PRIMARY_CONT #508ff8 — gradient endpoint for CTAs +PRIMARY_DARK #005bbf — pressed/active state for primary elements + +SECONDARY #cebdff — purple, AI suggestions, insight chips +SECONDARY_CONT #4f319c — suggestion chip backgrounds + +TERTIARY #49e095 — green, success states, connected status, positive data + +ERROR #ffb4ab — red-pink, errors, disconnected, critical alerts +ERROR_CONT #93000a — error container backgrounds + +AMBER #F7B84F — warnings, caution, pending human review +``` + +**Gradient rule:** Primary CTAs use a 135° linear gradient from `PRIMARY` (#acc7ff) to `PRIMARY_CONT` (#508ff8). Never a flat hex. + +**Glass rule:** Floating modals use `SURFACE` at 70% opacity + `backdrop-blur: 20px`. Context bleeds through. + +### 1.3 Typography + +Three typefaces, each with a strict purpose: + +| Face | Use | Never use for | +|---|---|---| +| **Inter** (sans-serif) | Body, headlines, UI labels, all prose | Code, raw data | +| **JetBrains Mono** (monospace) | Code blocks, session IDs, API keys, raw AI data, timestamps in metadata | Any human-readable prose | +| **Space Grotesk** (display) | `label-sm`, `label-md` metadata chips, section headers, connector type badges | Long body text | + +**Scale:** +``` +display-lg 3.5rem / 700 — welcome states, zero-data screens +display-sm 2.25rem / 600 — page headers +headline-lg 1.5rem / 600 — section titles +headline-md 1.25rem / 600 — card headers +body-lg 1rem / 400 — main body, chat messages +body-md 0.875rem / 400 — secondary content, descriptions +label-md 0.75rem / 500 / Space Grotesk — metadata, badges +label-sm 0.625rem / 500 / Space Grotesk — micro-labels, sub-captions +mono-sm 0.75rem / JetBrains Mono — IDs, timestamps, tokens +``` + +### 1.4 Surfaces and elevation + +``` +Layer 0 VOID (#0e0e13) — absolute background, rarely used +Layer 1 BACKGROUND (#131318) — main canvas +Layer 2 SURFACE (#1b1b20) — sidebar, persistent navigation +Layer 3 SURFACE_HIGH (#2a292f) — cards, modules, input fields +Layer 4 SURFACE_HIGHEST (#35343a) — selected rows, open dropdowns, tooltips +Layer 5 Glass (SURFACE at 70% + blur 20px) — floating modals, command palette +``` + +**Shadow rule:** Ambient only. Use `rgba(from ON_SURFACE, 0.06)` at `blur: 48px, spread: -8px`. Never pure black shadows. Never shadows on buttons. + +### 1.5 Component library + +#### Buttons + +``` +PRIMARY_BTN gradient(PRIMARY → PRIMARY_CONT, 135°), radius: 8px, + label-md uppercase, height: 40px, px: 20px + hover: opacity +10%, no shadow + +SECONDARY_BTN transparent, ghost border (OUTLINE at 20%), radius: 8px + label-md, height: 40px, px: 20px + hover: SURFACE_HIGH background + +GHOST_BTN no background, no border, ON_MUTED text + hover: ON_DIM text, subtle underline + +ICON_BTN 32×32px, SURFACE_HIGH background, radius: 8px + hover: SURFACE_HIGHEST + +DANGER_BTN ERROR_CONT background, ERROR text + used only for destructive confirmations +``` + +#### Input fields + +``` +BASE SURFACE background, no border, radius: 4px + ON_DIM placeholder text, body-md + height: 44px, px: 16px + +FOCUS ghost border (PRIMARY at 40%, 1px), helper text → PRIMARY color + +CHAT special: full-width, SURFACE background, radius: 12px + multiline auto-grow, JetBrains Mono for message content + send button floats inside right edge +``` + +#### Cards + +``` +STANDARD SURFACE_HIGH background, radius: 16px, no borders + padding: 24px, ambient shadow + +DATA_CARD asymmetric: header 100%, content 65% / metadata 35% + SURFACE_HIGH bg, radius: 16px + +ENTITY_CARD compact: 72px height, SURFACE background + left: colored type indicator (4px strip) + hover: SURFACE_HIGH background shift (no animation jump) + +ACTION_CARD AMBER at 8% opacity background, radius: 12px + left border: 3px solid AMBER + "Aprobar" (PRIMARY gradient btn) / "Rechazar" (ghost) +``` + +#### AI-specific components + +``` +THINKING_INDICATOR Three dots, PRIMARY color, slow pulse (1.4s ease-in-out) + "CompanyBrain está analizando..." label in label-sm / ON_MUTED + +STREAM_CURSOR 1px × 1em blinking vertical bar, PRIMARY color + appears at text insertion point during streaming response + +SOURCE_CHIP SURFACE_HIGHEST bg, OUTLINE ghost border at 15% + document icon + doc name in mono-sm + click → expands inline with section excerpt + +SUGGESTION_CHIP SECONDARY_CONT bg, SECONDARY text, radius: 9999px + "💡" prefix, label-md / Space Grotesk + hover: SECONDARY_CONT at 70%, glow effect + +STATUS_DOT 8px circle: TERTIARY = connected, AMBER = syncing, + ERROR = disconnected, ON_MUTED = inactive + paired with mono-sm status text + +INSIGHT_BADGE SECONDARY_CONT bg, radius: 6px, label-sm + used on sidebar items with unread alerts count +``` + +#### Data visualization + +All charts: dark-first, no white backgrounds, axes in ON_MUTED, gridlines in OUTLINE at 10% opacity. + +``` +ACTIVITY_TIMELINE horizontal, event dots in TERTIARY/AMBER/ERROR by type + last-contact marker with days-ago label in mono-sm + +ENTITY_MINI_GRAPH force-directed, client nodes (PRIMARY) → project nodes (SECONDARY) + → decision nodes (TERTIARY), ON_MUTED edge lines + hover node: expand with label tooltip + +STATUS_SPARKLINE thin 2px line, PRIMARY color, no fill + SURFACE_HIGH background panel, 7-day or 30-day view + +METRIC_RING donut chart, 120px, single metric + center label + TERTIARY fill, SURFACE_HIGH track + +ALERT_HEATMAP calendar-style grid, cell color: AMBER → ERROR by severity + used in Alerts overview screen +``` + +#### Navigation + +``` +SIDEBAR width: 240px, SURFACE background (Layer 2) + logo + product name at top, nav items, user avatar at bottom + active item: SURFACE_HIGH bg + PRIMARY left border (2px) + hover: SURFACE_HIGH bg + +NAV_ITEM height: 40px, px: 16px, radius: 8px + icon (20px) + label in body-md + badge: INSIGHT_BADGE floated right for alerts count + +TOPBAR height: 56px, SURFACE background + breadcrumb left, connection status + model name right + no visible border — tonal separation from content below +``` + +--- + +## Part 2 — Prototype: All Screens + +### Screen 1 — Chat (main) + +**Purpose:** The primary interface. Ask anything about the company. + +**Layout:** Left sidebar (240px) + main area. Main area is asymmetric: response column 65%, context panel 35% (slides in when source citations exist). + +**Elements:** +- **Zero state:** `display-lg` headline "¿Qué quieres saber de tu empresa?" centered. Below: 3 `SUGGESTION_CHIP` example prompts ("¿Quiénes son nuestros clientes activos?", "¿Qué decisiones tomamos este mes?", "¿Hay proyectos en riesgo?"). CompanyBrain logomark above in 64px, slow breathing glow in PRIMARY. +- **Message thread:** User messages right-aligned, SURFACE_HIGHEST bg, body-lg. AI messages left-aligned, no bubble — prose on BACKGROUND directly. AI messages have `COMPASS INTELLIGENCE` label above in label-sm / Space Grotesk / ON_MUTED, with 20px AI icon. +- **Source citations:** `SOURCE_CHIP` row below each AI message. On click, a 35%-wide context panel slides in from right showing the exact doc section with highlighted text. +- **ActionCard:** When AI proposes an action (e.g., draft email), it appears below the message as an `ACTION_CARD` with amber left border, action summary, and two buttons. +- **Suggestion chips:** `SUGGESTION_CHIP` rows for follow-up questions auto-generated by the AI. +- **Input area:** Bottom-anchored, SURFACE bg, radius: 12px. Paperclip icon for file attachment, send arrow. `SHIFT+ENTER` label in mono-sm / ON_MUTED on the right. Session info (session ID, message count) in mono-sm below. +- **Thinking state:** Between sending and first token: `THINKING_INDICATOR` replaces message area. Source chip skeletons pulse in SURFACE_HIGH. +- **Streaming:** Text appears word by word with `STREAM_CURSOR` at insertion point. + +--- + +### Screen 2 — Knowledge Base + +**Purpose:** View and manage indexed documents. Trigger entity extraction. + +**Layout:** Full-width, two sections: document grid above, extraction status below. + +**Elements:** +- **Header:** "Base de Conocimiento" in headline-lg, "+ Agregar documento" PRIMARY_BTN right-aligned. +- **Document grid:** 3-column grid of `DATA_CARD`s. Each card: document icon (colored by type: PDF=ERROR, MD=TERTIARY, TXT=ON_MUTED), filename in headline-md, doc type badge in label-sm / Space Grotesk, page count in mono-sm, indexed date. Bottom: "Ver estructura" ghost btn + "Extraer entidades" secondary btn. +- **Extraction status banner:** When extraction runs, a SURFACE_HIGH full-width bar appears below header with progress: "Extrayendo entidades de servicios.md... 3/5 documentos" + thin PRIMARY progress bar. +- **Empty state:** Illustration of a glowing document node, `display-sm` "Ningún documento indexado aún", body-md instruction, drop zone with dashed ghost border (OUTLINE at 20%). +- **Drag & drop overlay:** When file is dragged over, full-screen BACKGROUND at 80% opacity + SURFACE_HIGH centered panel with dashed border + drop icon in PRIMARY. + +--- + +### Screen 3 — Entities Dashboard + +**Purpose:** Structured memory view — People, Clients, Projects, Decisions as live entities. + +**Layout:** Full-width. Horizontal tab bar at top. Tab content below. + +**Tab bar:** "Personas", "Clientes", "Proyectos", "Decisiones" — Space Grotesk label-md, active: PRIMARY underline (2px). + +**People tab:** +- Compact list of `ENTITY_CARD`s. Left strip: SECONDARY color. Avatar initials circle in SECONDARY_CONT. Name in body-lg, role in label-md / ON_MUTED. Email + phone in mono-sm. `source_doc` chip bottom-right. "+ Agregar persona" ghost btn at top. + +**Clients tab:** +- Same structure but left strip color: TERTIARY (active), AMBER (prospect), ERROR (inactive). +- Each card: client name, contact name, status badge, `last_contact_at` in mono-sm with color indicator (TERTIARY if < 14 days, AMBER if 14–30, ERROR if > 30 or null). +- Click → Client Detail (Screen 3b). + +**Projects tab:** +- Cards with left strip: TERTIARY (active), PRIMARY (completed), AMBER (paused), ERROR (cancelled). +- Project name, client name link, status badge, deadline in mono-sm. If deadline < 7 days away: AMBER AMBER badge "Vence pronto". +- Last update age displayed as "Actualizado hace 3 días" in mono-sm. + +**Decisions tab:** +- Timeline layout instead of grid. Vertical line in OUTLINE, decision nodes as circles in TERTIARY/AMBER/ERROR by impact. +- Each node: decision title in body-lg, date in mono-sm, made_by in label-md. Expand → context + outcome text. + +**Zero state (any tab):** Glowing empty node illustration, "No hay [entidades] aún", "Agrega documentos para extraer automáticamente" body-md, link to Knowledge Base. + +--- + +### Screen 3b — Client / Project Detail + +**Purpose:** Deep dive on a single entity. + +**Layout:** Asymmetric. Left: 60% main info. Right: 40% related entities sidebar. + +**Client detail left:** +- Header: client name in headline-lg, status badge. Edit icon ghost btn. +- Contact info row: email, phone, contact person — all in mono-sm. +- `ACTIVITY_TIMELINE`: horizontal timeline of last contacts/interactions (placeholder if empty). +- Notes section: markdown-rendered, editable on click. +- Linked projects: compact `ENTITY_CARD` list. + +**Right sidebar:** +- "Decisiones relacionadas" — filtered decisions list. +- "Documentos fuente" — SOURCE_CHIPs linking back to original docs. +- "Acciones pendientes" — ACTION_CARDs if any actions are pending for this client. + +--- + +### Screen 4 — Connectors + +**Purpose:** Connect CompanyBrain to live data sources. + +**Layout:** Full-width. Connector cards grid + event feed. + +**Elements:** +- **Header:** "Conectores" headline-lg, "+ Conectar" PRIMARY_BTN. +- **Connector cards:** Large `DATA_CARD`s, 2-column grid. Each card: connector logo (WhatsApp green, Gmail red, GCal blue) at 40px, connector name in headline-md, type badge (Space Grotesk). Status row: `STATUS_DOT` + "Conectado", "Sincronizando…" or "Desconectado" + last sync time in mono-sm. Message count / events processed in mono-sm. Two actions: "Sincronizar ahora" secondary btn, toggle switch (TERTIARY when on). +- **WhatsApp card special:** Shows "Último mensaje recibido: hace 2h" + phone number in mono-sm. +- **Empty state:** 4 greyed-out connector cards (WhatsApp, Gmail, GCal, Slack) as placeholders. "Conecta tus herramientas" overlay. Click any → setup modal. +- **Setup modal:** Glass overlay, centered panel. Step indicator at top (1/3, 2/3, 3/3). Form fields per connector type. "Verificar conexión" PRIMARY_BTN. Connection test result: TERTIARY checkmark or ERROR with message. +- **Event feed:** Right side, 35% width, "Últimos eventos" label-sm. Scrollable list of raw events in mono-sm: timestamp + event type + sender + preview. TERTIARY for processed, ON_MUTED for pending. + +--- + +### Screen 5 — Actions + +**Purpose:** Review and approve AI-proposed actions before execution. + +**Layout:** Split. Left 55%: pending actions. Right 45%: action detail + preview. + +**Elements:** +- **Header:** "Acciones propuestas" headline-lg. Filter tabs: "Pendientes" (AMBER count badge), "Aprobadas", "Rechazadas". +- **Action list:** `ACTION_CARD` for each pending. Shows: action type icon (email, calendar, task), summary line in body-md, proposed time in mono-sm, originating session chip. AMBER left border for pending, TERTIARY for approved, OUTLINE for rejected. +- **Selected action detail (right):** + - Full action title in headline-md. + - "Propuesto en respuesta a:" quote block in body-md italics, SURFACE background, OUTLINE left border. + - For `draft_email`: full email preview — To, Subject, Body rendered in SURFACE_HIGH card. + - For `create_calendar_event`: mini calendar widget showing the proposed slot, attendees list. + - For `create_task`: task card preview with due date and assignee. + - Action buttons: "Aprobar y ejecutar" PRIMARY_BTN + "Rechazar" DANGER_BTN (secondary style). + - After approval: `THINKING_INDICATOR` while executing, then TERTIARY success state "Ejecutado correctamente". +- **Empty state:** "No hay acciones pendientes" + TERTIARY checkmark illustration. + +--- + +### Screen 6 — Alerts + +**Purpose:** Proactive intelligence — triggered alerts and rule configuration. + +**Layout:** Full width. Top: alert rules configuration. Bottom: triggered events feed. + +**Elements:** +- **"Reglas activas" section:** Horizontal row of compact rule cards. Each: rule name, threshold ("sin contacto > 30 días"), delivery badge (Web / WhatsApp / Ambos), toggle. "+ Nueva regla" ghost btn. +- **Alert event feed:** Table layout. Columns: severity dot, entity type + name (link to entity detail), message, triggered time in mono-sm, dismiss button. + - Severity: ERROR = critical (project deadline missed, client lost), AMBER = warning (approaching threshold), TERTIARY = informational. + - Rows are dismissible — fade out on dismiss. +- **"Vista de calor" section:** `ALERT_HEATMAP` — last 30 days as calendar grid. Cell intensity = number of alerts that day. Hover cell → tooltip with alert summary. +- **Zero state:** "El sistema está monitoreando tu empresa" body-md, TERTIARY shield illustration, list of active rules as proof. + +--- + +### Screen 7 — Daily Digest + +**Purpose:** Daily/weekly AI-generated summary of company state. + +**Layout:** Article-style. Full-width, max 720px centered, generous padding. + +**Elements:** +- **Header:** Date in mono-sm / ON_MUTED. "Resumen diario" in display-sm. Subtitle: "Generado por CompanyBrain a las 8:00 AM" in body-md / ON_DIM. +- **Metrics row:** 4 `METRIC_RING` charts side by side: active clients, active projects, pending actions, open alerts. Each ring: TERTIARY fill, SURFACE_HIGH track, center number in headline-lg, label below in label-sm. +- **Digest sections:** AI-generated prose in body-lg, organized under Space Grotesk headers: "Clientes", "Proyectos", "Decisiones recientes", "Atención requerida". Each section cites entities as inline chips (ENTITY_CARD compact inline). +- **"Atención requerida" section:** Highlighted with AMBER subtle background. Bullet list of items needing human action — each with an inline CTA chip. +- **Footer:** "¿Algo incorrecto?" ghost link. "Enviar por WhatsApp" secondary btn. "Ver historial de resúmenes" ghost link. + +--- + +### Screen 8 — Onboarding (First-time setup) + +**Purpose:** Guide a new user from zero to first insight in under 10 minutes. + +**Layout:** Full-screen, centered, step wizard. Progress bar at top. + +**Steps:** + +**Step 1 — Welcome (display-lg "Bienvenido a CompanyBrain")** +- Logomark animated: circle expanding from VOID to glow state. +- Tagline: "Tu empresa, toda en un cerebro." body-lg / ON_DIM. +- "Empezar configuración" PRIMARY_BTN. + +**Step 2 — Upload first documents** +- headline-md "Agrega documentos de tu empresa". body-md instruction. +- Large drop zone (SURFACE_HIGH, dashed ghost border, document icon in PRIMARY). +- "Arrastra PDFs, Markdown, o archivos TXT". Supported formats in mono-sm. +- Skip link for users who want to connect integrations first. + +**Step 3 — Connect integrations (optional)** +- headline-md "Conecta tus herramientas". +- 4 integration cards (WhatsApp, Gmail, GCal, Slack) in a 2×2 grid with "Conectar" secondary btn each. +- "Saltar por ahora" ghost btn. + +**Step 4 — First extraction** +- Progress animation: documents being processed. Each doc shows extraction status: THINKING_INDICATOR → TERTIARY checkmark. +- Extracted entity counts appear live: "3 clientes encontrados", "2 proyectos", "5 decisiones". + +**Step 5 — First question** +- "CompanyBrain está listo." headline-lg, TERTIARY glow. +- Pre-filled chat input with suggested first question pulled from extracted entities. +- "Pregunta esto" PRIMARY_BTN. + +--- + +## Key interaction patterns + +### Streaming AI responses +Text appears word-by-word. As text streams in, source chips materialize below one by one. The right context panel slides in after the first source is identified — not after the full response. This makes the AI feel like it's thinking in real time, not retrieving a cached answer. + +### Human-in-the-loop approval +When an `ACTION_CARD` appears in chat, the input field dims and shows "Revisa la acción propuesta antes de continuar" in label-sm / AMBER. The send button is still active — user can keep chatting. The action card persists until explicitly approved or rejected. + +### Entity linking +Wherever an entity name appears (client, project, person) in any AI response, it is rendered as a subtle inline link — PRIMARY text, no underline, hover shows mini `ENTITY_CARD` tooltip. Click navigates to Entity Detail. + +### Connector sync states +Connectors pulse their `STATUS_DOT` with a slow animation (2s ease-in-out) while syncing. The sidebar nav item for Connectors shows a spinning sync icon during active sync. Never a blocking loading overlay — always optimistic UI. + +### Notification delivery +New alerts cause the sidebar "Alertas" nav item to gain an `INSIGHT_BADGE` with count. No browser push notifications for MVP — the badge is the primary signal. WhatsApp delivery is the secondary signal for mobile. + +--- + +## Responsiveness notes + +The web app targets desktop (1280px+) as primary. Tablets (768px–1280px): sidebar collapses to icons only, context panels become bottom sheets. Mobile is out of scope — WhatsApp is the mobile interface. + +--- + +## What this product is NOT + +- Not a dashboard with KPIs. Metrics appear in context, not as primary UI. +- Not a chatbot. The interface is a command center, the chat is one modality. +- Not generic. Every label, prompt, and empty state is in Spanish, for a LATAM founder managing a real company. +- Not loud. No gradients on backgrounds, no color overload, no animations for decoration. Animation serves information, never aesthetics. diff --git a/docs/pitch.md b/docs/pitch.md new file mode 100644 index 0000000..67efd8a --- /dev/null +++ b/docs/pitch.md @@ -0,0 +1,115 @@ +# CompanyBrain — Pitch + +--- + +## Una línea + +**CompanyBrain es el cerebro operativo de tu empresa: sabe todo, recuerda todo, actúa cuando lo necesitas.** + +--- + +## El problema + +Cada empresa pierde inteligencia todos los días. + +Una decisión tomada en una reunión que no quedó documentada. Un cliente cuyo historial vive en el email de alguien que ya se fue. Un patrón de ventas que nadie analizó porque toma tres días armar el Excel. Una contratación que repite el mismo error de hace dos años porque nadie recuerda qué pasó. + +El conocimiento operativo de una empresa — sus decisiones, sus clientes, sus procesos, sus patrones — está fragmentado en 12 herramientas distintas, en la cabeza de su gente, y en archivos que nadie encuentra. + +El resultado: los dueños de empresa toman decisiones con información incompleta, todos los días. + +--- + +## La oportunidad + +Las empresas que van a ganar en la próxima década no son las que adoptaron IA — son las que nacieron con IA como capa operativa desde el primer día. + +Hoy existe una ventana: la mayoría de las PyMEs de LATAM no tienen un sistema de gestión del conocimiento. No tienen CRM integrado. No tienen análisis de datos. Tienen WhatsApp, Google Drive y la memoria de su equipo. + +Eso no es una debilidad — es la oportunidad. Podemos llegar antes que Salesforce, antes que Microsoft, antes que cualquier solución enterprise que cuesta $50k al año y tarda seis meses en implementar. + +--- + +## La solución + +**CompanyBrain** es el primer sistema AI-first diseñado desde cero para PyMEs de LATAM. + +No es un chatbot. No es un dashboard. Es un cerebro. + +Conecta todos los documentos, conversaciones, métricas y decisiones de la empresa en un único punto de inteligencia. Cualquier persona del equipo puede preguntarle cualquier cosa en lenguaje natural — por WhatsApp, desde el teléfono, sin aprender ninguna herramienta nueva — y recibe una respuesta precisa con la fuente exacta de donde viene la información. + +Pero más importante: **actúa**. Cuando detecta que un cliente no ha tenido contacto en 30 días, lo dice. Cuando ve que un proyecto va a incumplir un deadline, lo avisa. Cuando el dueño dice "redactá un email de seguimiento para ese cliente", lo propone — y el dueño aprueba con un clic. + +--- + +## Cómo funciona (la diferencia técnica) + +La mayoría de los sistemas RAG usan búsqueda vectorial: fragmentan documentos en pedazos y buscan los más parecidos a la pregunta. Funciona para búsqueda, no para razonamiento. + +CompanyBrain usa **árboles de conocimiento jerárquicos**: cada documento se indexa como un árbol de resúmenes estructurados. El sistema construye un árbol maestro de toda la empresa — dominios, subdominios, períodos de tiempo. El agente navega ese árbol como lo haría un analista experto: empieza en lo general, baja a lo específico, consulta los datos exactos cuando los necesita. + +El resultado: puede responder *"¿debemos lanzar un nuevo servicio premium en Q4?"* analizando en paralelo tres años de historial de ventas, el crecimiento por segmento, el patrón estacional, y las decisiones históricas relacionadas — en menos de 15 segundos, con cada afirmación citada en su fuente. + +Todo corre local. Los datos de la empresa nunca salen de la empresa. + +--- + +## El MVP + +**Lo que existe hoy (Compass):** +- Indexación de documentos con árboles jerárquicos de resúmenes +- Q&A en lenguaje natural con citas de fuente +- Detección proactiva de gaps de conocimiento +- Historial de conversaciones por sesión +- API completa con auth y rate limiting +- 58 tests, arquitectura limpia + +**Lo que estamos construyendo (CompanyBrain):** +- Memoria estructurada: clientes, proyectos, personas y decisiones como entidades reales (no solo texto) +- Extracción automática de entidades al indexar documentos +- Conector WhatsApp — preguntale a tu empresa desde el teléfono, sin instalar nada +- Gmail + Google Calendar — lee, redacta, agenda +- Acciones agénticas con aprobación humana en el chat +- Alertas proactivas y resumen diario +- Capa de métricas para razonamiento sobre datos numéricos + +--- + +## El modelo de negocio + +**Dos motores, un flywheel:** + +**Motor 1 — SaaS:** $200–500/mes por empresa. La empresa conecta sus herramientas, CompanyBrain aprende su operación. Sin implementación costosa, sin equipo de IT. + +**Motor 2 — Transformación:** Entramos a una empresa que no es AI-first y la transformamos. Auditamos sus procesos, instalamos CompanyBrain, integramos sus herramientas, capacitamos al equipo. $5k–20k por proyecto + retainer mensual. + +El motor 2 financia el motor 1 en etapa temprana. Cada empresa transformada es un caso de estudio, una referencia, y un cliente SaaS recurrente. La consultoría crea el mercado que el producto captura. + +--- + +## El mercado + +**8 millones de PyMEs en Colombia.** Solo en el segmento de 5-50 empleados con herramientas digitales básicas: más de 400,000 empresas. Multiplica por LATAM. + +Ninguna solución actual las atiende bien: +- **Notion AI / Confluence:** orientado a documentación, no a operación, no a acción +- **Microsoft Copilot:** requiere M365, demasiado caro, demasiado genérico +- **ChatGPT Enterprise:** sin memoria de empresa, sin integraciones, sin acciones +- **Soluciones verticales (Harvey, Ironclad):** un caso de uso por industria + +CompanyBrain es horizontal, operativo, en español, y diseñado para el contexto LATAM desde el primer día. + +--- + +## La visión + +En cinco años, toda empresa nueva nace AI-first. Tiene un cerebro operativo desde el día uno. + +Hoy, el camino más corto para llegar ahí pasa por las PyMEs de LATAM: mercado subatendido, adopción de WhatsApp del 95%, dueños que toman todas las decisiones y no tienen tiempo de buscar información. + +CompanyBrain no es una herramienta más en el stack. Es el sistema operativo de la empresa inteligente. + +--- + +*Construido sobre Compass — base de conocimiento local-first con PageIndex, FastAPI y React.* +*Repositorio: github.com/mrjunos/compass*