graph-engine-memory 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graph_engine/__init__.py +1 -0
- graph_engine/__main__.py +1275 -0
- graph_engine/access.py +202 -0
- graph_engine/agent_memory.py +103 -0
- graph_engine/brain.py +731 -0
- graph_engine/brain_engine.py +559 -0
- graph_engine/communities.py +503 -0
- graph_engine/consolidation.py +191 -0
- graph_engine/context.py +203 -0
- graph_engine/decay.py +65 -0
- graph_engine/demo.py +165 -0
- graph_engine/dream.py +408 -0
- graph_engine/embedder.py +84 -0
- graph_engine/engine.py +58 -0
- graph_engine/entities.py +376 -0
- graph_engine/evals.py +1206 -0
- graph_engine/feedback.py +155 -0
- graph_engine/gaps.py +209 -0
- graph_engine/graph.py +133 -0
- graph_engine/hygiene.py +213 -0
- graph_engine/inference.py +251 -0
- graph_engine/intent.py +207 -0
- graph_engine/mcp/__init__.py +1 -0
- graph_engine/mcp/agent/instructions.md +34 -0
- graph_engine/mcp/format.py +66 -0
- graph_engine/mcp/server.py +407 -0
- graph_engine/merge.py +220 -0
- graph_engine/model.py +56 -0
- graph_engine/namespaces.py +74 -0
- graph_engine/recall.py +161 -0
- graph_engine/report.py +401 -0
- graph_engine/reranker.py +86 -0
- graph_engine/retrieval.py +296 -0
- graph_engine/review.py +95 -0
- graph_engine/runtime.py +83 -0
- graph_engine/server.py +239 -0
- graph_engine/similarity.py +42 -0
- graph_engine/store.py +63 -0
- graph_engine/suggester.py +63 -0
- graph_engine/temporal.py +216 -0
- graph_engine/tiers.py +135 -0
- graph_engine/web/app.js +578 -0
- graph_engine/web/index.html +169 -0
- graph_engine/web/report.html +39 -0
- graph_engine/web/report.js +50 -0
- graph_engine/web/review.html +104 -0
- graph_engine/web/review.js +211 -0
- graph_engine_memory-0.6.1.dist-info/METADATA +125 -0
- graph_engine_memory-0.6.1.dist-info/RECORD +53 -0
- graph_engine_memory-0.6.1.dist-info/WHEEL +5 -0
- graph_engine_memory-0.6.1.dist-info/entry_points.txt +3 -0
- graph_engine_memory-0.6.1.dist-info/licenses/LICENSE +21 -0
- graph_engine_memory-0.6.1.dist-info/top_level.txt +1 -0
graph_engine/tiers.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Storage priority, independent of node type and lifecycle status.
|
|
2
|
+
|
|
3
|
+
Tier changes are persisted by maintenance, never by retrieval. Files remain at
|
|
4
|
+
stable nodes/<id>.md paths: tiers are logical storage classes, not moves.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from .brain import Brain, Node, STORAGE_TIERS
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .brain_engine import BrainEngine
|
|
17
|
+
|
|
18
|
+
MAIN_MIN_RECALLS = 3
|
|
19
|
+
MAIN_RECENT_DAYS = 7
|
|
20
|
+
MAIN_IDLE_DAYS = 30
|
|
21
|
+
ARCHIVE_IDLE_DAYS = 90
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _age_days(value: str | None, now: datetime) -> float | None:
|
|
25
|
+
try:
|
|
26
|
+
stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
27
|
+
if stamp.tzinfo is None:
|
|
28
|
+
stamp = stamp.replace(tzinfo=timezone.utc)
|
|
29
|
+
return max(0.0, (now - stamp).total_seconds() / 86400)
|
|
30
|
+
except (ValueError, TypeError, AttributeError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def recommended_tier(node: Node, *, now: datetime | None = None) -> str:
|
|
35
|
+
"""Keep manual choices; promote used memories and demote idle ones."""
|
|
36
|
+
if node.tier_locked or node.status == "tombstone":
|
|
37
|
+
return node.storage_tier
|
|
38
|
+
now = now or datetime.now(timezone.utc)
|
|
39
|
+
if now.tzinfo is None:
|
|
40
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
41
|
+
age = _age_days(node.last_recalled or node.created, now)
|
|
42
|
+
if age is None:
|
|
43
|
+
return node.storage_tier # unknown age must not trigger a demotion
|
|
44
|
+
if age >= ARCHIVE_IDLE_DAYS:
|
|
45
|
+
return "archival"
|
|
46
|
+
if (node.last_recalled and age <= MAIN_RECENT_DAYS
|
|
47
|
+
and node.recall_count >= MAIN_MIN_RECALLS):
|
|
48
|
+
return "main"
|
|
49
|
+
if node.storage_tier == "main" and age < MAIN_IDLE_DAYS:
|
|
50
|
+
return "main" # hysteresis avoids oscillation at the promotion boundary
|
|
51
|
+
return "recall"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def rebalance(brain: Brain, *, dry_run: bool = False, commit: bool = True,
|
|
55
|
+
now: datetime | None = None) -> list[dict]:
|
|
56
|
+
"""Apply automatic tier policy during maintenance (including idle brains)."""
|
|
57
|
+
from .brain_engine import BRAIN_LOCK
|
|
58
|
+
getattr(brain, "authorize", lambda **_: None)(action="admin")
|
|
59
|
+
with BRAIN_LOCK:
|
|
60
|
+
changes = []
|
|
61
|
+
for node in brain.read_nodes():
|
|
62
|
+
target = recommended_tier(node, now=now)
|
|
63
|
+
if target == node.storage_tier:
|
|
64
|
+
continue
|
|
65
|
+
changes.append({"node_id": node.id, "from": node.storage_tier, "to": target})
|
|
66
|
+
if not dry_run:
|
|
67
|
+
node.storage_tier = target
|
|
68
|
+
brain.write_node(node)
|
|
69
|
+
if changes and not dry_run and commit:
|
|
70
|
+
brain.commit_and_push(f"tiers: rebalance {len(changes)} memories")
|
|
71
|
+
return changes
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_tier(brain: Brain, node_id: str, tier: str | None, *,
|
|
75
|
+
commit: bool = True) -> Node:
|
|
76
|
+
"""A manual tier is pinned until reset with tier=None (automatic policy)."""
|
|
77
|
+
if tier is not None and tier not in STORAGE_TIERS:
|
|
78
|
+
raise ValueError(f"Unknown storage tier: {tier!r}")
|
|
79
|
+
from .brain_engine import BRAIN_LOCK
|
|
80
|
+
with BRAIN_LOCK:
|
|
81
|
+
getattr(brain, "authorize", lambda **_: None)(action="write", node_id=node_id)
|
|
82
|
+
brain.ensure_ready()
|
|
83
|
+
brain.pull()
|
|
84
|
+
node = brain.read_node(node_id)
|
|
85
|
+
if node is None:
|
|
86
|
+
raise ValueError(f"No node with id {node_id!r}.")
|
|
87
|
+
node.tier_locked = tier is not None
|
|
88
|
+
node.storage_tier = tier if tier is not None else recommended_tier(node)
|
|
89
|
+
brain.write_node(node)
|
|
90
|
+
if commit:
|
|
91
|
+
brain.commit_and_push(f"tier: {node_id} -> {node.storage_tier}")
|
|
92
|
+
return node
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def order_by_tier(hits: list[tuple[str, float]], tiers: dict[str, str]) -> list[tuple[str, float]]:
|
|
96
|
+
"""Stable priority order; preserve relevance/reranker order within a tier."""
|
|
97
|
+
priority = {tier: index for index, tier in enumerate(STORAGE_TIERS)}
|
|
98
|
+
return sorted(hits, key=lambda hit: priority.get(tiers.get(hit[0], "recall"), 1))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_tier(engine: BrainEngine, args: list[str]) -> None:
|
|
102
|
+
parser = argparse.ArgumentParser(prog="ig tier")
|
|
103
|
+
parser.add_argument("node_id", nargs="?")
|
|
104
|
+
parser.add_argument("tier", nargs="?", choices=(*STORAGE_TIERS, "auto"))
|
|
105
|
+
parser.add_argument("--rebalance", action="store_true")
|
|
106
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
107
|
+
parser.add_argument("--json", action="store_true")
|
|
108
|
+
opts = parser.parse_args(args)
|
|
109
|
+
if opts.rebalance and (opts.node_id or opts.tier):
|
|
110
|
+
parser.error("--rebalance cannot be combined with a node")
|
|
111
|
+
if opts.dry_run and not opts.rebalance:
|
|
112
|
+
parser.error("--dry-run requires --rebalance")
|
|
113
|
+
if not opts.rebalance and not opts.node_id:
|
|
114
|
+
parser.error("provide a node ID or --rebalance")
|
|
115
|
+
try:
|
|
116
|
+
if opts.rebalance:
|
|
117
|
+
getattr(engine.brain, "authorize", lambda **_: None)(action="admin")
|
|
118
|
+
if not opts.dry_run:
|
|
119
|
+
engine.brain.ensure_ready()
|
|
120
|
+
engine.brain.pull()
|
|
121
|
+
result = {"changes": rebalance(engine.brain, dry_run=opts.dry_run),
|
|
122
|
+
"dry_run": opts.dry_run}
|
|
123
|
+
else:
|
|
124
|
+
node = (set_tier(engine.brain, opts.node_id,
|
|
125
|
+
None if opts.tier == "auto" else opts.tier)
|
|
126
|
+
if opts.tier else engine.brain.read_node(opts.node_id))
|
|
127
|
+
if node is None:
|
|
128
|
+
raise ValueError(f"No node with id {opts.node_id!r}.")
|
|
129
|
+
result = {"node_id": node.id, "storage_tier": node.storage_tier,
|
|
130
|
+
"tier_locked": node.tier_locked}
|
|
131
|
+
except ValueError as exc:
|
|
132
|
+
parser.error(str(exc))
|
|
133
|
+
print(json.dumps(result, indent=2) if opts.json or opts.rebalance else
|
|
134
|
+
f"{result['node_id']} {result['storage_tier']} "
|
|
135
|
+
f"({'manual' if result['tier_locked'] else 'automatic'})")
|