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.
Files changed (53) hide show
  1. graph_engine/__init__.py +1 -0
  2. graph_engine/__main__.py +1275 -0
  3. graph_engine/access.py +202 -0
  4. graph_engine/agent_memory.py +103 -0
  5. graph_engine/brain.py +731 -0
  6. graph_engine/brain_engine.py +559 -0
  7. graph_engine/communities.py +503 -0
  8. graph_engine/consolidation.py +191 -0
  9. graph_engine/context.py +203 -0
  10. graph_engine/decay.py +65 -0
  11. graph_engine/demo.py +165 -0
  12. graph_engine/dream.py +408 -0
  13. graph_engine/embedder.py +84 -0
  14. graph_engine/engine.py +58 -0
  15. graph_engine/entities.py +376 -0
  16. graph_engine/evals.py +1206 -0
  17. graph_engine/feedback.py +155 -0
  18. graph_engine/gaps.py +209 -0
  19. graph_engine/graph.py +133 -0
  20. graph_engine/hygiene.py +213 -0
  21. graph_engine/inference.py +251 -0
  22. graph_engine/intent.py +207 -0
  23. graph_engine/mcp/__init__.py +1 -0
  24. graph_engine/mcp/agent/instructions.md +34 -0
  25. graph_engine/mcp/format.py +66 -0
  26. graph_engine/mcp/server.py +407 -0
  27. graph_engine/merge.py +220 -0
  28. graph_engine/model.py +56 -0
  29. graph_engine/namespaces.py +74 -0
  30. graph_engine/recall.py +161 -0
  31. graph_engine/report.py +401 -0
  32. graph_engine/reranker.py +86 -0
  33. graph_engine/retrieval.py +296 -0
  34. graph_engine/review.py +95 -0
  35. graph_engine/runtime.py +83 -0
  36. graph_engine/server.py +239 -0
  37. graph_engine/similarity.py +42 -0
  38. graph_engine/store.py +63 -0
  39. graph_engine/suggester.py +63 -0
  40. graph_engine/temporal.py +216 -0
  41. graph_engine/tiers.py +135 -0
  42. graph_engine/web/app.js +578 -0
  43. graph_engine/web/index.html +169 -0
  44. graph_engine/web/report.html +39 -0
  45. graph_engine/web/report.js +50 -0
  46. graph_engine/web/review.html +104 -0
  47. graph_engine/web/review.js +211 -0
  48. graph_engine_memory-0.6.1.dist-info/METADATA +125 -0
  49. graph_engine_memory-0.6.1.dist-info/RECORD +53 -0
  50. graph_engine_memory-0.6.1.dist-info/WHEEL +5 -0
  51. graph_engine_memory-0.6.1.dist-info/entry_points.txt +3 -0
  52. graph_engine_memory-0.6.1.dist-info/licenses/LICENSE +21 -0
  53. 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'})")