243 lines
11 KiB
Markdown
243 lines
11 KiB
Markdown
# Gestione delle memory
|
|
|
|
Questo documento descrive l'organizzazione attuale delle memory nel workflow ThothII: modello concettuale, ciclo di vita, persistenza, ricerca semantica, gate di revisione, visualizzazione delle sessioni e principali limiti tecnici.
|
|
|
|
## Sintesi architetturale
|
|
|
|
Una memory è conoscenza di dominio riutilizzabile tra domande. Non è una copia dello schema-linking di una singola domanda.
|
|
|
|
```text
|
|
F1: chiarimento di un concetto
|
|
│
|
|
▼
|
|
decisione concept_clarified nel ledger della sessione
|
|
│
|
|
▼
|
|
F8: il reviewer decide se promuoverla
|
|
│
|
|
├── registro globale registry.jsonl
|
|
└── indice semantico Qdrant
|
|
│
|
|
▼
|
|
F2 di una sessione futura
|
|
ricerca e proposta al reviewer
|
|
```
|
|
|
|
L'invariante principale è `REUSABLE_TYPES = {"concept_clarified"}`: le sole memory generabili, salvabili, ricercabili e proponibili sono i concetti chiariti. Le decisioni `table_promoted`, `table_excluded`, `column_promoted` e analoghe restano decisioni locali alla domanda.
|
|
|
|
Implementazione principale: la façade [harness/tht/memory/](../harness/tht/memory/),
|
|
con le policy riusabili in
|
|
[harness/tht/memory/core.py](../harness/tht/memory/core.py), e l'adapter
|
|
[harness/tht/cli/memory_cmd.py](../harness/tht/cli/memory_cmd.py).
|
|
|
|
## I tre livelli della gestione
|
|
|
|
| Livello | Contenuto | Funzione |
|
|
| --- | --- | --- |
|
|
| Ledger della sessione | `concept_clarified`, `memory_promoted`, `memory_promotion_declined` | Audit e stato della singola sessione |
|
|
| Registro globale | Record `mem-XXXX` in `registry.jsonl` | Archivio canonico attuale delle memory |
|
|
| Indice Qdrant | Embedding e metadati derivati dal registro | Ricerca semantica |
|
|
|
|
Il ledger contiene la provenienza e le decisioni umane. Il record globale contiene il testo riutilizzabile. L'indice vettoriale è una proiezione per la ricerca, non il posto in cui il workflow registra direttamente le decisioni.
|
|
|
|
## Che cosa può diventare una memory
|
|
|
|
Durante F1 il workflow registra i chiarimenti come decisioni `concept_clarified`. Un chiarimento può esprimere:
|
|
|
|
- definizioni di concetti clinici o organizzativi;
|
|
- criteri di inclusione ed esclusione di una popolazione;
|
|
- formule e metodi di calcolo;
|
|
- interpretazioni temporali;
|
|
- mapping verso tabelle e colonne specifiche;
|
|
- significato di flag, codici o indicatori.
|
|
|
|
Il modello `MemoryRecord` contiene:
|
|
|
|
- `id`, ad esempio `mem-0001`;
|
|
- timestamp, sessione e sequenza della decisione originale;
|
|
- `type`;
|
|
- `subject`;
|
|
- `detail`;
|
|
- `rationale`;
|
|
- `question_context`;
|
|
- `tables` e `concepts`.
|
|
|
|
Per le nuove memory il tipo è sempre `concept_clarified` e `tables` viene inizializzato vuoto. Una tabella o una colonna può essere citata dentro la spiegazione come mapping tecnico; non può essere il concetto autonomo della memory.
|
|
|
|
Esempio valido:
|
|
|
|
> Per ablazione si intende una procedura con `ablazione_transcatetere = TRUE`, conteggiata con `COUNT(DISTINCT cod_paz)` per anno.
|
|
|
|
Esempi non validi:
|
|
|
|
- `fact_cardioversione` come memory approvata;
|
|
- `dim_time` come memory rifiutata;
|
|
- una decisione “includi questa tabella” salvata per domande future.
|
|
|
|
La regola è documentata anche nella skill del workflow, in [harness/.pi/skills/tht-sessione/SKILL.md](../harness/.pi/skills/tht-sessione/SKILL.md:217).
|
|
|
|
## Promozione alla fine della sessione: F8
|
|
|
|
Alla fine del workflow il gate `reviewer_memory_promote` esegue una preview deterministica:
|
|
|
|
```text
|
|
tht memory promote --session <id> --preview --json
|
|
```
|
|
|
|
La preview:
|
|
|
|
1. legge le decisioni effettive della sessione;
|
|
2. considera solo `concept_clarified`;
|
|
3. scarta le decisioni già promosse;
|
|
4. scarta le sequenze già rifiutate in F8;
|
|
5. deduplica contenuti equivalenti;
|
|
6. propone al massimo cinque candidati.
|
|
|
|
Il codice applica il filtro e la deduplica in
|
|
[harness/tht/memory/core.py](../harness/tht/memory/core.py); il gate applica un
|
|
ulteriore filtro difensivo in
|
|
[harness/.pi/extensions/gate/memory/index.js](../harness/.pi/extensions/gate/memory/index.js).
|
|
|
|
Il reviewer vede un'unica checklist, preselezionata. Per ogni candidato:
|
|
|
|
- selezionato: viene eseguito `tht memory save-one` e poi viene registrato `memory_promoted`;
|
|
- deselezionato: viene registrato `memory_promotion_declined`;
|
|
- nessun candidato: F8 si chiude automaticamente.
|
|
|
|
Il marker `memory_promoted` usa `detail: seq:N`, cioè un riferimento alla decisione `concept_clarified` originale. Il flusso è in [harness/.pi/extensions/tht-gate.js](../harness/.pi/extensions/tht-gate.js:1691).
|
|
|
|
La promozione non viene eseguita in F2 e il modello non può inventare candidati F8. I comandi diretti di promozione sono inoltre protetti dal gate anti-bypass.
|
|
|
|
## Persistenza globale
|
|
|
|
### Registro JSONL
|
|
|
|
Il registro attuale è:
|
|
|
|
```text
|
|
<artifacts>/memory/registry.jsonl
|
|
```
|
|
|
|
La scrittura viene fatta tramite file temporaneo e `os.replace`, quindi la sostituzione del registro è atomica. L'idempotenza della promozione è basata sulla coppia `session_id + decision_seq`: la stessa decisione della stessa sessione non genera due record globali.
|
|
|
|
### Qdrant
|
|
|
|
Dopo la promozione, `save-one` costruisce un solo `VectorRecord` e lo invia all'indice Qdrant. Il testo indicizzato include:
|
|
|
|
- tipo e soggetto;
|
|
- dettaglio;
|
|
- motivazione;
|
|
- domanda di contesto;
|
|
- eventuali concetti e mapping.
|
|
|
|
Il record vettoriale usa l'id `memory:mem-XXXX`, mentre i metadati conservano `subject`, `detail`, `rationale`, `tables`, `concepts` e il discriminante `kind`. L'hash SHA-256 del contenuto impedisce di ricalcolare embedding e upsert quando il testo non è cambiato.
|
|
|
|
Il comportamento è implementato in
|
|
[harness/tht/memory/core.py](../harness/tht/memory/core.py).
|
|
|
|
### Fonte canonica attuale
|
|
|
|
Oggi il registro JSONL è ancora la fonte canonica applicativa e Qdrant resta un indice derivato ma persistente. Il workflow non registra direttamente le decisioni nel vector DB: usa Qdrant come proiezione interrogabile del registro e del ledger effettivo.
|
|
|
|
## Riutilizzo in F2
|
|
|
|
In una sessione futura F2 esegue:
|
|
|
|
```text
|
|
tht memory search "<domanda>" --session <id> --json
|
|
```
|
|
|
|
Il comando:
|
|
|
|
1. crea l'embedding della domanda;
|
|
2. cerca nel vector store solo record `kind=memory`;
|
|
3. risolve ogni hit nel registro JSONL tramite il suo `ref`;
|
|
4. scarta record assenti dal registro;
|
|
5. scarta qualsiasi tipo diverso da `concept_clarified`;
|
|
6. esclude le memory già decise nella sessione corrente;
|
|
7. restituisce i risultati ordinati per similarità.
|
|
|
|
L'implementazione è in [harness/tht/cli/memory_cmd.py](../harness/tht/cli/memory_cmd.py:368).
|
|
|
|
Le memory non vengono mai applicate automaticamente. Il modello deve presentarle in un'unica scelta `reviewer_decide`:
|
|
|
|
- una memory selezionata viene registrata come nuovo `concept_clarified` nella sessione corrente;
|
|
- il rationale deve citare l'id originale `mem-XXXX`;
|
|
- una memory deselezionata significa “non applicarla ora”, non “cancellarla globalmente”.
|
|
|
|
Se F2 viene riaperta, una memory deselezionata può quindi essere proposta di nuovo. `memory_rejected` resta supportato per decisioni e sessioni legacy, ma non rappresenta il normale comportamento della deselezione F2 attuale.
|
|
|
|
## Ledger effettivo, rollback e riaperture
|
|
|
|
Il ledger è append-only. Riaperture e ritrattazioni non cancellano le righe precedenti; cambiano però quali decisioni sono effettive.
|
|
|
|
Gli helper memory usano `effective_decisions()` per:
|
|
|
|
- escludere decisioni ritirate;
|
|
- ignorare decisioni appartenenti a fasi diventate stale dopo un rollback;
|
|
- impedire la promozione di chiarimenti non più validi.
|
|
|
|
La vista effettiva è definita in [harness/tht/phase.py](../harness/tht/phase.py:82).
|
|
|
|
## Visualizzazione nel riepilogo della sessione
|
|
|
|
La sezione “Memories” del riepilogo viene proiettata a runtime dal ledger della sessione; non è una copia diretta del registro globale.
|
|
|
|
La proiezione:
|
|
|
|
- mostra prima le memory approved;
|
|
- mostra poi le memory declined;
|
|
- risolve `seq:N` verso il `concept_clarified` originale;
|
|
- nasconde marker il cui record originale non è `concept_clarified`;
|
|
- nasconde soggetti che corrispondono a tabelle dello schema-linking;
|
|
- nasconde soggetti autonomi con forma `fact_*` o `dim_*`;
|
|
- mantiene invece i riferimenti a tabelle e campi quando fanno parte della spiegazione concettuale.
|
|
|
|
La logica è in [harness/tht/session/store.py](../harness/tht/session/store.py:237). Il rendering frontend usa una lista strutturata e tratta `subject`, `detail` e `rationale` come Markdown, evitando di mostrare il markdown grezzo.
|
|
|
|
La trasformazione avviene in lettura: anche le sessioni storiche vengono organizzate con il layout e i filtri correnti senza riscrivere gli artefatti originali.
|
|
|
|
## Invarianti applicate
|
|
|
|
Le protezioni sono distribuite su più confini:
|
|
|
|
1. `REUSABLE_TYPES` nel core Python;
|
|
2. filtro del comando `memory search`;
|
|
3. filtro della preview F8;
|
|
4. filtro e deduplica nel gate Pi;
|
|
5. esclusione delle table-memory nella proiezione UI.
|
|
|
|
Questo evita che una singola modifica al prompt o a un solo componente reintroduca le tabelle come memory.
|
|
|
|
## Limiti e rischi residui
|
|
|
|
### Registro e indice non sono una singola transazione
|
|
|
|
Il salvataggio segue sostanzialmente questa sequenza:
|
|
|
|
```text
|
|
registro JSONL → Qdrant → marker memory_promoted nel ledger
|
|
```
|
|
|
|
Se Qdrant non è disponibile, il registro può contenere una memory non ancora ricercabile; il comando segnala che sarà necessario reindicizzare.
|
|
|
|
Se il marker del ledger fallisce dopo il salvataggio nel vector DB, la memory può risultare globalmente presente ma senza audit completo nella sessione. Il gate restituisce un comando di recupero manuale.
|
|
|
|
### Limite di cinque candidati
|
|
|
|
F8 propone al massimo cinque memory. Se una sessione produce più di cinque concetti validi, gli elementi eccedenti non vengono mostrati e la sessione può essere finalizzata senza promuoverli.
|
|
|
|
### Deduplica non globale
|
|
|
|
La deduplica impedisce duplicati nella stessa proposta e l'idempotenza impedisce di ripromuovere la stessa decisione. Non esiste però una fusione globale di due memory semanticamente simili provenienti da sessioni diverse.
|
|
|
|
### Vecchi record fisici
|
|
|
|
Vecchi record `table_promoted` o `table_excluded` possono ancora esistere in artefatti o indici storici. Il codice attuale li rende non riutilizzabili filtrandoli per tipo e non li mostra nella proiezione delle sessioni. La loro eventuale rimozione fisica dal vector DB resta un'attività di bonifica separata.
|
|
|
|
## Valutazione finale
|
|
|
|
La gestione attuale è coerente con il requisito funzionale: una memory è una conoscenza concettuale riutilizzabile, non una scelta di schema-linking.
|
|
|
|
La parte più solida è la difesa multilivello del tipo `concept_clarified`. Il principale debito tecnico riguarda invece la convivenza del registro JSONL con Qdrant e l'assenza di una transazione unica tra archivio globale, indice semantico e ledger della sessione.
|