HippoAgent — entity-first recall (hippo_entity_get)
Quando l'utente chiede "che sai di X?" / "ricordami chi è Y?" /
"dammi tutto su Z", chiama hippo_entity_get(name=<X>) PRIMA di
rispondere da memoria interna. Il tool ritorna l'entity canonica
(risolvendo eventuali alias) + la lista di fact collegati, da un
knowledge graph SQLite persistente.
⚠️ Quando ATTIVARE (trigger)
Pattern lessicali che attivano il skill:
- Italiano: "che sai di X", "cosa ricordi di Y", "dammi tutto
su Z", "chi è W", "ricostruisci ciò che sappiamo di V",
"abbiamo discusso di U", "informazioni su T", "storia di S".
- English: "what do you know about X", "tell me about Y",
"facts about Z", "who is W", "history of V", "what have we said
about U", "summarize what we know on T".
- Implicito: l'utente menziona un nome Capitalized (persona,
paper, repo, progetto interno) e ti aspetti che ne sappia qualcosa.
Risoluzione automatica alias: lookup di "S. Tonegawa" trova
l'entity canonica "Tonegawa" se è stato registrato l'alias.
Case + Unicode-safe: "müller", "MÜLLER", "Müller" trovano
tutti la stessa entity. Anche NFC vs NFD (unicodedata.normalize
forma decomposta vs composta) sono trattati come uguali.
NON serve per:
- Claim factual da validare ("X is Y") →
hippo_validate_claim
- Ricerca su episodi/dialoghi su un topic →
hippo_search,
hippo_recall
- Ricerca su fact per topic-namespace →
hippo_facts_search,
hippo_facts_list
- Saluti, chat casuale, opinioni
Flow
1. Identifica il nome dell'entity nella domanda (Capitalized,
eventualmente con prefisso "S." / "Dr." / titolo).
2. hippo_entity_get(name="<entity>")
3. Read payload:
{
"entity": { "id": "...", "canonical_name": "...", "type": "..." }
| None,
"aliases": ["...", "..."],
"facts": ["fact_id_1", "fact_id_2", ...]
}
4. Act:
┌─ entity is None → "Non ho memoria di <name> nel KG. Vuoi
│ che cerchi episodi correlati?
│ (`hippo_search`)"
├─ entity, facts == [] → "Ho il nodo <canonical_name> (id=...,
│ type=...) ma nessun fatto direttamente
│ linkato. Posso cercare con
│ `hippo_facts_search` o `hippo_recall`."
└─ entity, facts ≠ [] → Leggi i fact con `hippo_facts_list`
(passando i fact_id ottenuti) per
ricostruire la conoscenza completa,
poi rispondi citando i fact_id come
evidenza.
Output semantics
| campo |
tipo |
semantica |
entity |
dict | None |
None = entity sconosciuta. Dict = {id, canonical_name, type} |
entity.id |
str |
UUID 12-char, primary key SQLite, usalo per hippo_entity_link (futuro P2.a addendum) |
entity.canonical_name |
str |
Nome canonico (la forma "ufficiale", es. "Tonegawa") |
entity.type |
str |
Tag libero, es. "person", "paper", "repo", "project", "concept" — può essere "" |
aliases |
list[str] |
Tutte le forme alternative registrate (es. ["S. Tonegawa", "Susumu Tonegawa"]) |
facts |
list[str] |
fact_id collegati all'entity tramite tabella entity_facts. Vuota se nessun link |
Parametri
name: str — il nome o alias da cercare. Trim + Unicode NFC +
case-fold automatici dal backend.
Esempi concreti
Esempio 1 — entity trovata con facts
User: "cosa ricordi di Tonegawa?"
hippo_entity_get(name="Tonegawa")
→ {
"entity": {
"id": "a8f3c9d12e4b",
"canonical_name": "Tonegawa",
"type": "person"
},
"aliases": ["S. Tonegawa", "Susumu Tonegawa"],
"facts": ["f_tonegawa_nobel_1987", "f_tonegawa_engram_2014"]
}
Azione: chiama hippo_facts_list(fact_ids=["f_tonegawa_nobel_1987", "f_tonegawa_engram_2014"]) per leggere le proposizioni complete,
poi rispondi:
Tonegawa (Susumu Tonegawa, person) — 2 fact in memoria:
- Nobel Prize 1987 per immunology (V(D)J recombination)
[f_tonegawa_nobel_1987]
- Lavori sull'engram a MIT 2014 [f_tonegawa_engram_2014]
Alias noti: "S. Tonegawa", "Susumu Tonegawa".
Esempio 2 — alias risolve a canonical
User: "dammi tutto su S. Tonegawa"
hippo_entity_get(name="S. Tonegawa")
→ {
"entity": {"id": "a8f3c9d12e4b", "canonical_name": "Tonegawa", ...},
"aliases": [...],
"facts": [...]
}
L'alias è risolto trasparentemente: stesso id dell'entity canonica.
Esempio 3 — entity sconosciuta
User: "che sai di Marie Curie?"
hippo_entity_get(name="Marie Curie")
→ {"entity": None, "aliases": [], "facts": []}
Risposta corretta (onesta):
Non ho memoria di Marie Curie nel knowledge graph entity-centric.
Posso cercare episodi correlati con hippo_search("Marie Curie")
o fatti con hippo_facts_search?
NON CONFABULARE una risposta basata su training data — l'utente
ha chiesto cosa c'è in memoria persistente, non cosa sa il modello.
Esempio 4 — entity nota, zero fact linkati
User: "abbiamo discusso di NeuroFlask?"
hippo_entity_get(name="NeuroFlask")
→ {
"entity": {"id": "...", "canonical_name": "NeuroFlask", "type": "repo"},
"aliases": [],
"facts": []
}
Risposta:
Il nodo entity "NeuroFlask" esiste nel KG (type=repo) ma non ha
fatti direttamente linkati. Probabilmente è stato menzionato in
episodi senza che facts/triple siano state estratte (l'auto-
extraction OpenIE è P2.c, non ancora attiva). Vuoi che cerchi negli
episodi con hippo_search?
Esempio 5 — Unicode case-fold + NFC
User: "che sai di müller?" (lowercase, NFC)
User store-side: hippo_entity_get(name="MÜLLER") o
name="Müller" o anche forma NFD u+combining-diaeresis →
tutti risolvono allo stesso id. Backend usa unicodedata.normalize
("NFC", s).strip().lower() su una colonna name_norm indicizzata
UNIQUE (schema v4).
Limitazioni note (P2.a)
- No edges: non c'è ancora
entity_edges (predicate + weight),
quindi nessuna navigazione multi-hop tipo "entity vicine di X" o
"fact che connettono X a Y". P2.b lo aggiungerà con
hippo_entity_neighbors + hippo_ppr_retrieve (HippoRAG-style
Personalized PageRank).
- No auto-extraction: le entity e i link entity↔fact non vengono
estratti automaticamente dagli episodi. Per ora vanno inseriti
manualmente con script di seed. P2.c esporrà
hippo_extract_entities(text) LLM-based con json.loads strict.
- No type schema: il campo
type è una stringa libera
("person", "paper", ...). Non ci sono vincoli o gerarchie.
- Solo lookup esatto: cerca per
canonical_name o alias
con match Unicode case-insensitive. Nessun fuzzy match,
nessuna espansione semantica. "Schroedinger" NON matcha
"Schrödinger" (Ö vs OE è translitterazione, fuori scope).
- Population seed-only: il KG è popolato manualmente fino a P2.c.
Cost
- Zero LLM call.
- Sub-50ms su corpus ~500 entity (SQLite WAL + UNIQUE INDEX su
name_norm).
- Free in hosted mode (read on local SQLite + Python str ops).
Quando NON chiamarlo
- Claim factual da validare ("X did Y in YEAR") →
hippo_validate_claim
- Ricerca topic-based su fact ("fatti su engram") →
hippo_facts_search(query="engram")
- Ricerca episodica su dialoghi/sessioni ("quando abbiamo parlato di
Dominic") →
hippo_search o hippo_recall
- Domanda generica senza nome Capitalized ("che lavoro ho fatto ieri")
→
hippo_episode_list + hippo_session_recap
- Chat casuale, saluti, opinioni, output codice
Combinazioni utili
- Entity + tutti i suoi fact:
hippo_entity_get(name) →
prendi facts[] → hippo_facts_list(fact_ids=facts) per le
proposizioni complete.
- Entity + episodi correlati (workaround per assenza di edges):
hippo_entity_get(name) → leggi i fact_id → per ogni fact_id
chiama hippo_lineage_trace(start_id=fact_id, kind="fact", direction="backward") per arrivare agli episodi sorgente.
- Disambigua un nome:
hippo_entity_get ritorna gli aliases —
utile per capire come l'utente potrebbe averlo chiamato in altre
sessioni.
Storia
P2.a della roadmap Engram-amplifies-Claude (cycle #70).
Commit 54a0c35 (2026-05-15): MVP iniziale, engram/entity_kg.py,
3 tabelle SQLite (entities + entity_aliases + entity_facts),
10 RED→GREEN test, tool MCP hippo_entity_get.
Commit 12fa4ff (2026-05-15 notte tarda): hardening
multi-round critic-driven. 5 round adversariali consecutivi con
critic-orchestrator hanno scoperto 6 bug reali e li hanno
forzati a essere fixati:
- Race condition check-then-insert non atomico (8 thread paralleli
→ 2-8 duplicate). Fix:
BEGIN IMMEDIATE + UNIQUE INDEX su
name_norm + IntegrityError fallback re-SELECT.
- Cross-alias dedupe contract violation. Fix: SELECT
canonical-only (no JOIN su alias durante
store).
- Empty
canonical_name bypass. Fix: strip + ValueError.
- Unicode case-folding bug (SQLite
LOWER() ASCII-only:
MÜLLER/Müller diventavano 2 entity). Fix: colonne
name_norm/alias_norm Python-side via str.lower()
full-Unicode + migration v3 backfill + dedup duplicate
pre-existenti.
- NFC vs NFD form-folding bypass. Fix:
unicodedata.normalize("NFC", ...) in _norm() + migration
v4 re-backfill.
- Migration v4 IntegrityError su DB v3 con duplicate NFC+NFD.
Fix: DROP UNIQUE INDEX a inizio v4, UPDATE+dedup, ricreare
UNIQUE alla fine.
Schema finale v4: entities(id, canonical_name, name_norm, type, created_at) con UNIQUE su name_norm + entity_aliases
con alias_norm + entity_facts (PRIMARY KEY composito,
idempotente via INSERT OR IGNORE).
Test coverage: 17/17 PASS (10 base + 7 critic-driven). Full
pytest 2451 PASS 0 fail.
Spec: docs/specs/p2-entity-centric-kg.md (commit 48678a2).
Test: tests/test_entity_kg.py.
Cosa arriva dopo (NON ANCORA disponibile)
- P2.b (next):
hippo_entity_neighbors(entity_id, k, hops) +
hippo_ppr_retrieve(query_entities, damping=0.5, k=20) per
recall multi-hop tipo HippoRAG. Dipendenza: networkx (prpack).
- P2.c:
hippo_extract_entities(text, mode="ner+triple")
LLM-based 2-step (NER → triple) con json.loads strict, NO
parser eseguibile su LLM output. Hook automatico in
hippo_record_episode quando l'episodio è abbastanza ricco.
- P2.b/c addendum:
hippo_entity_link(fact_id, entities[])
per manual override e seed pipeline.
1---2name: hippoagent-entity-recall3description: When the user asks "cosa ricordi di X?", "dammi tutto su Y", "chi è Z?", "cosa abbiamo detto di W?", "ricostruisci ciò che sai di [Capitalized name or known alias]", call hippo_entity_get(name) BEFORE answering. Triggers also on "what do you know about X", "tell me about Y", "facts about Z", "history of W". The tool returns entity+aliases+facts from a persistent SQLite KG (P2.a entity-centric KG, schema v4, Unicode-safe). Lookup is case-insensitive Unicode (Müller/MÜLLER/müller same match) and resolves aliases ("S. Tonegawa" → canonical "Tonegawa"), returning the list of fact_id linked to the entity. If entity is None, declare honestly "non ho memoria di X" — do NOT confabulate from training data. If entity exists but facts == [], declare "ho il nodo entity ma nessun fatto collegato" and suggest hippo_facts_search or hippo_recall. Zero LLM cost, sub-50ms on ~500 entity corpus, free in hosted mode. Backend SQLite WAL + UNIQUE INDEX on name_norm Python-normalized (NFC + str.lower, NOT SQLite ASCII-only LOWER). 4---5
6# HippoAgent — entity-first recall (hippo_entity_get)
7
8Quando l'utente chiede "che sai di X?" / "ricordami chi è Y?" /
9"dammi tutto su Z", chiama `hippo_entity_get(name=<X>)` PRIMA di
10rispondere da memoria interna. Il tool ritorna l'entity canonica
11(risolvendo eventuali alias) + la lista di fact collegati, da un
12knowledge graph SQLite persistente.
13
14## ⚠️ Quando ATTIVARE (trigger)
15
16Pattern lessicali che attivano il skill:
17
18- **Italiano**: "che sai di X", "cosa ricordi di Y", "dammi tutto
19 su Z", "chi è W", "ricostruisci ciò che sappiamo di V",
20 "abbiamo discusso di U", "informazioni su T", "storia di S".
21- **English**: "what do you know about X", "tell me about Y",
22 "facts about Z", "who is W", "history of V", "what have we said
23 about U", "summarize what we know on T".
24- **Implicito**: l'utente menziona un nome Capitalized (persona,
25 paper, repo, progetto interno) e ti aspetti che ne sappia qualcosa.
26
27**Risoluzione automatica alias**: lookup di "S. Tonegawa" trova
28l'entity canonica "Tonegawa" se è stato registrato l'alias.
29
30**Case + Unicode-safe**: "müller", "MÜLLER", "Müller" trovano
31tutti la stessa entity. Anche NFC vs NFD (`unicodedata.normalize`
32forma decomposta vs composta) sono trattati come uguali.
33
34**NON serve** per:
35- Claim factual da validare ("X is Y") → `hippo_validate_claim`
36- Ricerca su episodi/dialoghi su un topic → `hippo_search`,
37 `hippo_recall`
38- Ricerca su fact per topic-namespace → `hippo_facts_search`,
39 `hippo_facts_list`
40- Saluti, chat casuale, opinioni
41
42## Flow
43
44```
451. Identifica il nome dell'entity nella domanda (Capitalized,
46 eventualmente con prefisso "S." / "Dr." / titolo).
472. hippo_entity_get(name="<entity>")
483. Read payload:
49 {
50 "entity": { "id": "...", "canonical_name": "...", "type": "..." }
51 | None,
52 "aliases": ["...", "..."],
53 "facts": ["fact_id_1", "fact_id_2", ...]
54 }
554. Act:
56 ┌─ entity is None → "Non ho memoria di <name> nel KG. Vuoi
57 │ che cerchi episodi correlati?
58 │ (`hippo_search`)"
59 ├─ entity, facts == [] → "Ho il nodo <canonical_name> (id=...,
60 │ type=...) ma nessun fatto direttamente
61 │ linkato. Posso cercare con
62 │ `hippo_facts_search` o `hippo_recall`."
63 └─ entity, facts ≠ [] → Leggi i fact con `hippo_facts_list`
64 (passando i fact_id ottenuti) per
65 ricostruire la conoscenza completa,
66 poi rispondi citando i fact_id come
67 evidenza.
68```
69
70## Output semantics
71
72| campo | tipo | semantica |
73|---|---|---|
74| `entity` | dict \| None | None = entity sconosciuta. Dict = `{id, canonical_name, type}` |
75| `entity.id` | str | UUID 12-char, primary key SQLite, usalo per `hippo_entity_link` (futuro P2.a addendum) |
76| `entity.canonical_name` | str | Nome canonico (la forma "ufficiale", es. "Tonegawa") |
77| `entity.type` | str | Tag libero, es. `"person"`, `"paper"`, `"repo"`, `"project"`, `"concept"` — può essere `""` |
78| `aliases` | list[str] | Tutte le forme alternative registrate (es. `["S. Tonegawa", "Susumu Tonegawa"]`) |
79| `facts` | list[str] | `fact_id` collegati all'entity tramite tabella `entity_facts`. Vuota se nessun link |
80
81## Parametri
82
83- `name: str` — il nome o alias da cercare. Trim + Unicode NFC +
84 case-fold automatici dal backend.
85
86## Esempi concreti
87
88### Esempio 1 — entity trovata con facts
89
90User: "cosa ricordi di Tonegawa?"
91
92```
93hippo_entity_get(name="Tonegawa")
94→ {
95 "entity": {
96 "id": "a8f3c9d12e4b",
97 "canonical_name": "Tonegawa",
98 "type": "person"
99 },
100 "aliases": ["S. Tonegawa", "Susumu Tonegawa"],
101 "facts": ["f_tonegawa_nobel_1987", "f_tonegawa_engram_2014"]
102 }
103```
104
105Azione: chiama `hippo_facts_list(fact_ids=["f_tonegawa_nobel_1987",
106"f_tonegawa_engram_2014"])` per leggere le proposizioni complete,
107poi rispondi:
108
109> Tonegawa (Susumu Tonegawa, person) — 2 fact in memoria:
110> 1. Nobel Prize 1987 per immunology (V(D)J recombination)
111> [f_tonegawa_nobel_1987]
112> 2. Lavori sull'engram a MIT 2014 [f_tonegawa_engram_2014]
113> Alias noti: "S. Tonegawa", "Susumu Tonegawa".
114
115### Esempio 2 — alias risolve a canonical
116
117User: "dammi tutto su S. Tonegawa"
118
119```
120hippo_entity_get(name="S. Tonegawa")
121→ {
122 "entity": {"id": "a8f3c9d12e4b", "canonical_name": "Tonegawa", ...},
123 "aliases": [...],
124 "facts": [...]
125 }
126```
127
128L'alias è risolto trasparentemente: stesso `id` dell'entity canonica.
129
130### Esempio 3 — entity sconosciuta
131
132User: "che sai di Marie Curie?"
133
134```
135hippo_entity_get(name="Marie Curie")
136→ {"entity": None, "aliases": [], "facts": []}
137```
138
139Risposta corretta (onesta):
140
141> Non ho memoria di Marie Curie nel knowledge graph entity-centric.
142> Posso cercare episodi correlati con `hippo_search("Marie Curie")`
143> o fatti con `hippo_facts_search`?
144
145**NON CONFABULARE** una risposta basata su training data — l'utente
146ha chiesto cosa c'è in *memoria persistente*, non cosa sa il modello.
147
148### Esempio 4 — entity nota, zero fact linkati
149
150User: "abbiamo discusso di NeuroFlask?"
151
152```
153hippo_entity_get(name="NeuroFlask")
154→ {
155 "entity": {"id": "...", "canonical_name": "NeuroFlask", "type": "repo"},
156 "aliases": [],
157 "facts": []
158 }
159```
160
161Risposta:
162
163> Il nodo entity "NeuroFlask" esiste nel KG (type=repo) ma non ha
164> fatti direttamente linkati. Probabilmente è stato menzionato in
165> episodi senza che facts/triple siano state estratte (l'auto-
166> extraction OpenIE è P2.c, non ancora attiva). Vuoi che cerchi negli
167> episodi con `hippo_search`?
168
169### Esempio 5 — Unicode case-fold + NFC
170
171User: "che sai di müller?" (lowercase, NFC)
172
173User store-side: `hippo_entity_get(name="MÜLLER")` o
174`name="Müller"` o anche forma NFD u+combining-diaeresis →
175tutti risolvono allo stesso `id`. Backend usa `unicodedata.normalize`
176`("NFC", s).strip().lower()` su una colonna `name_norm` indicizzata
177UNIQUE (schema v4).
178
179## Limitazioni note (P2.a)
180
181- **No edges**: non c'è ancora `entity_edges` (predicate + weight),
182 quindi nessuna navigazione multi-hop tipo "entity vicine di X" o
183 "fact che connettono X a Y". P2.b lo aggiungerà con
184 `hippo_entity_neighbors` + `hippo_ppr_retrieve` (HippoRAG-style
185 Personalized PageRank).
186- **No auto-extraction**: le entity e i link entity↔fact non vengono
187 estratti automaticamente dagli episodi. Per ora vanno inseriti
188 manualmente con script di seed. P2.c esporrà
189 `hippo_extract_entities(text)` LLM-based con `json.loads` strict.
190- **No type schema**: il campo `type` è una stringa libera
191 (`"person"`, `"paper"`, ...). Non ci sono vincoli o gerarchie.
192- **Solo lookup esatto**: cerca per `canonical_name` o alias
193 con match Unicode case-insensitive. Nessun fuzzy match,
194 nessuna espansione semantica. "Schroedinger" NON matcha
195 "Schrödinger" (Ö vs OE è translitterazione, fuori scope).
196- **Population seed-only**: il KG è popolato manualmente fino a P2.c.
197
198## Cost
199
200- Zero LLM call.
201- Sub-50ms su corpus ~500 entity (SQLite WAL + UNIQUE INDEX su
202 `name_norm`).
203- Free in hosted mode (read on local SQLite + Python str ops).
204
205## Quando NON chiamarlo
206
207- Claim factual da validare ("X did Y in YEAR") → `hippo_validate_claim`
208- Ricerca topic-based su fact ("fatti su engram") →
209 `hippo_facts_search(query="engram")`
210- Ricerca episodica su dialoghi/sessioni ("quando abbiamo parlato di
211 Dominic") → `hippo_search` o `hippo_recall`
212- Domanda generica senza nome Capitalized ("che lavoro ho fatto ieri")
213 → `hippo_episode_list` + `hippo_session_recap`
214- Chat casuale, saluti, opinioni, output codice
215
216## Combinazioni utili
217
218- **Entity + tutti i suoi fact**: `hippo_entity_get(name)` →
219 prendi `facts[]` → `hippo_facts_list(fact_ids=facts)` per le
220 proposizioni complete.
221- **Entity + episodi correlati** (workaround per assenza di edges):
222 `hippo_entity_get(name)` → leggi i fact_id → per ogni fact_id
223 chiama `hippo_lineage_trace(start_id=fact_id, kind="fact",
224 direction="backward")` per arrivare agli episodi sorgente.
225- **Disambigua un nome**: `hippo_entity_get` ritorna gli `aliases` —
226 utile per capire come l'utente potrebbe averlo chiamato in altre
227 sessioni.
228
229## Storia
230
231P2.a della roadmap Engram-amplifies-Claude (cycle #70).
232- Commit `54a0c35` (2026-05-15): MVP iniziale, `engram/entity_kg.py`,
233 3 tabelle SQLite (`entities` + `entity_aliases` + `entity_facts`),
234 10 RED→GREEN test, tool MCP `hippo_entity_get`.
235- Commit `12fa4ff` (2026-05-15 notte tarda): **hardening
236 multi-round critic-driven**. 5 round adversariali consecutivi con
237 critic-orchestrator hanno scoperto 6 bug reali e li hanno
238 forzati a essere fixati:
239 1. Race condition check-then-insert non atomico (8 thread paralleli
240 → 2-8 duplicate). Fix: `BEGIN IMMEDIATE` + `UNIQUE INDEX` su
241 `name_norm` + IntegrityError fallback re-SELECT.
242 2. Cross-alias dedupe contract violation. Fix: SELECT
243 canonical-only (no JOIN su alias durante `store`).
244 3. Empty `canonical_name` bypass. Fix: strip + `ValueError`.
245 4. Unicode case-folding bug (SQLite `LOWER()` ASCII-only:
246 MÜLLER/Müller diventavano 2 entity). Fix: colonne
247 `name_norm`/`alias_norm` Python-side via `str.lower()`
248 full-Unicode + migration v3 backfill + dedup duplicate
249 pre-existenti.
250 5. NFC vs NFD form-folding bypass. Fix:
251 `unicodedata.normalize("NFC", ...)` in `_norm()` + migration
252 v4 re-backfill.
253 6. Migration v4 IntegrityError su DB v3 con duplicate NFC+NFD.
254 Fix: DROP UNIQUE INDEX a inizio v4, UPDATE+dedup, ricreare
255 UNIQUE alla fine.
256
257 Schema finale v4: `entities(id, canonical_name, name_norm,
258 type, created_at)` con UNIQUE su `name_norm` + `entity_aliases`
259 con `alias_norm` + `entity_facts` (PRIMARY KEY composito,
260 idempotente via INSERT OR IGNORE).
261
262 Test coverage: 17/17 PASS (10 base + 7 critic-driven). Full
263 pytest 2451 PASS 0 fail.
264
265- Spec: `docs/specs/p2-entity-centric-kg.md` (commit `48678a2`).
266- Test: `tests/test_entity_kg.py`.
267
268## Cosa arriva dopo (NON ANCORA disponibile)
269
270- **P2.b** (next): `hippo_entity_neighbors(entity_id, k, hops)` +
271 `hippo_ppr_retrieve(query_entities, damping=0.5, k=20)` per
272 recall multi-hop tipo HippoRAG. Dipendenza: `networkx` (prpack).
273- **P2.c**: `hippo_extract_entities(text, mode="ner+triple")`
274 LLM-based 2-step (NER → triple) con `json.loads` strict, NO
275 parser eseguibile su LLM output. Hook automatico in
276 `hippo_record_episode` quando l'episodio è abbastanza ricco.
277- **P2.b/c addendum**: `hippo_entity_link(fact_id, entities[])`
278 per manual override e seed pipeline.