MicroFish/docs/superpowers/specs/2026-04-26-persistencia-des...

17 KiB
Raw Blame History

Disseny: Capa de Persistència MiroFish

1. Context i motivació

MiroFish es desplega com a contenidor Docker. Sense volums muntats, tot el que es guarda al filesystem del contenidor (backend/uploads/) es perd en cada reinici o redeploy. A més:

  • TaskManager és 100% in-memory: cada reinici invalida les tasques en curs, impedint recovery de l'usuari
  • ProjectManager persisteix via JSON a disc: funcional en dev però no en contenidors efímers ni en multi-instància
  • No hi ha gestió d'usuaris: tots els projectes són globals i sense propietari
  • No hi ha configuració dinàmica: tots els paràmetres d'LLM, límits i features van hard-coded a variables d'entorn

Aquest disseny estableix la capa de persistència completa que farà l'aplicació viable en producció.


2. Principis de disseny

  1. Abstracció total del backend — les capes de negoci mai toquen SQL ni fitxers directament; ho fan a través de StorageService i sessions SQLAlchemy
  2. Un URL, dos entornsDATABASE_URL i STORAGE_TYPE seleccionen el backend; el codi de negoci no canvia
  3. Paths estables — els storage_path guardats a la BD no depenen del backend de storage; l'adaptador els resol
  4. Dades de simulació OASIS separades — els fitxers .db SQLite del motor OASIS es tracten com a artefactes i es guarden a storage, no a la BD de l'app
  5. Configuració dinàmica per admins — paràmetres modificables en runtime van a SystemConfig a la BD, no a variables d'entorn

3. Arquitectura general

┌─────────────────────────────────────────┐
│  Flask Routes                           │
├─────────────────────────────────────────┤
│  Services (ProjectService, AuthService, │
│  SimulationService, ReportService...)   │
│    → usen StorageService + DB Session   │
├──────────────────┬──────────────────────┤
│  StorageService  │  SQLAlchemy 2.x      │
│  (Protocol)      │  Session Factory     │
├────────┬─────────┼──────────────────────┤
│LocalFS │AzureBlob│  SQLite (dev)        │
│        │  (S3*)  │  PostgreSQL (prod)   │
└────────┴─────────┴──────────────────────┘

* S3: interfície dissenyada, implementació diferida

4. Model de dades (SQLAlchemy 2.x)

User

camp tipus notes
id UUID PK
email text unique
name text
password_hash text bcrypt
role enum: user|admin
status enum: pending|active|disabled
created_at datetime
updated_at datetime

Project

camp tipus notes
id UUID PK
user_id FK → User CASCADE delete
name text
status text created|ontology_generated|graph_building|graph_completed|failed
analysis_summary text nullable resum LLM del document
simulation_requirement text nullable pregunta de simulació
chunk_size int default 500
chunk_overlap int default 50
active_task_id FK → Task nullable ON DELETE SET NULL
created_at datetime
updated_at datetime

ProjectFile

camp tipus notes
id UUID PK
project_id FK → Project CASCADE delete
original_name text nom original del fitxer
storage_path text path relatiu a StorageService
size int bytes
mime_type text
file_type enum: upload|extracted_text
created_at datetime

Ontology

camp tipus notes
id UUID PK
project_id FK → Project CASCADE delete
version int default 1, incrementa en re-generació
entity_types JSON llista de {name, description, attributes, examples}
edge_types JSON llista de {name, source_targets, attributes}
created_at datetime

Graph

camp tipus notes
id UUID PK
project_id FK → Project CASCADE delete
ontology_id FK → Ontology quin esquema va usar
backend enum: zep|graphiti
external_id text ID a Zep Cloud o nom del graf Neo4j
status enum: building|ready|failed
node_count int nullable stats post-build
edge_count int nullable stats post-build
created_at datetime
updated_at datetime

Simulation

camp tipus notes
id UUID PK
project_id FK → Project CASCADE delete
graph_id FK → Graph quin graf usa
status text prepared|running|completed|failed
platform enum: twitter|reddit|both
config JSON time steps, events, agent params
profiles_path text nullable storage_path al JSON/CSV perfils
db_path text nullable storage_path al fitxer .db OASIS
actions_path text nullable storage_path al JSONL accions
rounds_total int nullable
rounds_completed int default 0
created_at datetime
updated_at datetime

Report

camp tipus notes
id UUID PK
project_id FK → Project CASCADE delete
simulation_id FK → Simulation
graph_id FK → Graph per fer queries Zep durant generació
status text generating|completed|failed
outline JSON nullable estructura de seccions
storage_prefix text prefix del directori a storage
created_at datetime
updated_at datetime

Task

camp tipus notes
id UUID PK
task_type text ontology_generate|graph_build|simulation_prepare|etc.
entity_type text project|simulation|report
entity_id text UUID de l'entitat relacionada
status enum: pending|processing|completed|failed
progress int 0100
message text nullable missatge de progrés
result JSON nullable resultat final
error text nullable error si failed
created_at datetime
updated_at datetime

SystemConfig

camp tipus notes
key text PK p.ex. llm.api_key
value text valor serialitzat
value_type enum: string|int|float|bool|json per deserialitzar
group enum: llm|limits|email|graph|features agrupació UI
label text text llegible
description text
is_secret bool emmascara el valor a la UI
updated_at datetime
updated_by FK → User nullable qui va fer el canvi

Claus predefinides per grup

group key secret
llm llm.api_key
llm llm.base_url no
llm llm.model_name no
llm llm.boost_model_name no
graph graph.backend no
graph graph.zep_api_key
limits limits.max_projects_per_user no
limits limits.max_file_size_mb no
limits limits.max_simulation_agents no
email email.sender_address no
email email.acs_endpoint no
email email.acs_key
features features.registration_open no

InvitationToken

camp tipus notes
token text PK UUID o token segur
user_id FK → User CASCADE delete
expires_at datetime TTL 48h
used_at datetime nullable null = no usat

PasswordResetToken

camp tipus notes
token text PK
user_id FK → User CASCADE delete
expires_at datetime TTL 1h
used_at datetime nullable

Relacions clau

User ──1:N──► Project ──1:N──► ProjectFile
                      ──1:N──► Ontology ──1:N──► Graph ──1:N──► Simulation ──1:N──► Report
                      ──1:N──► Task (via entity_id)
  • Project.active_task_id → FK → Task amb ON DELETE SET NULL
  • Eliminació User → CASCADE elimina tots els seus Projects i descendents
  • Eliminació Project → CASCADE elimina Files, Ontologies, Graphs, Simulations, Reports

5. StorageService (abstracció de fitxers)

Interfície Protocol (Python)

from typing import IO, Protocol

class StorageService(Protocol):
    def upload(self, path: str, data: bytes | IO, content_type: str) -> None: ...
    def download(self, path: str) -> bytes: ...
    def download_stream(self, path: str) -> IO: ...
    def delete(self, path: str) -> None: ...
    def delete_prefix(self, prefix: str) -> None: ...
    def exists(self, path: str) -> bool: ...
    def list(self, prefix: str) -> list[str]: ...
    def public_url(self, path: str) -> str | None: ...

Implementacions

STORAGE_TYPE Classe Dependència
local LocalFSStorage stdlib pathlib
azure AzureBlobStorage azure-storage-blob
s3 (disseny llest, no implementat) S3Storage boto3

La selecció es fa a l'inici de l'app via STORAGE_TYPE:

def create_storage_service() -> StorageService:
    match os.environ.get("STORAGE_TYPE", "local"):
        case "azure": return AzureBlobStorage(...)
        case "s3":    return S3Storage(...)       # futur
        case _:       return LocalFSStorage(...)

Convenció de paths

Els paths guardats a la BD són relatius i independents del backend:

projects/{project_id}/files/{filename}
projects/{project_id}/extracted_text.txt
simulations/{sim_id}/{platform}_simulation.db
simulations/{sim_id}/profiles.json
simulations/{sim_id}/twitter_profiles.csv
simulations/{sim_id}/actions.jsonl
reports/{report_id}/full_report.md
reports/{report_id}/section_{nn}.md
reports/{report_id}/agent_log.jsonl
reports/{report_id}/console_log.txt

En canviar de local a azure (o s3), els paths a la BD no canvien — només l'adaptador que els resol.


6. Autenticació i RBAC

Mecanisme

  • JWT via flask-jwt-extended
  • Access token: 8h, en cookie HTTP-only
  • Refresh token: 7 dies, en cookie HTTP-only
  • Cap token en localStorage (evita XSS)

Rols i accés

Un admin és també un user: té els seus propis projectes i usa l'aplicació igual que qualsevol usuari. Les capacitats addicionals d'administració (veure tots els projectes, gestió d'usuaris, SystemConfig) estan en pantalles separades, accessibles des d'un menú d'administració.

Rol Accés aplicació Accés administració
user Els seus propis projectes
admin Els seus propis projectes Tots els projectes + Gestió d'usuaris + SystemConfig

Decoradors Flask:

@require_auth          # qualsevol usuari autenticat
@require_admin         # només admins
@require_project_owner # propietari del projecte o admin

Auth provisional a eliminar

Actualment existeix un sistema d'autenticació provisional (backend/app/api/auth.py) basat en un usuari únic demo amb DEMO_PASSWORD com a variable d'entorn i JWT HS256 de 24h. Cal eliminar-lo completament i substituir-lo pel sistema descrit en aquesta especificació.

Flux d'invitació

Admin crea usuari (status: pending)
  → email amb InvitationToken (TTL 48h)
  → usuari clica l'enllaç, defineix contrasenya
  → status: active

Flux "He oblidat la contrasenya"

Usuari introdueix el seu email al formulari
  → resposta sempre idèntica: "Si l'adreça existeix, rebràs un correu en breus"
     (no revelar si l'email és vàlid o no)
  → si l'email existeix a la BD: enviar PasswordResetToken (TTL 1h)
  → usuari clica l'enllaç, defineix nova contrasenya
  → token marcat com a used_at

Principi de seguretat: cap resposta de l'API d'autenticació ha de revelar si un email existeix o no al sistema. Tant el login fallit com el forgot-password han de retornar missatges genèrics.

Endpoints d'autenticació

POST /api/auth/login             → no revelar si email/password és incorrecte per separat
POST /api/auth/refresh
POST /api/auth/logout
POST /api/auth/set-password      (invitació + primer login)
POST /api/auth/forgot-password   → resposta genèrica sempre
POST /api/auth/reset-password

Look & feel

La interfície d'autenticació i administració seguirà el Sistema de Disseny de la Generalitat de Catalunya (Gencat), consistent amb el disseny existent a dev/AgentCAT. Principis aplicables:

  • Accessibilitat per defecte (teclat, focus visible, labels explícits)
  • Inputs amb ajuda contextual i missatges d'error associats (però sense revelar info sensible)
  • Estados completament modelats: loading, error, confirmació
  • Patrons previsibles i feedback immediat

7. SystemConfig: resolució de valors

Prioritat descendent (el primer que es trobi guanya):

  1. Variable d'entorn — sobreescriu tot; útil per secrets en CI/CD
  2. SystemConfig a la BD — configurable per admins via UI en runtime
  3. Valor per defecte al codi — fallback si no hi ha cap dels anteriors
def get_config(key: str, default=None):
    env_key = key.replace(".", "_").upper()  # llm.api_key → LLM_API_KEY
    if val := os.environ.get(env_key):
        return val
    if row := db.query(SystemConfig).get(key):
        return row.typed_value
    return default

8. Variables d'entorn (infraestructura)

Les variables d'entorn cobreixen exclusivament la infraestructura (no modificables en runtime):

# Base de dades
DATABASE_URL=sqlite:///dev.db
# DATABASE_URL=postgresql://user:pass@host:5432/mirofish

# Storage
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=./uploads
# STORAGE_TYPE=azure
AZURE_STORAGE_CONNECTION_STRING=...
AZURE_STORAGE_CONTAINER=mirofish

# Autenticació
JWT_SECRET=...
JWT_REFRESH_SECRET=...

# Entorn
ENVIRONMENT=development
# ENVIRONMENT=production

Paràmetres com LLM_API_KEY, LLM_MODEL_NAME, ZEP_API_KEY, límits i features van a SystemConfig (amb fallback a variable d'entorn per compatibilitat).


9. Fases d'implementació

Fase 1 — Infraestructura base

Objectiu: BD + storage funcionals, ProjectManager i TaskManager migrats.

  • Configurar SQLAlchemy 2.x + Alembic (migració inicial)
  • Crear tots els models SQLAlchemy
  • Implementar StorageService + LocalFSStorage + AzureBlobStorage
  • Migrar ProjectManager (JSON → BD)
  • Migrar TaskManager (memòria → BD)
  • Injectar StorageService i sessió DB a Flask via app factory

Verificació: crear projecte, pujar fitxer, crear task → reiniciar servidor → verificar que tot sobreviu


Fase 2 — Autenticació i multi-usuari

Objectiu: login, RBAC i aïllament de dades per usuari.

  • Models User, InvitationToken, PasswordResetToken
  • JWT via flask-jwt-extended (cookies HTTP-only)
  • Decoradors @require_auth, @require_admin, @require_project_owner
  • Flux invitació + reset contrasenya
  • Totes les rutes de Project filtrades per user_id

Verificació: login, accés projecte propi, bloqueig projecte d'altri, flux invitació complet


Fase 3 — Migració pipeline complet

Objectiu: les 5 etapes del pipeline usen BD + storage.

  • GraphBuilder → desa Ontology + Graph a BD
  • SimulationRunner → desa .db OASIS a StorageService, metadades a Simulation
  • ReportAgent → desa seccions a StorageService, metadades a Report
  • SystemConfig operatiu (LLM keys, Zep key, límits via BD)

Verificació: pipeline end-to-end (upload → ontologia → graf → simulació → report) amb BD + storage


Fase 4 — Hardening producció

Objectiu: desplegament estable en contenidor amb PostgreSQL + Azure Blob.

  • Configurar PostgreSQL + connection pool (SQLAlchemy pool_size, max_overflow)
  • Configurar Azure Blob Storage
  • .env.dev i .env.prod per entorn
  • Health check endpoint (GET /api/health) que verifica BD + storage
  • Smoke tests en contenidor Docker

Verificació: docker build + docker run sense volums → pipeline complet → reinici contenidor → dades persistides


10. Verificació end-to-end

1. Arrancar en mode dev (SQLite + LocalFS)
2. Crear projecte → verificar fila a BD + fitxer a uploads/
3. Executar pipeline complet → verificar Ontology, Graph, Simulation, Report a BD
4. Reiniciar servidor → verificar que projecte, tasks i fitxers sobreviuen
5. Canviar STORAGE_TYPE=azure → verificar que paths a BD no canvien
6. Canviar DATABASE_URL=postgresql://... + alembic upgrade head → verificar sense errors
7. Reiniciar contenidor sense volum → verificar dades persistides a BD + Azure Blob