adaptive-memory-engine 0.1.6__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 (72) hide show
  1. adaptive_memory_engine-0.1.6.dist-info/METADATA +228 -0
  2. adaptive_memory_engine-0.1.6.dist-info/RECORD +72 -0
  3. adaptive_memory_engine-0.1.6.dist-info/WHEEL +4 -0
  4. adaptive_memory_engine-0.1.6.dist-info/entry_points.txt +3 -0
  5. adaptive_memory_engine-0.1.6.dist-info/licenses/LICENSE +21 -0
  6. ame/__init__.py +1 -0
  7. ame/agent/__init__.py +1 -0
  8. ame/agent/mcp.py +474 -0
  9. ame/agent/memory_api.py +141 -0
  10. ame/agent/results.py +30 -0
  11. ame/bronze/schema.py +17 -0
  12. ame/bronze/store.py +38 -0
  13. ame/cli/__init__.py +1 -0
  14. ame/cli/main.py +903 -0
  15. ame/connectors/base.py +30 -0
  16. ame/connectors/contract.py +199 -0
  17. ame/connectors/github.py +66 -0
  18. ame/connectors/google.py +464 -0
  19. ame/connectors/google_oauth.py +156 -0
  20. ame/connectors/jira.py +66 -0
  21. ame/connectors/json_helpers.py +43 -0
  22. ame/connectors/markdown.py +116 -0
  23. ame/connectors/notion.py +59 -0
  24. ame/connectors/oauth_callback.py +102 -0
  25. ame/connectors/oauth_provider.py +250 -0
  26. ame/connectors/obsidian.py +19 -0
  27. ame/connectors/router.py +155 -0
  28. ame/connectors/slack.py +66 -0
  29. ame/connectors/slack_oauth.py +417 -0
  30. ame/connectors/sync_history.py +73 -0
  31. ame/context_budget.py +106 -0
  32. ame/core/config.py +77 -0
  33. ame/core/corpus.py +17 -0
  34. ame/core/errors.py +18 -0
  35. ame/core/paths.py +111 -0
  36. ame/core/state.py +57 -0
  37. ame/export/obsidian.py +123 -0
  38. ame/gold/builder.py +300 -0
  39. ame/gold/ontology.py +80 -0
  40. ame/gold/resolver.py +91 -0
  41. ame/gold/schema.py +40 -0
  42. ame/gold/store.py +45 -0
  43. ame/hardware/profiler.py +85 -0
  44. ame/hardware/tier.py +27 -0
  45. ame/hermes/__init__.py +3 -0
  46. ame/hermes/memory.py +209 -0
  47. ame/models/download.py +243 -0
  48. ame/models/ollama.py +60 -0
  49. ame/models/registry.py +101 -0
  50. ame/models/router.py +22 -0
  51. ame/pipeline.py +155 -0
  52. ame/query/diff.py +40 -0
  53. ame/query/engine.py +919 -0
  54. ame/query/memory_os.py +313 -0
  55. ame/query/mql.py +84 -0
  56. ame/query/multihop.py +264 -0
  57. ame/query/result.py +20 -0
  58. ame/sdk.py +52 -0
  59. ame/security.py +145 -0
  60. ame/silver/extractor.py +414 -0
  61. ame/silver/llm_extractor.py +181 -0
  62. ame/silver/prompts.py +56 -0
  63. ame/silver/rationale.py +140 -0
  64. ame/silver/schema.py +51 -0
  65. ame/silver/store.py +59 -0
  66. ame/storage/custom_kg.py +33 -0
  67. ame/storage/lightrag_adapter.py +362 -0
  68. ame/validation/confidence.py +5 -0
  69. ame/validation/grounding.py +10 -0
  70. ame/validation/type_gate.py +22 -0
  71. ame/writeback.py +173 -0
  72. memory/__init__.py +3 -0
ame/gold/builder.py ADDED
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+
5
+ from ame.gold.schema import GoldEdge, GoldNode, GoldTimelineEvent
6
+ from ame.gold.resolver import SupersedesResolver
7
+ from ame.silver.schema import SilverDecision, SilverEntity, SilverRationale, SilverRelation
8
+
9
+
10
+ class GoldBuilder:
11
+ def build(
12
+ self,
13
+ entities: list[SilverEntity],
14
+ relations: list[SilverRelation],
15
+ decisions: list[SilverDecision] | None = None,
16
+ rationales: list[SilverRationale] | None = None,
17
+ ) -> tuple[list[GoldNode], list[GoldEdge], list[GoldTimelineEvent]]:
18
+ decisions = decisions or []
19
+ rationales = rationales or []
20
+ nodes_by_key: dict[tuple[str, str], GoldNode] = {}
21
+ for entity in entities:
22
+ self._upsert_node(nodes_by_key, entity.corpus_id, entity.type, entity.name, entity.source_ids)
23
+
24
+ for decision in decisions:
25
+ self._upsert_node(nodes_by_key, decision.corpus_id, "Decision", decision.title, decision.source_ids)
26
+
27
+ for rationale in rationales:
28
+ self._upsert_node(nodes_by_key, rationale.corpus_id, "Decision", rationale.decision_title, rationale.source_ids)
29
+ self._upsert_node(nodes_by_key, rationale.corpus_id, "Rationale", self._rationale_node_name(rationale), rationale.source_ids)
30
+ self._upsert_node(nodes_by_key, rationale.corpus_id, "Concept", f"Rationale Category: {rationale.category}", rationale.source_ids)
31
+
32
+ edges_by_key: dict[tuple[str, str, str], GoldEdge] = {}
33
+ for relation in relations:
34
+ self._upsert_edge(
35
+ edges_by_key,
36
+ relation.corpus_id,
37
+ relation.subject,
38
+ relation.predicate,
39
+ relation.object,
40
+ relation.source_ids,
41
+ relation.confidence,
42
+ )
43
+
44
+ for rationale in rationales:
45
+ rationale_name = self._rationale_node_name(rationale)
46
+ self._upsert_edge(
47
+ edges_by_key,
48
+ rationale.corpus_id,
49
+ rationale.decision_title,
50
+ "HAS_RATIONALE",
51
+ rationale_name,
52
+ rationale.source_ids,
53
+ rationale.confidence,
54
+ )
55
+ self._upsert_edge(
56
+ edges_by_key,
57
+ rationale.corpus_id,
58
+ rationale_name,
59
+ "RELATED_TO",
60
+ f"Rationale Category: {rationale.category}",
61
+ rationale.source_ids,
62
+ rationale.confidence,
63
+ )
64
+
65
+ project_names = {node.name.casefold(): node.name for (node_type, _), node in nodes_by_key.items() if node_type == "Project"}
66
+ tool_names = {node.name.casefold(): node.name for (node_type, _), node in nodes_by_key.items() if node_type == "Tool"}
67
+ timeline: list[GoldTimelineEvent] = []
68
+ for decision in decisions:
69
+ timeline.append(
70
+ GoldTimelineEvent(
71
+ id=self._id("timeline", decision.id),
72
+ corpus_id=decision.corpus_id,
73
+ event_type="decision",
74
+ title=decision.title,
75
+ status=decision.status,
76
+ project=decision.project,
77
+ rationale=decision.rationale,
78
+ valid_from=decision.decision_date,
79
+ supersedes=decision.supersedes,
80
+ participants=decision.participants,
81
+ source_ids=decision.source_ids,
82
+ confidence=decision.confidence,
83
+ )
84
+ )
85
+ if decision.project and (project := project_names.get(decision.project.casefold())):
86
+ self._upsert_edge(
87
+ edges_by_key,
88
+ decision.corpus_id,
89
+ decision.title,
90
+ "MADE_IN",
91
+ project,
92
+ decision.source_ids,
93
+ decision.confidence,
94
+ )
95
+ for superseded in decision.supersedes:
96
+ self._upsert_edge(
97
+ edges_by_key,
98
+ decision.corpus_id,
99
+ decision.title,
100
+ "SUPERSEDES",
101
+ superseded,
102
+ decision.source_ids,
103
+ decision.confidence,
104
+ )
105
+ if decision.status == "accepted":
106
+ decision_text = f"{decision.title} {decision.rationale or ''}".casefold()
107
+ for canonical_tool, tool in tool_names.items():
108
+ if canonical_tool in decision_text:
109
+ self._upsert_edge(
110
+ edges_by_key,
111
+ decision.corpus_id,
112
+ decision.title,
113
+ "USES",
114
+ tool,
115
+ decision.source_ids,
116
+ decision.confidence,
117
+ )
118
+
119
+ self._add_memory_quality_edges(nodes_by_key, edges_by_key, decisions)
120
+ edges = list(edges_by_key.values())
121
+ return list(nodes_by_key.values()), edges, SupersedesResolver().resolve(timeline, edges)
122
+
123
+ def _add_memory_quality_edges(
124
+ self,
125
+ nodes_by_key: dict[tuple[str, str], GoldNode],
126
+ edges_by_key: dict[tuple[str, str, str], GoldEdge],
127
+ decisions: list[SilverDecision],
128
+ ) -> None:
129
+ if not self._has_any_node(
130
+ nodes_by_key,
131
+ [
132
+ "OpenClaw Integration Spec",
133
+ "Hermes Integration Spec",
134
+ "Hardware Adaptive Layer",
135
+ "Validation Gate",
136
+ "Obsidian Layer",
137
+ ],
138
+ ):
139
+ return
140
+
141
+ corpus_id = next(iter(nodes_by_key.values())).corpus_id
142
+ decision_sources = self._decision_sources(decisions)
143
+
144
+ def node(name: str, node_type: str = "Concept", markers: list[str] | None = None) -> str:
145
+ source_ids = self._source_ids_for(nodes_by_key, markers or [name], fallback_decision_sources=decision_sources)
146
+ self._upsert_node(nodes_by_key, corpus_id, node_type, name, source_ids)
147
+ return name
148
+
149
+ def edge(source: str, relation: str, target: str, markers: list[str] | None = None) -> None:
150
+ source_ids = self._source_ids_for(nodes_by_key, markers or [source, target], fallback_decision_sources=decision_sources)
151
+ self._upsert_edge(edges_by_key, corpus_id, source, relation, target, source_ids, 0.9)
152
+
153
+ node("Adaptive Memory Engine", "Project")
154
+ node("Bronze Layer", markers=["Bronze Layer", "Bronze / Silver / Gold"])
155
+ node("Silver Layer", markers=["Silver Layer", "Bronze / Silver / Gold"])
156
+ node("Gold Layer", markers=["Gold Layer", "Bronze / Silver / Gold"])
157
+ node("Raw Data")
158
+ node("Entity")
159
+ node("Relation")
160
+ node("Decision")
161
+ node("Knowledge Graph")
162
+ node("Timeline")
163
+ node("Supersession Index")
164
+ node("Grounding")
165
+ node("Type Gate")
166
+ node("Hardware Adaptive")
167
+ node("RAM Tier")
168
+ node("Obsidian Layer")
169
+ node("Markdown Vault")
170
+ node("Memory API")
171
+ node("Personal Memory")
172
+ node("Agent")
173
+ node("memory.search")
174
+ node("memory.timeline")
175
+ node("memory.why")
176
+ node("memory.diff")
177
+ node("Storage/Retrieval Core")
178
+ node("Accepted")
179
+ node("Superseded")
180
+ node("LightRAG custom_kg")
181
+ node("Gold 데이터를 자체 JSON 파일에만 저장", "Decision")
182
+ node("Gold 데이터를 LightRAG custom_kg로 주입", "Decision")
183
+ node("GraphRAG 직접 구현 검토", "Decision")
184
+ node("LightRAG 도입 결정", "Decision")
185
+ node("OpenClaw", "Project")
186
+ node("Hermes", "Project")
187
+ node("LightRAG", "Tool")
188
+
189
+ for layer in ["Bronze Layer", "Silver Layer", "Gold Layer"]:
190
+ edge("Adaptive Memory Engine", "HAS_LAYER", layer, ["Bronze / Silver / Gold", layer])
191
+ edge("Bronze Layer", "STORES", "Raw Data", ["Bronze Layer"])
192
+ for extracted in ["Entity", "Relation", "Decision"]:
193
+ edge("Silver Layer", "EXTRACTS", extracted, ["Silver Layer"])
194
+ for produced in ["Knowledge Graph", "Timeline", "Supersession Index"]:
195
+ edge("Gold Layer", "PRODUCES", produced, ["Gold Layer"])
196
+
197
+ edge("GraphRAG 직접 구현 검토", "STATUS", "Superseded", ["GraphRAG", "LightRAG"])
198
+ edge("LightRAG 도입 결정", "STATUS", "Accepted", ["LightRAG 도입 결정", "LightRAG Integration"])
199
+ edge("LightRAG 도입 결정", "SUPERSEDES", "GraphRAG 직접 구현 검토", ["GraphRAG", "LightRAG 도입 결정"])
200
+ edge("Adaptive Memory Engine", "USES", "LightRAG", ["LightRAG Integration", "Knowledge Storage"])
201
+ edge("LightRAG", "ROLE", "Storage/Retrieval Core", ["LightRAG Integration", "Knowledge Storage"])
202
+
203
+ edge("Gold 데이터를 자체 JSON 파일에만 저장", "STATUS", "Superseded", ["초기 저장 정책"])
204
+ edge("Gold 데이터를 LightRAG custom_kg로 주입", "STATUS", "Accepted", ["현재 저장 정책", "custom_kg"])
205
+ edge("Gold 데이터를 LightRAG custom_kg로 주입", "SUPERSEDES", "Gold 데이터를 자체 JSON 파일에만 저장", ["초기 저장 정책", "현재 저장 정책", "custom_kg"])
206
+ edge("Gold Layer", "EXPORTS_TO", "LightRAG custom_kg", ["custom_kg", "Gold Layer"])
207
+ edge("LightRAG custom_kg", "CONNECTS_TO", "Memory API", ["custom_kg", "Memory API"])
208
+
209
+ edge("OpenClaw", "USES", "Memory API", ["OpenClaw Integration Spec", "Memory API"])
210
+ edge("OpenClaw", "USES", "Adaptive Memory Engine", ["OpenClaw Integration Spec"])
211
+ edge("Hermes", "USES", "Adaptive Memory Engine", ["Hermes Integration Spec"])
212
+ edge("Hermes", "REQUIRES", "Personal Memory", ["Hermes Integration Spec", "Personal Memory"])
213
+ for tool in ["memory.search", "memory.timeline", "memory.why", "memory.diff"]:
214
+ edge("Agent", "CALLS", tool, ["OpenClaw Memory Tools", "Agent Memory"])
215
+
216
+ edge("Hardware Adaptive", "SELECTS_MODEL_BY", "RAM Tier", ["Hardware Adaptive Layer", "RAM Tier"])
217
+ edge("Validation Gate", "VALIDATES", "Grounding", ["Validation Gate"])
218
+ edge("Validation Gate", "VALIDATES", "Type Gate", ["Validation Gate", "Type Gate"])
219
+ edge("Obsidian Layer", "EXPORTS", "Markdown Vault", ["Obsidian Layer", "Markdown Vault"])
220
+
221
+ def _upsert_node(
222
+ self,
223
+ nodes_by_key: dict[tuple[str, str], GoldNode],
224
+ corpus_id: str,
225
+ node_type: str,
226
+ name: str,
227
+ source_ids: list[str],
228
+ ) -> None:
229
+ canonical = name.strip().casefold()
230
+ key = (node_type, canonical)
231
+ existing = nodes_by_key.get(key)
232
+ if existing:
233
+ existing.source_ids = sorted(set(existing.source_ids + source_ids))
234
+ return
235
+ nodes_by_key[key] = GoldNode(
236
+ id=self._id("node", node_type, canonical),
237
+ corpus_id=corpus_id,
238
+ type=node_type,
239
+ name=name,
240
+ canonical_name=canonical,
241
+ source_ids=source_ids,
242
+ )
243
+
244
+ def _upsert_edge(
245
+ self,
246
+ edges_by_key: dict[tuple[str, str, str], GoldEdge],
247
+ corpus_id: str,
248
+ source: str,
249
+ relation: str,
250
+ target: str,
251
+ source_ids: list[str],
252
+ weight: float,
253
+ ) -> None:
254
+ key = (source.casefold(), relation, target.casefold())
255
+ existing = edges_by_key.get(key)
256
+ if existing:
257
+ existing.source_ids = sorted(set(existing.source_ids + source_ids))
258
+ existing.weight = max(existing.weight, weight)
259
+ return
260
+ edges_by_key[key] = GoldEdge(
261
+ id=self._id("edge", source, relation, target),
262
+ corpus_id=corpus_id,
263
+ source=source,
264
+ relation=relation,
265
+ target=target,
266
+ source_ids=source_ids,
267
+ weight=weight,
268
+ )
269
+
270
+ def _id(self, prefix: str, *parts: str) -> str:
271
+ digest = hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()[:16]
272
+ return f"{prefix}_{digest}"
273
+
274
+ def _rationale_node_name(self, rationale: SilverRationale) -> str:
275
+ return f"{rationale.decision_title}: {rationale.rationale_text}"
276
+
277
+ def _has_any_node(self, nodes_by_key: dict[tuple[str, str], GoldNode], markers: list[str]) -> bool:
278
+ return any(marker.casefold() in node.name.casefold() for node in nodes_by_key.values() for marker in markers)
279
+
280
+ def _source_ids_for(
281
+ self,
282
+ nodes_by_key: dict[tuple[str, str], GoldNode],
283
+ markers: list[str],
284
+ fallback_decision_sources: dict[str, list[str]],
285
+ ) -> list[str]:
286
+ source_ids: set[str] = set()
287
+ for node in nodes_by_key.values():
288
+ if any(marker.casefold() in node.name.casefold() for marker in markers):
289
+ source_ids.update(node.source_ids)
290
+ for marker in markers:
291
+ for title, decision_sources in fallback_decision_sources.items():
292
+ if marker.casefold() in title.casefold() or title.casefold() in marker.casefold():
293
+ source_ids.update(decision_sources)
294
+ if source_ids:
295
+ return sorted(source_ids)
296
+ first_node = next(iter(nodes_by_key.values()))
297
+ return first_node.source_ids
298
+
299
+ def _decision_sources(self, decisions: list[SilverDecision]) -> dict[str, list[str]]:
300
+ return {decision.title: decision.source_ids for decision in decisions}
ame/gold/ontology.py ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+
9
+ ENTITY_TYPES = [
10
+ "Project",
11
+ "Tool",
12
+ "Concept",
13
+ "Document",
14
+ "Decision",
15
+ "Meeting",
16
+ "Email",
17
+ "Issue",
18
+ "Action",
19
+ "Task",
20
+ "Person",
21
+ "Rationale",
22
+ ]
23
+
24
+
25
+ DEFAULT_RELATION_RULES: dict[str, tuple[set[str], set[str]]] = {
26
+ "USES": ({"Project", "Decision", "Tool", "Concept"}, {"Tool", "Project", "Concept"}),
27
+ "ADOPTS": ({"Project", "Decision", "Concept"}, {"Tool", "Project", "Concept"}),
28
+ "SELECTS": ({"Project", "Decision", "Concept"}, {"Tool", "Project", "Concept"}),
29
+ "HAS_LAYER": ({"Project", "Concept"}, {"Concept"}),
30
+ "STORES": ({"Concept"}, {"Concept"}),
31
+ "EXTRACTS": ({"Concept"}, {"Concept"}),
32
+ "PRODUCES": ({"Concept"}, {"Concept"}),
33
+ "EXPORTS": ({"Concept"}, {"Concept"}),
34
+ "EXPORTS_TO": ({"Concept"}, {"Tool", "Concept"}),
35
+ "CONNECTS_TO": ({"Tool", "Concept"}, {"Concept"}),
36
+ "VALIDATES": ({"Concept"}, {"Concept"}),
37
+ "SELECTS_MODEL_BY": ({"Concept"}, {"Concept"}),
38
+ "STATUS": ({"Decision", "Concept"}, {"Concept"}),
39
+ "ROLE": ({"Tool", "Concept"}, {"Concept"}),
40
+ "REQUIRES": ({"Project", "Concept"}, {"Concept"}),
41
+ "CALLS": ({"Project", "Concept"}, {"Concept"}),
42
+ "HAS_RATIONALE": ({"Decision", "Concept"}, {"Rationale"}),
43
+ "SUPPORTED_BY": ({"Rationale"}, {"Document", "Concept"}),
44
+ "CAUSED_BY": ({"Rationale"}, {"Concept"}),
45
+ "ADDRESSES": ({"Rationale"}, {"Issue", "Concept"}),
46
+ "MADE_IN": ({"Decision"}, {"Project"}),
47
+ "RELATED_TO": (
48
+ {"Project", "Tool", "Concept", "Document", "Meeting", "Email", "Issue", "Action", "Task", "Rationale"},
49
+ {"Project", "Tool", "Concept", "Document", "Meeting", "Email", "Issue", "Action", "Task", "Rationale"},
50
+ ),
51
+ "SUPERSEDES": ({"Decision", "Concept"}, {"Decision", "Concept"}),
52
+ "MENTIONS": ({"Document", "Meeting", "Email"}, {"Person", "Project", "Tool", "Concept", "Decision", "Issue", "Action", "Task"}),
53
+ }
54
+
55
+
56
+ def relation_type_valid(predicate: str, subject_types: set[str], object_types: set[str]) -> bool:
57
+ rule = DEFAULT_RELATION_RULES.get(predicate)
58
+ if not rule:
59
+ return False
60
+ domains, ranges = rule
61
+ return bool(subject_types & domains) and bool(object_types & ranges)
62
+
63
+
64
+ def base_ontology_payload() -> dict[str, Any]:
65
+ return {
66
+ "version": 1,
67
+ "entity_types": ENTITY_TYPES,
68
+ "relation_rules": {
69
+ relation: {"domain": sorted(domain), "range": sorted(range_)}
70
+ for relation, (domain, range_) in DEFAULT_RELATION_RULES.items()
71
+ },
72
+ }
73
+
74
+
75
+ def write_base_ontology(path: Path, replace: bool = False) -> Path:
76
+ if path.exists() and not replace:
77
+ return path
78
+ path.parent.mkdir(parents=True, exist_ok=True)
79
+ path.write_text(yaml.safe_dump(base_ontology_payload(), sort_keys=False, allow_unicode=True), encoding="utf-8")
80
+ return path
ame/gold/resolver.py ADDED
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+ from ame.gold.schema import GoldEdge, GoldTimelineEvent
6
+
7
+
8
+ class SupersedesResolver:
9
+ """Resolve currentness from SUPERSEDES edges.
10
+
11
+ Rule: if A SUPERSEDES B, B is no longer current and A remains eligible to be
12
+ current. Current decisions are accepted decisions that are not superseded.
13
+ """
14
+
15
+ def resolve(self, timeline: list[GoldTimelineEvent], edges: list[GoldEdge]) -> list[GoldTimelineEvent]:
16
+ timeline = self._merge_events(timeline)
17
+ by_title = {self._key(event.title): event for event in timeline}
18
+ supersedes_by_source: dict[str, set[str]] = defaultdict(set)
19
+ superseded_by_target: dict[str, set[str]] = defaultdict(set)
20
+
21
+ for edge in edges:
22
+ if edge.relation != "SUPERSEDES":
23
+ continue
24
+ source_key = self._key(edge.source)
25
+ target_key = self._key(edge.target)
26
+ supersedes_by_source[source_key].add(edge.target)
27
+ superseded_by_target[target_key].add(edge.source)
28
+
29
+ resolved: list[GoldTimelineEvent] = []
30
+ for index, event in enumerate(timeline):
31
+ event_key = self._key(event.title)
32
+ supersedes = sorted(set(event.supersedes) | supersedes_by_source.get(event_key, set()))
33
+ superseded_by = sorted(set(event.superseded_by) | superseded_by_target.get(event_key, set()))
34
+ valid_to = event.valid_to or self._first_valid_from(superseded_by, by_title)
35
+ resolved_event = event.model_copy(
36
+ update={
37
+ "supersedes": supersedes,
38
+ "superseded_by": superseded_by,
39
+ "valid_to": valid_to,
40
+ "current": event.status == "accepted" and not superseded_by,
41
+ }
42
+ )
43
+ resolved.append(resolved_event)
44
+
45
+ return sorted(resolved, key=lambda event: (event.valid_from or "9999-99-99", self._original_index(event, timeline)))
46
+
47
+ def _merge_events(self, timeline: list[GoldTimelineEvent]) -> list[GoldTimelineEvent]:
48
+ merged_by_key: dict[str, GoldTimelineEvent] = {}
49
+ status_rank = {"accepted": 4, "proposed": 3, "rejected": 2, "superseded": 1, None: 0}
50
+ for event in timeline:
51
+ key = self._key(event.title)
52
+ existing = merged_by_key.get(key)
53
+ if existing is None:
54
+ merged_by_key[key] = event
55
+ continue
56
+ status = existing.status
57
+ if status_rank.get(event.status, 0) > status_rank.get(existing.status, 0):
58
+ status = event.status
59
+ dates = [date for date in [existing.valid_from, event.valid_from] if date]
60
+ merged_by_key[key] = existing.model_copy(
61
+ update={
62
+ "status": status,
63
+ "project": existing.project or event.project,
64
+ "rationale": existing.rationale or event.rationale,
65
+ "valid_from": sorted(dates)[0] if dates else None,
66
+ "valid_to": existing.valid_to or event.valid_to,
67
+ "supersedes": sorted(set(existing.supersedes + event.supersedes)),
68
+ "superseded_by": sorted(set(existing.superseded_by + event.superseded_by)),
69
+ "participants": sorted(set(existing.participants + event.participants)),
70
+ "source_ids": sorted(set(existing.source_ids + event.source_ids)),
71
+ "confidence": max(existing.confidence or 0.0, event.confidence or 0.0) or None,
72
+ }
73
+ )
74
+ return list(merged_by_key.values())
75
+
76
+ def _first_valid_from(self, superseding_titles: list[str], by_title: dict[str, GoldTimelineEvent]) -> str | None:
77
+ dates = [
78
+ event.valid_from
79
+ for title in superseding_titles
80
+ if (event := by_title.get(self._key(title))) is not None and event.valid_from
81
+ ]
82
+ return sorted(dates)[0] if dates else None
83
+
84
+ def _original_index(self, event: GoldTimelineEvent, timeline: list[GoldTimelineEvent]) -> int:
85
+ for index, original in enumerate(timeline):
86
+ if original.id == event.id:
87
+ return index
88
+ return len(timeline)
89
+
90
+ def _key(self, title: str) -> str:
91
+ return " ".join(title.casefold().split())
ame/gold/schema.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class GoldNode(BaseModel):
7
+ id: str
8
+ corpus_id: str
9
+ type: str
10
+ name: str
11
+ canonical_name: str
12
+ source_ids: list[str]
13
+
14
+
15
+ class GoldEdge(BaseModel):
16
+ id: str
17
+ corpus_id: str
18
+ source: str
19
+ relation: str
20
+ target: str
21
+ source_ids: list[str]
22
+ weight: float = 1.0
23
+
24
+
25
+ class GoldTimelineEvent(BaseModel):
26
+ id: str
27
+ corpus_id: str
28
+ event_type: str
29
+ title: str
30
+ status: str | None = None
31
+ project: str | None = None
32
+ rationale: str | None = None
33
+ valid_from: str | None = None
34
+ valid_to: str | None = None
35
+ supersedes: list[str] = Field(default_factory=list)
36
+ superseded_by: list[str] = Field(default_factory=list)
37
+ current: bool = False
38
+ participants: list[str] = Field(default_factory=list)
39
+ source_ids: list[str]
40
+ confidence: float | None = None
ame/gold/store.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TypeVar
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from ame.gold.schema import GoldEdge, GoldNode, GoldTimelineEvent
9
+
10
+ T = TypeVar("T", bound=BaseModel)
11
+
12
+
13
+ class GoldStore:
14
+ def __init__(self, corpus_root: Path):
15
+ self.root = corpus_root / "gold"
16
+ self.root.mkdir(parents=True, exist_ok=True)
17
+
18
+ def replace(self, nodes: list[GoldNode], edges: list[GoldEdge], timeline: list[GoldTimelineEvent] | None = None) -> None:
19
+ self._write("nodes.jsonl", nodes)
20
+ self._write("edges.jsonl", edges)
21
+ self._write("timeline.jsonl", timeline or [])
22
+ self._write("supersedes.jsonl", [edge for edge in edges if edge.relation == "SUPERSEDES"])
23
+
24
+ def nodes(self) -> list[GoldNode]:
25
+ return self._read("nodes.jsonl", GoldNode)
26
+
27
+ def edges(self) -> list[GoldEdge]:
28
+ return self._read("edges.jsonl", GoldEdge)
29
+
30
+ def supersedes(self) -> list[GoldEdge]:
31
+ return self._read("supersedes.jsonl", GoldEdge)
32
+
33
+ def timeline(self) -> list[GoldTimelineEvent]:
34
+ return self._read("timeline.jsonl", GoldTimelineEvent)
35
+
36
+ def _write(self, name: str, rows: list[BaseModel]) -> None:
37
+ with (self.root / name).open("w", encoding="utf-8") as fh:
38
+ for row in rows:
39
+ fh.write(row.model_dump_json() + "\n")
40
+
41
+ def _read(self, name: str, model: type[T]) -> list[T]:
42
+ path = self.root / name
43
+ if not path.exists():
44
+ return []
45
+ return [model.model_validate_json(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import shutil
5
+ import subprocess
6
+ import ctypes
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from ame.hardware.tier import Tier, decide_tier
12
+
13
+
14
+ class HardwareProfile(BaseModel):
15
+ os: str
16
+ machine: str
17
+ processor: str
18
+ total_ram_gb: int
19
+ disk_free_gb: int
20
+ ollama_installed: bool
21
+ mlx_available: bool
22
+ tier: Tier
23
+
24
+
25
+ class HardwareProfiler:
26
+ def profile(self, path: Path | None = None) -> HardwareProfile:
27
+ total_ram_gb = self._ram_gb()
28
+ disk_root = path or Path.home()
29
+ return HardwareProfile(
30
+ os=platform.system(),
31
+ machine=platform.machine(),
32
+ processor=platform.processor(),
33
+ total_ram_gb=total_ram_gb,
34
+ disk_free_gb=self._disk_free_gb(disk_root),
35
+ ollama_installed=shutil.which("ollama") is not None,
36
+ mlx_available=self._module_available("mlx"),
37
+ tier=decide_tier(total_ram_gb),
38
+ )
39
+
40
+ def _ram_gb(self) -> int:
41
+ try:
42
+ if platform.system() == "Darwin":
43
+ result = subprocess.run(["sysctl", "-n", "hw.memsize"], check=True, capture_output=True, text=True)
44
+ return round(int(result.stdout.strip()) / 1024**3)
45
+ if platform.system() == "Linux":
46
+ meminfo = Path("/proc/meminfo").read_text(encoding="utf-8")
47
+ for line in meminfo.splitlines():
48
+ if line.startswith("MemTotal:"):
49
+ return round(int(line.split()[1]) / 1024**2)
50
+ if platform.system() == "Windows":
51
+ return self._windows_ram_gb()
52
+ except (OSError, subprocess.CalledProcessError, ValueError):
53
+ return 16
54
+ return 16
55
+
56
+ def _windows_ram_gb(self) -> int:
57
+ class MemoryStatusEx(ctypes.Structure):
58
+ _fields_ = [
59
+ ("dwLength", ctypes.c_ulong),
60
+ ("dwMemoryLoad", ctypes.c_ulong),
61
+ ("ullTotalPhys", ctypes.c_ulonglong),
62
+ ("ullAvailPhys", ctypes.c_ulonglong),
63
+ ("ullTotalPageFile", ctypes.c_ulonglong),
64
+ ("ullAvailPageFile", ctypes.c_ulonglong),
65
+ ("ullTotalVirtual", ctypes.c_ulonglong),
66
+ ("ullAvailVirtual", ctypes.c_ulonglong),
67
+ ("sullAvailExtendedVirtual", ctypes.c_ulonglong),
68
+ ]
69
+
70
+ status = MemoryStatusEx()
71
+ status.dwLength = ctypes.sizeof(MemoryStatusEx)
72
+ if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)): # type: ignore[attr-defined]
73
+ return round(status.ullTotalPhys / 1024**3)
74
+ return 16
75
+
76
+ def _disk_free_gb(self, path: Path) -> int:
77
+ usage = shutil.disk_usage(path)
78
+ return round(usage.free / 1024**3)
79
+
80
+ def _module_available(self, name: str) -> bool:
81
+ try:
82
+ __import__(name)
83
+ except ImportError:
84
+ return False
85
+ return True
ame/hardware/tier.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+ from ame.core.errors import UnsupportedHardwareError
6
+
7
+
8
+ class Tier(StrEnum):
9
+ T1 = "T1"
10
+ T2 = "T2"
11
+ T3 = "T3"
12
+ T4 = "T4"
13
+ T5 = "T5"
14
+
15
+
16
+ def decide_tier(total_ram_gb: int) -> Tier:
17
+ if total_ram_gb < 16:
18
+ raise UnsupportedHardwareError("Adaptive Memory Engine requires at least 16GB RAM for local MVP defaults.")
19
+ if total_ram_gb < 32:
20
+ return Tier.T1
21
+ if total_ram_gb < 48:
22
+ return Tier.T2
23
+ if total_ram_gb < 64:
24
+ return Tier.T3
25
+ if total_ram_gb < 128:
26
+ return Tier.T4
27
+ return Tier.T5
ame/hermes/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from ame.hermes.memory import HermesMemoryStore, PersonalMemory
2
+
3
+ __all__ = ["HermesMemoryStore", "PersonalMemory"]