agent-memory-os 0.2.3__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.
- agent_memory_os/__init__.py +17 -0
- agent_memory_os/cache.py +33 -0
- agent_memory_os/candidates.py +39 -0
- agent_memory_os/cli.py +136 -0
- agent_memory_os/client.py +434 -0
- agent_memory_os/context_pack.py +231 -0
- agent_memory_os/db.py +1520 -0
- agent_memory_os/golden_recall.py +220 -0
- agent_memory_os/hermes_importer.py +193 -0
- agent_memory_os/mcp_server.py +85 -0
- agent_memory_os/memory_resonance.py +225 -0
- agent_memory_os/providers/__init__.py +5 -0
- agent_memory_os/providers/turbovec.py +190 -0
- agent_memory_os/schema.py +172 -0
- agent_memory_os/scoring.py +48 -0
- agent_memory_os/shadow_mode.py +261 -0
- agent_memory_os/web_app.py +423 -0
- agent_memory_os/web_ui.py +1063 -0
- agent_memory_os-0.2.3.dist-info/METADATA +148 -0
- agent_memory_os-0.2.3.dist-info/RECORD +24 -0
- agent_memory_os-0.2.3.dist-info/WHEEL +4 -0
- agent_memory_os-0.2.3.dist-info/entry_points.txt +3 -0
- agent_memory_os-0.2.3.dist-info/licenses/LICENSE +201 -0
- agent_memory_os-0.2.3.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .schema import SearchResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class ContextDecision:
|
|
11
|
+
memory_id: str
|
|
12
|
+
selected: bool
|
|
13
|
+
effective_score: float
|
|
14
|
+
token_count: int
|
|
15
|
+
reason: list[str] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class ContextPackReport:
|
|
20
|
+
text: str
|
|
21
|
+
decisions: list[ContextDecision]
|
|
22
|
+
used_tokens: int
|
|
23
|
+
max_tokens: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def approx_tokens(text: str) -> int:
|
|
27
|
+
# Conservative, dependency-free approximation for MVP.
|
|
28
|
+
return max(1, (len(text) + 3) // 4)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_context_pack(results: list[SearchResult], *, max_tokens: int = 1200) -> str:
|
|
32
|
+
return build_context_pack_report(results, max_tokens=max_tokens).text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_context_pack_report(results: list[SearchResult], *, max_tokens: int = 1200) -> ContextPackReport:
|
|
36
|
+
if max_tokens < 32:
|
|
37
|
+
raise ValueError("max_tokens must be >= 32")
|
|
38
|
+
|
|
39
|
+
ranked = sorted(results, key=_arbitration_score, reverse=True)
|
|
40
|
+
conflict_keys = _conflict_keys(results)
|
|
41
|
+
|
|
42
|
+
header = "MEMORY CONTEXT PACK:"
|
|
43
|
+
used = approx_tokens(header + "\n")
|
|
44
|
+
lines: list[str] = [header]
|
|
45
|
+
decisions: list[ContextDecision] = []
|
|
46
|
+
seen_claims: set[tuple[str, str]] = set()
|
|
47
|
+
selected_count = 0
|
|
48
|
+
core_selected_under_pressure = False
|
|
49
|
+
|
|
50
|
+
# Reserved budget for core memories: ensure at least 25% of total budget
|
|
51
|
+
# or 300 tokens (whichever is larger) is reserved for core items.
|
|
52
|
+
core_reserve_limit = max(300, int(max_tokens * 0.25))
|
|
53
|
+
|
|
54
|
+
for result in ranked:
|
|
55
|
+
record = result.record
|
|
56
|
+
score = _arbitration_score(result)
|
|
57
|
+
reason = _base_reasons(result, conflict_keys)
|
|
58
|
+
claim_key = _claim_key(result)
|
|
59
|
+
claim_value = _claim_value(result)
|
|
60
|
+
duplicate_key = (claim_key, claim_value)
|
|
61
|
+
|
|
62
|
+
if duplicate_key in seen_claims:
|
|
63
|
+
reason.append("duplicate_cluster_suppressed")
|
|
64
|
+
decisions.append(
|
|
65
|
+
ContextDecision(
|
|
66
|
+
memory_id=record.id,
|
|
67
|
+
selected=False,
|
|
68
|
+
effective_score=score,
|
|
69
|
+
token_count=_line_tokens(result, selected_count + 1, conflict_keys),
|
|
70
|
+
reason=reason,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
line = _format_line(result, selected_count + 1, conflict_keys)
|
|
76
|
+
cost = approx_tokens(line) + 1
|
|
77
|
+
|
|
78
|
+
# CORE RESERVE LOGIC:
|
|
79
|
+
# If it's a core memory and we haven't exhausted the reserve yet,
|
|
80
|
+
# let it in even if it exceeds the overall max_tokens (up to a reasonable limit).
|
|
81
|
+
is_core = "core_reserved_budget" in reason
|
|
82
|
+
|
|
83
|
+
if core_selected_under_pressure and not is_core:
|
|
84
|
+
reason.append("core_reserve_protected")
|
|
85
|
+
reason.append("budget_exceeded")
|
|
86
|
+
decisions.append(
|
|
87
|
+
ContextDecision(
|
|
88
|
+
memory_id=record.id,
|
|
89
|
+
selected=False,
|
|
90
|
+
effective_score=score,
|
|
91
|
+
token_count=cost,
|
|
92
|
+
reason=reason,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if used + cost > max_tokens:
|
|
98
|
+
if is_core and used < core_reserve_limit:
|
|
99
|
+
# Allow core memory to 'burst' slightly over total budget if within reserve limit
|
|
100
|
+
reason.append("core_reserve_burst")
|
|
101
|
+
else:
|
|
102
|
+
reason.append("budget_exceeded")
|
|
103
|
+
decisions.append(
|
|
104
|
+
ContextDecision(
|
|
105
|
+
memory_id=record.id,
|
|
106
|
+
selected=False,
|
|
107
|
+
effective_score=score,
|
|
108
|
+
token_count=cost,
|
|
109
|
+
reason=reason,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
reason.append("fits_budget")
|
|
115
|
+
selected_count += 1
|
|
116
|
+
if is_core and max_tokens <= core_reserve_limit:
|
|
117
|
+
core_selected_under_pressure = True
|
|
118
|
+
lines.append(line)
|
|
119
|
+
used += cost
|
|
120
|
+
seen_claims.add(duplicate_key)
|
|
121
|
+
decisions.append(
|
|
122
|
+
ContextDecision(
|
|
123
|
+
memory_id=record.id,
|
|
124
|
+
selected=True,
|
|
125
|
+
effective_score=score,
|
|
126
|
+
token_count=cost,
|
|
127
|
+
reason=reason,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return ContextPackReport(
|
|
132
|
+
text="\n".join(lines) + "\n",
|
|
133
|
+
decisions=decisions,
|
|
134
|
+
used_tokens=used,
|
|
135
|
+
max_tokens=max_tokens,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _format_line(result: SearchResult, idx: int, conflict_keys: set[str]) -> str:
|
|
140
|
+
r = result.record
|
|
141
|
+
tags = ",".join(r.tags[:5])
|
|
142
|
+
conflict = " [CONFLICT]" if _claim_key(result) in conflict_keys else ""
|
|
143
|
+
return (
|
|
144
|
+
f"- [{idx}]{conflict} ({r.scope}/{r.type}; importance={r.importance:.2f}; "
|
|
145
|
+
f"confidence={r.confidence:.2f}; score={_arbitration_score(result):.3f}; tags={tags}) {r.content}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _line_tokens(result: SearchResult, idx: int, conflict_keys: set[str]) -> int:
|
|
150
|
+
return approx_tokens(_format_line(result, idx, conflict_keys)) + 1
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _base_reasons(result: SearchResult, conflict_keys: set[str]) -> list[str]:
|
|
154
|
+
r = result.record
|
|
155
|
+
reasons = ["acl_allowed", "not_expired"]
|
|
156
|
+
source = r.source or {}
|
|
157
|
+
tags = set(r.tags)
|
|
158
|
+
if r.confidence >= 0.8:
|
|
159
|
+
reasons.append("high_confidence")
|
|
160
|
+
elif r.confidence < 0.5:
|
|
161
|
+
reasons.append("low_confidence")
|
|
162
|
+
if r.importance >= 0.8:
|
|
163
|
+
reasons.append("high_importance")
|
|
164
|
+
if source.get("authoritative") is True or "authoritative" in tags or "core" in tags:
|
|
165
|
+
reasons.append("authoritative")
|
|
166
|
+
if source.get("permanence") is True or r.pinned:
|
|
167
|
+
reasons.append("permanent")
|
|
168
|
+
if _weight(source) > 8:
|
|
169
|
+
reasons.append("weight_gt_8")
|
|
170
|
+
if {"authoritative", "permanent", "weight_gt_8"}.intersection(reasons):
|
|
171
|
+
reasons.append("core_reserved_budget")
|
|
172
|
+
if _claim_key(result) in conflict_keys:
|
|
173
|
+
reasons.append("conflict_detected")
|
|
174
|
+
if result.reason:
|
|
175
|
+
reasons.append(f"provider:{result.reason}")
|
|
176
|
+
return reasons
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _arbitration_score(result: SearchResult) -> float:
|
|
180
|
+
r = result.record
|
|
181
|
+
source = r.source or {}
|
|
182
|
+
tags = set(r.tags)
|
|
183
|
+
score = float(result.score)
|
|
184
|
+
score += r.confidence * 1.2
|
|
185
|
+
score += r.importance * 1.2
|
|
186
|
+
if source.get("authoritative") is True or "authoritative" in tags or "core" in tags:
|
|
187
|
+
score += 1.0
|
|
188
|
+
if source.get("permanence") is True or r.pinned:
|
|
189
|
+
score += 0.8
|
|
190
|
+
if _weight(source) > 8:
|
|
191
|
+
score += 0.8
|
|
192
|
+
if r.confidence < 0.5:
|
|
193
|
+
score -= 1.0
|
|
194
|
+
return score
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _conflict_keys(results: list[SearchResult]) -> set[str]:
|
|
198
|
+
claims_by_key: dict[str, set[str]] = {}
|
|
199
|
+
for result in results:
|
|
200
|
+
source = result.record.source or {}
|
|
201
|
+
if "claim" not in source:
|
|
202
|
+
continue
|
|
203
|
+
claims_by_key.setdefault(_claim_key(result), set()).add(str(source["claim"]))
|
|
204
|
+
return {key for key, claims in claims_by_key.items() if len(claims) > 1}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _claim_key(result: SearchResult) -> str:
|
|
208
|
+
source = result.record.source or {}
|
|
209
|
+
key = source.get("claim_key")
|
|
210
|
+
if key:
|
|
211
|
+
return str(key)
|
|
212
|
+
return _fingerprint(result.record.content)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _claim_value(result: SearchResult) -> str:
|
|
216
|
+
source = result.record.source or {}
|
|
217
|
+
if "claim" in source:
|
|
218
|
+
return str(source["claim"])
|
|
219
|
+
return _fingerprint(result.record.content)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _fingerprint(text: str) -> str:
|
|
223
|
+
normalized = re.sub(r"\W+", " ", text.casefold()).strip()
|
|
224
|
+
return " ".join(normalized.split())[:160]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _weight(source: dict) -> float:
|
|
228
|
+
try:
|
|
229
|
+
return float(source.get("weight", 0))
|
|
230
|
+
except (TypeError, ValueError):
|
|
231
|
+
return 0.0
|