From 9c60972bcd94fb0224b94a6965aed4e1f03b9a7e Mon Sep 17 00:00:00 2001 From: markosbenitez Date: Thu, 28 May 2026 01:12:25 -0300 Subject: [PATCH] prompt sprint 4 --- .claude/settings.json | 8 + PROMPT_CLAUDE_CODE_SPRINT4.md | 319 ++++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 .claude/settings.json create mode 100644 PROMPT_CLAUDE_CODE_SPRINT4.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e9285fe --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(git add *)", + "Bash(git commit -m ' *)" + ] + } +} diff --git a/PROMPT_CLAUDE_CODE_SPRINT4.md b/PROMPT_CLAUDE_CODE_SPRINT4.md new file mode 100644 index 0000000..c8de2a9 --- /dev/null +++ b/PROMPT_CLAUDE_CODE_SPRINT4.md @@ -0,0 +1,319 @@ +# Prompt para Claude Code — Sprint 4: Features 3, 5 y 1 + +Lee `CLAUDE.md` antes de empezar. Las invariantes de seguridad aplican. +Implementar las tres features en orden: primero F5 (más simple), luego F3, luego F1. + +--- + +## Feature 5 — Frases libres → comandos (sin LLM) + +### Contexto +El bot hoy responde a texto libre con "no entendí eso, mirá /ayuda". +Hay frases obvias que debería mapear a comandos existentes sin necesidad de LLM. + +### Implementación +En `telegram_bot.py`, modificar `handle_texto_libre()` para que antes de +responder "no entendí", intente mapear el texto a un comando conocido. + +```python +FRASES_COMANDOS = { + # Notificaciones + "novedades": "notif", + "actualizaciones": "notif", + "que hay nuevo": "notif", + "qué hay nuevo": "notif", + "hay algo nuevo": "notif", + "alguna novedad": "notif", + "notificaciones": "notif", + "notificacion": "notif", + "notificación": "notif", + "revisar": "notif", + "actualizar": "notif", + # Estado + "estado": "estado", + "como estas": "estado", + "cómo estás": "estado", + # Ayuda + "ayuda": "ayuda", + "help": "ayuda", + "que podes hacer": "ayuda", + "qué podés hacer": "ayuda", + "comandos": "ayuda", +} +``` + +Lógica de matching: normalizar el texto del usuario (lowercase, sin acentos, +sin signos de puntuación) y buscar si alguna frase del diccionario está +contenida en el texto normalizado. + +Si hay match → ejecutar el handler correspondiente directamente. +Si no hay match → respuesta actual ("no entendí eso..."). + +Casos especiales a detectar por regex: +- "expediente 198/2026" o "exp 198" → ejecutar cmd_exp con ese argumento +- "pdf 198/2026" → ejecutar cmd_pdf con ese argumento + +### Verificación +- [ ] "novedades" ejecuta lo mismo que /notif +- [ ] "qué hay nuevo?" ejecuta lo mismo que /notif +- [ ] "expediente 198/2026" ejecuta lo mismo que /exp 198/2026 +- [ ] Texto completamente irrelevante ("hola qué tal") sigue respondiendo con el mensaje de ayuda +- [ ] No interfiere con el flujo de onboarding + +--- + +## Feature 3 — Extracto de PDF sin LLM + +### Contexto +Cuando llega una notificación con documento adjunto, el abogado quiere saber +qué dice sin tener que descargar y abrir el PDF. La solución es extraer el +texto del PDF con `pypdf` y mostrar las primeras líneas como extracto. + +Sin LLM. Sin interpretación. Texto crudo del documento con disclaimer. + +### Nueva dependencia +Agregar a `pyproject.toml`: +``` +"pypdf>=4.0", +``` + +### Nuevo módulo `pdf_extractor.py` + +```python +""" +pdf_extractor.py — Extracción de texto de PDFs judiciales. + +Sin LLM. Sin interpretación. Solo extracción de texto crudo. +El abogado lee el extracto y decide. Pedrito no interpreta. + +Los PDFs nunca se guardan en disco — se procesan en memoria. +""" + +from pypdf import PdfReader +import io + +MAX_CHARS = 600 # máximo de caracteres a mostrar + +def extraer_extracto(pdf_bytes: bytes) -> str | None: + """ + Extrae las primeras líneas de texto de un PDF judicial. + + Retorna el extracto como string, o None si el PDF no tiene texto + extraíble (PDF escaneado, imagen, etc.) + + Los PDFs del PJ son nativos (no escaneados), así que pypdf funciona. + """ + try: + reader = PdfReader(io.BytesIO(pdf_bytes)) + texto = "" + for page in reader.pages: + texto += page.extract_text() or "" + if len(texto) >= MAX_CHARS: + break + + texto = texto.strip() + if not texto: + return None + + # Limpiar saltos de línea múltiples + import re + texto = re.sub(r'\n{3,}', '\n\n', texto) + + if len(texto) > MAX_CHARS: + texto = texto[:MAX_CHARS] + "..." + + return texto + except Exception: + return None +``` + +### Integración en el flujo de notificaciones + +En `poller.py`, método `revisar_ahora()`, cuando se detecta una notificación +nueva y se va a enviar al abogado: + +1. Intentar descargar el PDF (ya existe `descargar_documento_principal()`) +2. Si descarga OK → extraer extracto con `extraer_extracto()` +3. Enviar notificación con extracto incluido +4. Si descarga falla o PDF sin texto → enviar notificación sin extracto (degradación elegante) + +### Formato del mensaje con extracto + +``` +📋 Nueva notificación judicial + +{caratula} +Exp. {numero}/{anio} — {juzgado} + +Tipo: {tipo} +Fecha: {fecha} + +📄 Extracto del documento: +{extracto} + +⚠️ Este es el texto crudo del documento. Verificá +el original antes de actuar. + +[Ver PDF] [Ir al sistema del PJ] +``` + +Si no hay extracto disponible, el mensaje es igual pero sin la sección +"📄 Extracto del documento". + +### En `cmd_pdf()` de `telegram_bot.py` + +Cuando el abogado pide `/pdf 198/2026`, después de enviar el archivo también +enviar el extracto como mensaje de texto separado: + +``` +📄 Contenido del documento: +{extracto} + +⚠️ Verificá el original antes de actuar. +``` + +### Verificación +- [ ] Una notificación nueva llega con el extracto incluido +- [ ] Si el PDF no tiene texto extraíble, la notificación llega igual sin extracto +- [ ] El extracto no supera 600 caracteres +- [ ] El disclaimer siempre aparece cuando hay extracto +- [ ] Los bytes del PDF nunca se escriben a disco +- [ ] `scrub()` en cualquier log que mencione el contenido del extracto +- [ ] Agregar `pypdf` a `pyproject.toml` + +--- + +## Feature 1 — Backend de administración web (FastAPI + HTML puro) + +### Contexto +Panel de administración interno para gestionar tenants y ver actividad. +Solo para el admin (Marcos). No es un producto — es una herramienta operativa. +HTML puro servido por FastAPI. Sin framework frontend. Funcional sobre bonito. + +### Nueva dependencia +Agregar a `pyproject.toml`: +``` +"fastapi>=0.115", +"uvicorn>=0.32", +"jinja2>=3.1", +``` + +### Estructura de archivos nuevos + +``` +admin/ + __init__.py + app.py ← FastAPI app del panel + auth.py ← autenticación básica (token en .env) + templates/ + base.html ← layout común + tenants.html ← listado de tenants + tenant.html ← detalle de un tenant + audit.html ← audit log +``` + +### Autenticación del panel + +Token estático en `.env`: +``` +ADMIN_TOKEN=genera-uno-con-secrets.token_hex(32) +``` + +Middleware simple: si el request no tiene el header `X-Admin-Token` con el +valor correcto → 403. No hace falta sesiones ni cookies para este sprint. + +Agregar a `Settings` en `config.py`: +```python +admin_token: str = "" +admin_port: int = 8080 +``` + +### Rutas del panel + +``` +GET / → redirect a /tenants +GET /tenants → listado de todos los tenants +GET /tenants/{id} → detalle de un tenant +POST /tenants/{id}/resetear → resetea el tenant (borra DB + snapshot) +GET /audit → audit log, últimas 200 entradas, filtrable por chat_id +GET /audit?chat_id=X → audit log filtrado por tenant +``` + +### Página /tenants — tabla con columnas + +| chat_id | nombre | estado | tono | timezone | cédula (primeros 3 dígitos + ***) | último chequeo | acciones | +|---------|--------|--------|------|----------|-----------------------------------|----------------|---------| + +La cédula se muestra parcialmente — nunca en texto plano. +"Acciones" tiene un botón "Resetear" que hace POST a `/tenants/{id}/resetear`. + +### Página /tenants/{id} — detalle + +- Todos los campos del tenant +- Últimas 20 entradas del audit_log para ese chat_id +- Botón resetear + +### Página /audit — tabla con columnas + +| timestamp (hora local) | chat_id | nombre tenant | evento | resultado | detalle (JSON colapsado) | + +Filtro por chat_id con un input de texto simple. +Paginación básica: 200 registros por página, botón "más antiguos". + +### Arrancar el panel junto con el bot + +En `main.py`, arrancar el servidor de administración en un thread separado: + +```python +import uvicorn +import threading + +def run_admin(): + uvicorn.run("admin.app:app", host="127.0.0.1", port=settings.admin_port, log_level="warning") + +admin_thread = threading.Thread(target=run_admin, daemon=True) +admin_thread.start() +``` + +El panel corre en `127.0.0.1:8080` — solo accesible via SSH tunnel, no expuesto públicamente. + +Para acceder desde tu Mac: +```bash +ssh -L 8080:127.0.0.1:8080 pedrito@ +# Luego abrir http://localhost:8080 en el browser +``` + +### Seguridad del panel + +- Solo escucha en 127.0.0.1 (no en 0.0.0.0) — inaccesible desde internet +- Requiere el token en cada request +- Las cédulas nunca se muestran completas +- Los bearer tokens del PJ nunca aparecen en el panel +- El panel es de solo lectura excepto la acción "resetear" + +### Verificación +- [ ] `GET /tenants` muestra los tenants registrados con cédula parcial +- [ ] `POST /tenants/{id}/resetear` borra el tenant y su snapshot +- [ ] `GET /audit` muestra las últimas 200 entradas +- [ ] `GET /audit?chat_id=X` filtra correctamente +- [ ] Sin `ADMIN_TOKEN` en el request → 403 +- [ ] El panel NO es accesible desde internet (solo 127.0.0.1) +- [ ] El panel arranca junto con el bot sin errores +- [ ] Agregar `fastapi`, `uvicorn`, `jinja2` a `pyproject.toml` + +--- + +## Orden de implementación sugerido + +1. Feature 5 (frases libres) — 2-3 horas, sin dependencias nuevas +2. Feature 3 (extracto PDF) — 3-4 horas, una dependencia nueva +3. Feature 1 (panel admin) — 4-6 horas, más compleja + +## Lo que NO hacer en este sprint + +- No usar LLM para nada — ni resumen, ni clasificación, ni mapeo de intenciones +- No exponer el panel en un puerto público +- No guardar PDFs en disco +- No mostrar cédulas completas en el panel +- No cambiar el onboarding +- No cambiar el flujo de notificaciones existente más allá de agregar el extracto \ No newline at end of file