Cargando…
\n```\n\n- `?embed=1` oculta el dashboard de fondo y deja la p\u00e1gina transparente \u2014 solo se ve el FAB azul.\n- `email`/`username`/`createUserToken` se persisten en `bot_hdi.sesiones`.\n- `allow=\"microphone\"` es necesario para que el dictado por voz funcione dentro del iframe.\n- El bot emite `postMessage({source:'hdi-chatbot', state:'open'|'closed'})` al padre para que pueda redimensionar.\n\nPara validar el `createUserToken` contra el endpoint real del cliente, setear `BOT_REQUIRE_AUTH=true` y `TOKEN_VALIDATION_URL=https://...` en `.env`.\n\n## Voz\n\nEl chat tiene un bot\u00f3n de micr\u00f3fono (entre el textarea y \"Enviar\") que usa la Web Speech API del navegador. Click \u2192 habla en espa\u00f1ol, transcripci\u00f3n interina en vivo en el textarea \u2192 click para detener o silencio prolongado lo apaga solo. El texto queda en el input para que lo edites antes de mandar.\n\nLimitaciones: solo Chrome/Edge (Firefox/Safari ocultan el bot\u00f3n autom\u00e1ticamente). Requiere internet (Chrome env\u00eda audio a Google) y contexto seguro (HTTPS o localhost).\n\n## Forks por cliente (template)\n\nEste repo est\u00e1 dise\u00f1ado para ser **clonado por cliente**. Todo lo espec\u00edfico de\nHDI (nombre, color, dashboard mockup, ejemplos, conexi\u00f3n a BD) vive en dos\narchivos: `brand.py` y `.env`. El motor (FastAPI, function calling, memoria,\nexports, charts, b\u00fasqueda web, auth, voz, iframe) es el mismo para todos.\n\n### El archivo `brand.py`\n\nEs el \u00fanico que tocas al forkear. Contiene constantes para:\n\n- `BRAND_NAME` \u2014 cliente (HDI, Liverpool, Banorte...)\n- `BRAND_COMPANY` \u2014 consultora (Briyam \u2014 probablemente no cambia)\n- `BRAND_ASSISTANT_NAME` \u2014 nombre completo del bot\n- `BRAND_TAGLINE` \u2014 frase corta debajo del nombre\n- `BRAND_PRIMARY_COLOR` / `BRAND_PRIMARY_DARK_COLOR` \u2014 tema visual\n- `BRAND_DASHBOARD_TITLE` / `BRAND_DASHBOARD_SUBTITLE`\n- `BRAND_DASHBOARD_CARDS` \u2014 tarjetas mockup del fondo\n- `BRAND_EXAMPLE_QUESTIONS` \u2014 ejemplos en el info-box\n- `BRAND_INPUT_PLACEHOLDER`\n- `BRAND_EXTRA_PROMPT` \u2014 texto extra opcional para el system prompt (glosario, pol\u00edticas)\n\nEstos valores se inyectan en el HTML y en el system prompt al arrancar el servidor.\n\n### Script para crear un cliente nuevo: `new-client.ps1`\n\nAutomatiza el fork. Desde Windows / WSL / Linux con PowerShell Core:\n\n```powershell\n# M\u00ednimo: solo copia el repo y pre-configura brand.py + .env\n.\\new-client.ps1 Liverpool\n\n# Completo: adem\u00e1s crea la BD bot-liverpool en SQL Server\n.\\new-client.ps1 -ClientName Liverpool `\n -CreateBotDB `\n -DbServer \"sql.liverpool.com\" `\n -DbAdminUser \"sa\"\n# (pide el password de forma interactiva, sin echo)\n```\n\nLo que hace:\n1. Crea `..\\asistente-liverpool\\` copiando este repo (sin `.git`, `.venv`, `logs`, `static/exports/*`).\n2. Sustituye `\"HDI\"` por `\"Liverpool\"` solo en strings literales de `brand.py`.\n3. Genera `.env` desde `.env.example` con `DB_NAME=biliverpool` y `BOT_DB_NAME=bot-liverpool`.\n4. `git init` + commit inicial (a menos que pases `-NoGitInit`).\n5. Si pides `-CreateBotDB`, conecta a SQL Server y crea la base vac\u00eda idempotentemente (las tablas se crean en el primer arranque). Usa `Invoke-Sqlcmd` o `sqlcmd.exe`, y si no encuentra ninguno, imprime el SQL para que lo corras a mano.\n\n> \u26a0\ufe0f **Pendiente tras la migraci\u00f3n a PostgreSQL.** La memoria del bot ahora vive en PostgreSQL (`bot_hdi`), pero `new-client.ps1` todav\u00eda provisiona la base de memoria en SQL Server (paso 5) y escribe `BOT_DB_NAME=bot-` en el `.env` sin las dem\u00e1s `BOT_DB_*`. Hasta actualizar el script: crea manualmente la base `bot_` en PostgreSQL (`CREATE DATABASE bot_;`) y completa `BOT_DB_HOST/PORT/USER/PASS/NAME` en el `.env` del fork. Las tablas se crean solas al primer arranque.\n\nDespu\u00e9s solo te queda editar `brand.py` (colores, mockup) y `.env` (DEEPSEEK_API_KEY, credenciales reales). Documenta los pendientes obligatorios en el resumen final.\n\n### Script de deploy en Windows + Plesk: `deploy-windows.ps1`\n\nRegistra el bot como servicio Windows autom\u00e1tico via NSSM. Detecta `BRAND_NAME`\ndel `brand.py` del cliente y deriva el nombre del servicio:\n\n```powershell\n# Desde dentro del directorio del cliente\ncd C:\\inetpub\\asistente-liverpool\n.\\deploy-windows.ps1 # detecta BRAND_NAME=Liverpool\n # \u2192 servicio \"AsistenteLiverpool\" en puerto 8000\n\n# Para deployar varios clientes en el mismo servidor, asigna puerto distinto:\ncd C:\\inetpub\\asistente-banorte\n.\\deploy-windows.ps1 -Port 8001 # \u2192 \"AsistenteBanorte\" en puerto 8001\n```\n\nLo que hace:\n1. Valida prerrequisitos (Admin, Python en PATH, NSSM disponible, `main.py` presente, `.env` configurado).\n2. Lee `brand.py` para derivar `ServiceName = \"Asistente\"`.\n3. Avisa si el puerto ya est\u00e1 ocupado por otro proceso ajeno al servicio actual.\n4. Crea o reusa el venv en `.venv\\`, instala `requirements.txt` (a menos que pases `-SkipDeps`).\n5. Crea directorio `logs\\` y registra el servicio con NSSM:\n - Stdout / stderr a archivos rotados a 10 MB.\n - Start type `Automatic`.\n - Restart autom\u00e1tico tras fallo con delay 5s.\n6. Arranca el servicio y prueba `http://127.0.0.1:/` para validar.\n7. Imprime resumen y lista todos los servicios `Asistente*` registrados con su status.\n\n### Flujo end-to-end: cliente nuevo desde cero\n\n```powershell\n# 1. Clonar template + crear BD de memoria\ncd C:\\repos\\asistente_hdi\n.\\new-client.ps1 -ClientName Liverpool -CreateBotDB `\n -DbServer \"sql-prod.liverpool.com\" -DbAdminUser \"dba\"\n\n# 2. Editar branding y credenciales\nnotepad C:\\repos\\asistente-liverpool\\brand.py # color, mockup\nnotepad C:\\repos\\asistente-liverpool\\.env # DEEPSEEK_API_KEY, DB_USER/PASS\n\n# 3. Mover a inetpub y deployar\nMove-Item C:\\repos\\asistente-liverpool C:\\inetpub\\\ncd C:\\inetpub\\asistente-liverpool\n.\\deploy-windows.ps1 -Port 8001 # AsistenteLiverpool en :8001\n\n# 4. En Plesk: crear dominio chatbot-liverpool.briyam.com, emitir SSL,\n# agregar reverse proxy con proxy_pass a http://127.0.0.1:8001\n```\n\nListo, cliente nuevo en producci\u00f3n.\n\n### Lo que NO automatizan los scripts\n\n- **Crear la BD anal\u00edtica** (`bi`) \u2014 es la BD productiva del cliente con sus datos. El bot solo la lee.\n- **Crear el usuario SQL** que va en `.env` \u2014 eso t\u00edpicamente lo hace el DBA del cliente con permisos limitados.\n- **Configurar Plesk** (dominio, SSL, reverse proxy) \u2014 m\u00e1s c\u00f3modo y seguro desde el panel.\n- **Llenar `DEEPSEEK_API_KEY`** \u2014 decisi\u00f3n humana (key compartida o por cliente).\n\n## Personalizaci\u00f3n\n\n- **Branding (logo, nombre, colores, mockup, ejemplos):** edita `brand.py`. Se recarga en el siguiente arranque del server.\n- **Glosario / contexto extra del cliente para el LLM:** edita `brand.py:BRAND_EXTRA_PROMPT`. Se concatena al system prompt base.\n- **System prompt base (estructura, reglas, formato de respuesta):** `SYSTEM_PROMPT_TEMPLATE` en `main.py`. Solo toca esto si necesitas cambiar el comportamiento del bot, no para branding.\n- **F\u00f3rmula de c\u00e1lculo**: en `db.py:_agregados_sql` y `db.py:_build_resultado`. Por defecto sigue la convenci\u00f3n (promotores \u22659, detractores \u22646 sobre escala 0-10). Si cambian los cortes, modificar los `CASE WHEN` ah\u00ed.\n- **Modelo LLM**: variable `DEEPSEEK_MODEL` en `.env`.\n- **Tipos de gr\u00e1fica**: el LLM elige entre `bar`, `doughnut`, `line`. La paleta y opciones de Chart.js est\u00e1n en `static/index.html:appendChart()`.\n- **Formato de exports**: la generaci\u00f3n del xlsx/pdf vive en `main.py:_export_excel` y `_export_pdf`. Cambiar encabezados, agregar hojas, embeddear im\u00e1genes, etc.\n\n## Limitaciones conocidas\n\n- El historial y el \u00faltimo resultado **se persisten en `bot_hdi` (PostgreSQL)** y se restauran al reconectar. Los **cat\u00e1logos** (`cat_metricas`, `cat_filtros`) se cachean al arrancar; rec\u00e1rgalos sin reiniciar con `POST /admin/recargar-catalogos` (requiere `ADMIN_API_KEY`).\n- **Rate limiting** es en memoria (un proceso). Para varios workers, apuntar el storage de `slowapi` a Redis.\n- **Auth**: el scaffolding est\u00e1 endurecido, pero el wiring contra el IdP real del cliente sigue requiriendo su endpoint de validaci\u00f3n (`TOKEN_VALIDATION_URL`).\n- **Gr\u00e1ficas en PDF** requieren `matplotlib` (opcional). Sin \u00e9l, el PDF sale solo con tablas. Las comparaciones multi-dataset no embeben gr\u00e1fica.\n- El PDF usa fuente Helvetica que solo soporta Latin-1. Caracteres tipogr\u00e1ficos ex\u00f3ticos en los datos se sanean autom\u00e1ticamente v\u00eda `_safe_latin1`. Para Unicode completo, registrar una fuente externa con `pdf.add_font(..., uni=True)`.\n- `DB_SERVER` apunta a la IP del gateway WSL en `.env`, que cambia entre reinicios. Para deploy fuera de WSL, ajustar a `localhost` o al hostname/IP del servidor BD.\n- **Multi-conversaci\u00f3n** se guarda en `localStorage` (por navegador). El backend lista conversaciones por `email` (`/api/conversations`), pero la UI standalone no unifica entre navegadores distintos salvo que se pase `email`.\n- Voz solo en Chrome/Edge. Firefox y Safari (iOS) no implementan `SpeechRecognition`.\n- **Pool de conexiones** implementado (`DB_POOL_ENABLED`); su l\u00f3gica est\u00e1 cubierta por unit tests, conviene una prueba de carga real antes de producci\u00f3n.\n- **B\u00fasqueda web** con DuckDuckGo (default, sin API key) rate-limitea por IP y falla bajo r\u00e1fagas (degrada con gracia). Para fiabilidad, usar `WEB_SEARCH_PROVIDER=tavily|brave` con `WEB_SEARCH_API_KEY`.\n"; marked.setOptions({ gfm: true, breaks: false }); let html = marked.parse(markdown); // Rewrite enlaces relativos a otros .md → rutas /docs/ const linkMap = {"ARQUITECTURA.md": "/docs/arquitectura", "CLAUDE.md": "/docs/claude", "README.md": "/docs/readme"}; for (const [from, to] of Object.entries(linkMap)) { const re = new RegExp('href="(\\./)?' + from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '"', 'g'); html = html.replace(re, 'href="' + to + '"'); } // Bloques ```mermaid``` →
html = html.replace( /
([\s\S]*?)<\/code><\/pre>/g,
      (m, code) => {
        const decoded = code
          .replace(/</g, '<')
          .replace(/>/g, '>')
          .replace(/&/g, '&')
          .replace(/"/g, '"')
          .replace(/'/g, "'");
        return '
' + decoded + '
'; } ); document.getElementById('content').innerHTML = html; mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' }); mermaid.run();