| name | hippoagent-entity-recall |
| description | 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). Entity-first recall — complements hippo_facts_search (topic-based) and hippo_recall (semantic episode) with a third modality, graph node lookup. ANTI-PATTERN: do NOT use for factual claim validation ("Tonegawa won Nobel in 2014") — that's hippo_validate_claim. Do NOT use for topic-based episode/dialog search — that's hippo_search or hippo_recall. |
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.