269 lines
8.5 KiB
Markdown
269 lines
8.5 KiB
Markdown
# Graph Memory Architecture and Operations
|
|
|
|
This document describes the Graph features implemented by the current SDK.
|
|
It is intended for operators enabling Graph and maintainers working on
|
|
`pipelines/_graph`, `reader_base.py`, or a GraphStore backend.
|
|
|
|
Graph is an optional, fail-open enhancement. Vector retrieval remains the
|
|
source of truth: if Graph is unavailable, incomplete, or disabled, normal
|
|
memory search must continue to work.
|
|
|
|
## 1. Enabling Graph
|
|
|
|
Graph availability depends on the processing mode:
|
|
|
|
| Mode | Graph behavior |
|
|
|---|---|
|
|
| `lite` | Disabled |
|
|
| `pro` | Controlled by `MEMORY_ENABLE_GRAPH`, default `false` |
|
|
| `ultra` | Enabled |
|
|
|
|
The default backend is embedded Kuzu. Neo4j is available through the `graph`
|
|
optional dependency.
|
|
|
|
```bash
|
|
# Embedded, single-process deployment
|
|
MEMORY_MODE=pro
|
|
MEMORY_ENABLE_GRAPH=true
|
|
MEMORY_GRAPH_PROVIDER=kuzu
|
|
MEMORY_GRAPH_DB_PATH=/data/hy-memory/kuzu
|
|
|
|
# Multi-process or remote deployment
|
|
MEMORY_GRAPH_PROVIDER=neo4j
|
|
NEO4J_URL=bolt://localhost:7687
|
|
NEO4J_USERNAME=neo4j
|
|
NEO4J_PASSWORD=secret
|
|
NEO4J_DATABASE=neo4j
|
|
```
|
|
|
|
Kuzu permits only one process to open a database path. Use Neo4j when several
|
|
workers must share the same Graph.
|
|
|
|
## 2. Write-side projection
|
|
|
|
When Graph and `MEMORY_GRAPH_WRITE` are enabled, persisted L2 facts are
|
|
projected into Graph asynchronously. Projection failures are logged and do not
|
|
roll back the VDB write.
|
|
|
|
### 2.1 Node model
|
|
|
|
An L2 fact is represented as `Memory(layer="l2_fact")`, not as `VdbRef`.
|
|
This allows the node to participate in Topic, Entity, relation, and temporal
|
|
edges. `VdbRef` remains the lightweight target used by L6 `DERIVED_FROM`
|
|
evidence links.
|
|
|
|
Graph projection does not replace the VDB. Content, vectors, event time, and
|
|
normal retrieval remain VDB-owned.
|
|
|
|
### 2.2 Edge sources
|
|
|
|
Current relation sources are:
|
|
|
|
| Relation | Source | Additional LLM call |
|
|
|---|---|---|
|
|
| `co_occurs` | L2 facts created in one write batch | No |
|
|
| `semantic_neighbor` | Reconcile candidates above `MEMORY_GRAPH_EDGE_MIN_SCORE` | No |
|
|
| `causes` | Reconcile `related` output | No additional call |
|
|
| `elaborates` | Reconcile `related` output | No additional call |
|
|
| `contrasts` | Reconcile `related` output | No additional call |
|
|
| `same_series` | Reconcile `related` output | No additional call |
|
|
| `TAGGED_WITH` | Memory tags projected to Topic nodes | No new memory LLM call |
|
|
| `MENTIONS` | NER entities projected to Entity nodes | No |
|
|
| `TEMPORAL_NEXT` | Optional event-time ordering | No |
|
|
|
|
`co_occurs` is bounded by `MEMORY_GRAPH_CO_OCCURS_MAX_BATCH` to prevent an
|
|
O(n²) edge explosion. `semantic_neighbor` is inexpensive but mostly repeats
|
|
information already present in vector similarity; typed and co-occurrence
|
|
relations usually carry more structural information.
|
|
|
|
### 2.3 Lifecycle synchronization
|
|
|
|
Graph state follows memory lifecycle changes:
|
|
|
|
- ADD projects the new active fact and its edges.
|
|
- UPDATE refreshes the projected node.
|
|
- SUPERSEDE and SHADOW retire the old projection from active retrieval.
|
|
- Search filters inactive or shadowed nodes even if stale edges remain.
|
|
|
|
Projection code lives in `hy_memory/pipelines/_graph/projector.py` and is
|
|
scheduled from `hy_memory/pipelines/writer.py`.
|
|
|
|
## 3. Read-side Graph usage
|
|
|
|
`legacy` and `hybrid` readers share the same Graph post-processing. The readers
|
|
differ only in candidate recall: `legacy` uses dense vectors; `hybrid` adds the
|
|
keyword/BM25 channel.
|
|
|
|
Enable Graph post-processing with:
|
|
|
|
```bash
|
|
MEMORY_GRAPH_RERANK_ENABLED=true
|
|
```
|
|
|
|
All Graph operations are fail-open. Exceptions return the pre-Graph candidate
|
|
order unless a test explicitly requests strict behavior.
|
|
|
|
### 3.1 A — Support propagation
|
|
|
|
Support propagation reranks candidates already present in the expanded recall
|
|
pool. It never adds a new candidate.
|
|
|
|
For candidate `v`, the implementation computes bounded support from recalled
|
|
neighbors and applies a small score adjustment:
|
|
|
|
```text
|
|
support(v) = sum(edge_prior(u,v) * score(u)) / sqrt(pool_degree(v))
|
|
score'(v) = score(v) + beta * min(support(v), support_cap)
|
|
```
|
|
|
|
Relevant settings:
|
|
|
|
```bash
|
|
MEMORY_GRAPH_PROPAGATE_BETA=0.15
|
|
MEMORY_GRAPH_PROPAGATE_ITERS=1
|
|
MEMORY_GRAPH_SUPPORT_CAP=1.0
|
|
```
|
|
|
|
### 3.2 B — Bounded relation expansion
|
|
|
|
Relation expansion may nominate nodes outside the initial pool. Admission is
|
|
bounded by all of the following:
|
|
|
|
- traversal hop limit;
|
|
- seed score floor;
|
|
- prefilter and expansion limits;
|
|
- relation-type quota;
|
|
- semantic hard gate `sim(candidate) >= sim(seed) - delta`;
|
|
- final multi-channel scoring.
|
|
|
|
Admitted items are merged into a seed by default, so they do not consume an
|
|
extra top-k slot.
|
|
|
|
Core settings and current defaults:
|
|
|
|
```bash
|
|
MEMORY_GRAPH_RELAX_GAMMA=0.3
|
|
MEMORY_GRAPH_SEED_FLOOR=0.5
|
|
MEMORY_GRAPH_MAX_EXPAND=20
|
|
MEMORY_GRAPH_EXPAND_HOPS=2
|
|
MEMORY_GRAPH_EXPAND_PREFILTER_TOP_N=50
|
|
MEMORY_GRAPH_SEED_SIM_DELTA=0.2
|
|
MEMORY_GRAPH_EXPAND_MERGE_SEED=true
|
|
```
|
|
|
|
Set `MEMORY_GRAPH_RELAX_GAMMA=0` to disable relation expansion while retaining
|
|
support propagation and relation paths.
|
|
|
|
### 3.3 C — Relation paths
|
|
|
|
Relation paths preserve connection semantics that would be lost in a flat
|
|
memory list. The shared reader stores them independently from normal memories
|
|
in its diagnostic `ReadResponse.extra["related_paths"]` field:
|
|
|
|
```python
|
|
read_response.extra["related_paths"]
|
|
```
|
|
|
|
Each path contains the ordered nodes and relation types. Temporal edges also
|
|
include direction (`earlier -> later`).
|
|
|
|
```bash
|
|
MEMORY_GRAPH_PATH_ENABLED=true
|
|
MEMORY_GRAPH_PATH_HIGH_FLOOR=0.5
|
|
```
|
|
|
|
### 3.4 Relation-type priors
|
|
|
|
The reader weights an edge by both its stored weight and a relation-type prior.
|
|
Current priors are defined in `pipelines/_graph/rerank.py`:
|
|
|
|
| Relation | Prior |
|
|
|---|---:|
|
|
| `causes` | 0.90 |
|
|
| `elaborates` | 0.90 |
|
|
| `same_series` | 0.85 |
|
|
| `contrasts` | 0.70 |
|
|
| `temporal_next` | 0.70 |
|
|
| `co_occurs` | 0.55 |
|
|
| `episode` | 0.55 |
|
|
| untyped `related` | 0.50 |
|
|
| `semantic_neighbor` | 0.35 |
|
|
|
|
Higher priors are reserved for relations that provide information not already
|
|
encoded by vector similarity. The degree normalization and support cap prevent
|
|
high-degree nodes from dominating ranking.
|
|
|
|
## 4. Temporal edges and episodes
|
|
|
|
`MEMORY_GRAPH_TEMPORAL_SPINE=true` creates directed `TEMPORAL_NEXT` edges using
|
|
VDB `memory_at` values. Graph nodes do not own canonical event time; projection
|
|
resolves timestamps from VDB before building the spine.
|
|
|
|
Episodes are separate from Graph. `episode_id` groups memories belonging to one
|
|
discussion and can provide bounded candidate support even when Graph is off.
|
|
|
|
```bash
|
|
MEMORY_EPISODE_RECALL_ENABLED=false
|
|
MEMORY_EPISODE_RECALL_WEIGHT=0.7
|
|
MEMORY_EPISODE_SUMMARY_ENABLED=false
|
|
```
|
|
|
|
Detailed Episode and summary settings are listed in `docs/env_reference.md`.
|
|
|
|
## 5. Backfill and rebuild
|
|
|
|
Existing VDB facts can be projected with:
|
|
|
|
```python
|
|
client.build_memory_graph(
|
|
user_id="u1",
|
|
agent_id=None, # None means all agents for the user
|
|
clear_first=False,
|
|
)
|
|
```
|
|
|
|
Backfill can reconstruct:
|
|
|
|
- L2 Memory nodes;
|
|
- Topic and Entity projections;
|
|
- `semantic_neighbor` edges from stored vectors;
|
|
- optional `TEMPORAL_NEXT` edges.
|
|
|
|
Backfill cannot reconstruct batch `co_occurs` edges or LLM typed relations,
|
|
because the original write-batch and reconcile decisions are not recoverable.
|
|
|
|
Use `clear_first=True` after changing projection rules. It clears the selected
|
|
L2 projection and orphaned Topic/Entity nodes without deleting System 2 L6
|
|
schemas.
|
|
|
|
## 6. Observability
|
|
|
|
The shared reader records internal diagnostics in:
|
|
|
|
```python
|
|
read_response.extra["graph_rerank"] # edge, propagation, and admission stats
|
|
read_response.extra["related_paths"] # decorated relation paths
|
|
```
|
|
|
|
`HyMemoryClient.search()` does not currently forward these two internal fields.
|
|
For public operational visibility, use pipeline traces: read-side Graph work is
|
|
recorded as `READ_GRAPH_RERANK`, while write projection reports `GRAPH_PROJECT`
|
|
statistics and errors.
|
|
|
|
## 7. Implementation map
|
|
|
|
| Responsibility | Module |
|
|
|---|---|
|
|
| Projection and lifecycle synchronization | `hy_memory/pipelines/_graph/projector.py` |
|
|
| Support, expansion, paths, priors | `hy_memory/pipelines/_graph/rerank.py` |
|
|
| Reader integration | `hy_memory/pipelines/reader_base.py` |
|
|
| Write scheduling | `hy_memory/pipelines/writer.py` |
|
|
| Relation output parsing | `hy_memory/agent/reconciler.py` |
|
|
| GraphStore interface | `hy_memory/data/graph_store_base.py` |
|
|
| Kuzu backend | `hy_memory/data/graph_store_kuzu.py` |
|
|
| Neo4j backend | `hy_memory/data/graph_store_neo4j.py` |
|
|
| Graph backfill API | `HyMemoryClient.build_memory_graph()` |
|
|
|
|
Tests covering these contracts live primarily in `tests/test_memory_graph.py`,
|
|
`tests/test_graph_edge_idempotency.py`, and `tests/test_profile_evidence.py`.
|