Files
hechakuaa_bot/PROMPT_CLAUDE_CODE_SPRINT4.md
markosbenitez 9c60972bcd prompt sprint 4
2026-05-28 01:12:25 -03:00

9.4 KiB

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.

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

"""
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:

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:

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:

ssh -L 8080:127.0.0.1:8080 pedrito@<IP_VPS>
# 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