prompt sprint 4
This commit is contained in:
8
.claude/settings.json
Normal file
8
.claude/settings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit -m ' *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
319
PROMPT_CLAUDE_CODE_SPRINT4.md
Normal file
319
PROMPT_CLAUDE_CODE_SPRINT4.md
Normal file
@@ -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@<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
|
||||
Reference in New Issue
Block a user