alma-memory 0.5.1__py3-none-any.whl → 0.7.0__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.
- alma/__init__.py +296 -226
- alma/compression/__init__.py +33 -0
- alma/compression/pipeline.py +980 -0
- alma/confidence/__init__.py +47 -47
- alma/confidence/engine.py +540 -540
- alma/confidence/types.py +351 -351
- alma/config/loader.py +157 -157
- alma/consolidation/__init__.py +23 -23
- alma/consolidation/engine.py +678 -678
- alma/consolidation/prompts.py +84 -84
- alma/core.py +1189 -430
- alma/domains/__init__.py +30 -30
- alma/domains/factory.py +359 -359
- alma/domains/schemas.py +448 -448
- alma/domains/types.py +272 -272
- alma/events/__init__.py +75 -75
- alma/events/emitter.py +285 -284
- alma/events/storage_mixin.py +246 -246
- alma/events/types.py +126 -126
- alma/events/webhook.py +425 -425
- alma/exceptions.py +49 -49
- alma/extraction/__init__.py +31 -31
- alma/extraction/auto_learner.py +265 -265
- alma/extraction/extractor.py +420 -420
- alma/graph/__init__.py +106 -106
- alma/graph/backends/__init__.py +32 -32
- alma/graph/backends/kuzu.py +624 -624
- alma/graph/backends/memgraph.py +432 -432
- alma/graph/backends/memory.py +236 -236
- alma/graph/backends/neo4j.py +417 -417
- alma/graph/base.py +159 -159
- alma/graph/extraction.py +198 -198
- alma/graph/store.py +860 -860
- alma/harness/__init__.py +35 -35
- alma/harness/base.py +386 -386
- alma/harness/domains.py +705 -705
- alma/initializer/__init__.py +37 -37
- alma/initializer/initializer.py +418 -418
- alma/initializer/types.py +250 -250
- alma/integration/__init__.py +62 -62
- alma/integration/claude_agents.py +444 -444
- alma/integration/helena.py +423 -423
- alma/integration/victor.py +471 -471
- alma/learning/__init__.py +101 -86
- alma/learning/decay.py +878 -0
- alma/learning/forgetting.py +1446 -1446
- alma/learning/heuristic_extractor.py +390 -390
- alma/learning/protocols.py +374 -374
- alma/learning/validation.py +346 -346
- alma/mcp/__init__.py +123 -45
- alma/mcp/__main__.py +156 -156
- alma/mcp/resources.py +122 -122
- alma/mcp/server.py +955 -591
- alma/mcp/tools.py +3254 -509
- alma/observability/__init__.py +91 -84
- alma/observability/config.py +302 -302
- alma/observability/guidelines.py +170 -0
- alma/observability/logging.py +424 -424
- alma/observability/metrics.py +583 -583
- alma/observability/tracing.py +440 -440
- alma/progress/__init__.py +21 -21
- alma/progress/tracker.py +607 -607
- alma/progress/types.py +250 -250
- alma/retrieval/__init__.py +134 -53
- alma/retrieval/budget.py +525 -0
- alma/retrieval/cache.py +1304 -1061
- alma/retrieval/embeddings.py +202 -202
- alma/retrieval/engine.py +850 -427
- alma/retrieval/modes.py +365 -0
- alma/retrieval/progressive.py +560 -0
- alma/retrieval/scoring.py +344 -344
- alma/retrieval/trust_scoring.py +637 -0
- alma/retrieval/verification.py +797 -0
- alma/session/__init__.py +19 -19
- alma/session/manager.py +442 -399
- alma/session/types.py +288 -288
- alma/storage/__init__.py +101 -90
- alma/storage/archive.py +233 -0
- alma/storage/azure_cosmos.py +1259 -1259
- alma/storage/base.py +1083 -583
- alma/storage/chroma.py +1443 -1443
- alma/storage/constants.py +103 -103
- alma/storage/file_based.py +614 -614
- alma/storage/migrations/__init__.py +21 -21
- alma/storage/migrations/base.py +321 -321
- alma/storage/migrations/runner.py +323 -323
- alma/storage/migrations/version_stores.py +337 -337
- alma/storage/migrations/versions/__init__.py +11 -11
- alma/storage/migrations/versions/v1_0_0.py +373 -373
- alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
- alma/storage/pinecone.py +1080 -1080
- alma/storage/postgresql.py +1948 -1559
- alma/storage/qdrant.py +1306 -1306
- alma/storage/sqlite_local.py +3041 -1457
- alma/testing/__init__.py +46 -46
- alma/testing/factories.py +301 -301
- alma/testing/mocks.py +389 -389
- alma/types.py +292 -264
- alma/utils/__init__.py +19 -0
- alma/utils/tokenizer.py +521 -0
- alma/workflow/__init__.py +83 -0
- alma/workflow/artifacts.py +170 -0
- alma/workflow/checkpoint.py +311 -0
- alma/workflow/context.py +228 -0
- alma/workflow/outcomes.py +189 -0
- alma/workflow/reducers.py +393 -0
- {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/METADATA +210 -72
- alma_memory-0.7.0.dist-info/RECORD +112 -0
- alma_memory-0.5.1.dist-info/RECORD +0 -93
- {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Progressive Disclosure.
|
|
3
|
+
|
|
4
|
+
Implements summary → detail retrieval pattern to optimize context usage.
|
|
5
|
+
Based on context engineering principle: "Must know exists" before "must see."
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Summary extraction for memory items
|
|
9
|
+
- Lazy-loading of full details on demand
|
|
10
|
+
- Reference IDs for fetch-on-demand pattern
|
|
11
|
+
- Tiered disclosure (summary → key details → full content)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from alma.types import (
|
|
20
|
+
AntiPattern,
|
|
21
|
+
DomainKnowledge,
|
|
22
|
+
Heuristic,
|
|
23
|
+
Outcome,
|
|
24
|
+
UserPreference,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DisclosureLevel(Enum):
|
|
31
|
+
"""Levels of detail for progressive disclosure."""
|
|
32
|
+
|
|
33
|
+
REFERENCE = 1 # Just ID and type - agent knows it exists
|
|
34
|
+
SUMMARY = 2 # Brief summary - enough to decide if needed
|
|
35
|
+
KEY_DETAILS = 3 # Important fields only
|
|
36
|
+
FULL = 4 # Complete memory with all details
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MemorySummary:
|
|
41
|
+
"""Compact summary of a memory item."""
|
|
42
|
+
|
|
43
|
+
id: str
|
|
44
|
+
memory_type: str
|
|
45
|
+
summary: str
|
|
46
|
+
relevance_hint: str # Why this might be relevant
|
|
47
|
+
estimated_tokens: int
|
|
48
|
+
disclosure_level: DisclosureLevel = DisclosureLevel.SUMMARY
|
|
49
|
+
|
|
50
|
+
# Optional key details (for KEY_DETAILS level)
|
|
51
|
+
key_fields: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
# Reference to full item (for lazy loading)
|
|
54
|
+
_full_item: Optional[Any] = field(default=None, repr=False)
|
|
55
|
+
|
|
56
|
+
def get_full(self) -> Optional[Any]:
|
|
57
|
+
"""Get full item if available."""
|
|
58
|
+
return self._full_item
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ProgressiveSlice:
|
|
63
|
+
"""
|
|
64
|
+
Memory slice with progressive disclosure support.
|
|
65
|
+
|
|
66
|
+
Contains summaries by default, with ability to fetch full details
|
|
67
|
+
on demand.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Summaries by type
|
|
71
|
+
heuristic_summaries: List[MemorySummary] = field(default_factory=list)
|
|
72
|
+
outcome_summaries: List[MemorySummary] = field(default_factory=list)
|
|
73
|
+
knowledge_summaries: List[MemorySummary] = field(default_factory=list)
|
|
74
|
+
anti_pattern_summaries: List[MemorySummary] = field(default_factory=list)
|
|
75
|
+
preference_summaries: List[MemorySummary] = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
# Full items (populated on demand)
|
|
78
|
+
_full_items: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
79
|
+
|
|
80
|
+
# Metadata
|
|
81
|
+
query: str = ""
|
|
82
|
+
agent: str = ""
|
|
83
|
+
total_available: int = 0
|
|
84
|
+
summaries_included: int = 0
|
|
85
|
+
estimated_summary_tokens: int = 0
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def all_summaries(self) -> List[MemorySummary]:
|
|
89
|
+
"""Get all summaries across types."""
|
|
90
|
+
return (
|
|
91
|
+
self.heuristic_summaries
|
|
92
|
+
+ self.outcome_summaries
|
|
93
|
+
+ self.knowledge_summaries
|
|
94
|
+
+ self.anti_pattern_summaries
|
|
95
|
+
+ self.preference_summaries
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_full_item(self, memory_id: str) -> Optional[Any]:
|
|
99
|
+
"""Get full item by ID (lazy load if needed)."""
|
|
100
|
+
# Check cache first
|
|
101
|
+
if memory_id in self._full_items:
|
|
102
|
+
return self._full_items[memory_id]
|
|
103
|
+
|
|
104
|
+
# Check summaries for attached full items
|
|
105
|
+
for summary in self.all_summaries:
|
|
106
|
+
if summary.id == memory_id and summary._full_item:
|
|
107
|
+
self._full_items[memory_id] = summary._full_item
|
|
108
|
+
return summary._full_item
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def get_ids_by_type(self, memory_type: str) -> List[str]:
|
|
113
|
+
"""Get all IDs for a memory type."""
|
|
114
|
+
type_map = {
|
|
115
|
+
"heuristic": self.heuristic_summaries,
|
|
116
|
+
"outcome": self.outcome_summaries,
|
|
117
|
+
"domain_knowledge": self.knowledge_summaries,
|
|
118
|
+
"anti_pattern": self.anti_pattern_summaries,
|
|
119
|
+
"preference": self.preference_summaries,
|
|
120
|
+
}
|
|
121
|
+
summaries = type_map.get(memory_type, [])
|
|
122
|
+
return [s.id for s in summaries]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SummaryExtractor:
|
|
126
|
+
"""Extracts summaries from memory items."""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
max_summary_length: int = 100,
|
|
131
|
+
chars_per_token: int = 4,
|
|
132
|
+
):
|
|
133
|
+
self.max_summary_length = max_summary_length
|
|
134
|
+
self.chars_per_token = chars_per_token
|
|
135
|
+
|
|
136
|
+
def extract_heuristic_summary(
|
|
137
|
+
self,
|
|
138
|
+
h: Heuristic,
|
|
139
|
+
level: DisclosureLevel = DisclosureLevel.SUMMARY,
|
|
140
|
+
) -> MemorySummary:
|
|
141
|
+
"""Extract summary from a heuristic."""
|
|
142
|
+
if level == DisclosureLevel.REFERENCE:
|
|
143
|
+
summary = f"Heuristic: {h.condition[:30]}..."
|
|
144
|
+
relevance = "Learned pattern"
|
|
145
|
+
elif level == DisclosureLevel.SUMMARY:
|
|
146
|
+
summary = self._truncate(
|
|
147
|
+
f"When {h.condition}, {h.strategy}",
|
|
148
|
+
self.max_summary_length,
|
|
149
|
+
)
|
|
150
|
+
relevance = (
|
|
151
|
+
f"Success rate: {h.success_rate:.0%}, Confidence: {h.confidence:.0%}"
|
|
152
|
+
)
|
|
153
|
+
else: # KEY_DETAILS or FULL
|
|
154
|
+
summary = f"When {h.condition}, {h.strategy}"
|
|
155
|
+
relevance = f"Used {h.occurrence_count}x, {h.success_rate:.0%} success"
|
|
156
|
+
|
|
157
|
+
key_fields = {}
|
|
158
|
+
if level >= DisclosureLevel.KEY_DETAILS:
|
|
159
|
+
key_fields = {
|
|
160
|
+
"confidence": h.confidence,
|
|
161
|
+
"success_rate": h.success_rate,
|
|
162
|
+
"occurrence_count": h.occurrence_count,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return MemorySummary(
|
|
166
|
+
id=h.id,
|
|
167
|
+
memory_type="heuristic",
|
|
168
|
+
summary=summary,
|
|
169
|
+
relevance_hint=relevance,
|
|
170
|
+
estimated_tokens=len(summary) // self.chars_per_token + 10,
|
|
171
|
+
disclosure_level=level,
|
|
172
|
+
key_fields=key_fields,
|
|
173
|
+
_full_item=h if level == DisclosureLevel.FULL else None,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def extract_outcome_summary(
|
|
177
|
+
self,
|
|
178
|
+
o: Outcome,
|
|
179
|
+
level: DisclosureLevel = DisclosureLevel.SUMMARY,
|
|
180
|
+
) -> MemorySummary:
|
|
181
|
+
"""Extract summary from an outcome."""
|
|
182
|
+
status = "Success" if o.success else "Failed"
|
|
183
|
+
|
|
184
|
+
if level == DisclosureLevel.REFERENCE:
|
|
185
|
+
summary = f"Outcome: {o.task_type} ({status})"
|
|
186
|
+
relevance = f"{o.task_type} task"
|
|
187
|
+
elif level == DisclosureLevel.SUMMARY:
|
|
188
|
+
summary = self._truncate(
|
|
189
|
+
f"{status}: {o.task_description} using {o.strategy_used}",
|
|
190
|
+
self.max_summary_length,
|
|
191
|
+
)
|
|
192
|
+
relevance = f"{o.task_type} - {status}"
|
|
193
|
+
else:
|
|
194
|
+
summary = f"{status}: {o.task_description}\nStrategy: {o.strategy_used}"
|
|
195
|
+
if o.error_message:
|
|
196
|
+
summary += f"\nError: {o.error_message}"
|
|
197
|
+
relevance = f"{o.task_type} task outcome"
|
|
198
|
+
|
|
199
|
+
key_fields = {}
|
|
200
|
+
if level >= DisclosureLevel.KEY_DETAILS:
|
|
201
|
+
key_fields = {
|
|
202
|
+
"success": o.success,
|
|
203
|
+
"task_type": o.task_type,
|
|
204
|
+
"strategy_used": o.strategy_used,
|
|
205
|
+
}
|
|
206
|
+
if o.error_message:
|
|
207
|
+
key_fields["error"] = o.error_message[:100]
|
|
208
|
+
|
|
209
|
+
return MemorySummary(
|
|
210
|
+
id=o.id,
|
|
211
|
+
memory_type="outcome",
|
|
212
|
+
summary=summary,
|
|
213
|
+
relevance_hint=relevance,
|
|
214
|
+
estimated_tokens=len(summary) // self.chars_per_token + 10,
|
|
215
|
+
disclosure_level=level,
|
|
216
|
+
key_fields=key_fields,
|
|
217
|
+
_full_item=o if level == DisclosureLevel.FULL else None,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def extract_knowledge_summary(
|
|
221
|
+
self,
|
|
222
|
+
k: DomainKnowledge,
|
|
223
|
+
level: DisclosureLevel = DisclosureLevel.SUMMARY,
|
|
224
|
+
) -> MemorySummary:
|
|
225
|
+
"""Extract summary from domain knowledge."""
|
|
226
|
+
fact_str = str(k.fact)[:100] if k.fact else "N/A"
|
|
227
|
+
|
|
228
|
+
if level == DisclosureLevel.REFERENCE:
|
|
229
|
+
summary = f"Knowledge: {k.domain}"
|
|
230
|
+
relevance = f"Domain: {k.domain}"
|
|
231
|
+
elif level == DisclosureLevel.SUMMARY:
|
|
232
|
+
summary = self._truncate(
|
|
233
|
+
f"[{k.domain}] {fact_str}",
|
|
234
|
+
self.max_summary_length,
|
|
235
|
+
)
|
|
236
|
+
relevance = f"Confidence: {k.confidence:.0%}"
|
|
237
|
+
else:
|
|
238
|
+
summary = f"Domain: {k.domain}\nFact: {fact_str}\nSource: {k.source}"
|
|
239
|
+
relevance = f"Domain knowledge, {k.confidence:.0%} confidence"
|
|
240
|
+
|
|
241
|
+
key_fields = {}
|
|
242
|
+
if level >= DisclosureLevel.KEY_DETAILS:
|
|
243
|
+
key_fields = {
|
|
244
|
+
"domain": k.domain,
|
|
245
|
+
"confidence": k.confidence,
|
|
246
|
+
"source": k.source,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return MemorySummary(
|
|
250
|
+
id=k.id,
|
|
251
|
+
memory_type="domain_knowledge",
|
|
252
|
+
summary=summary,
|
|
253
|
+
relevance_hint=relevance,
|
|
254
|
+
estimated_tokens=len(summary) // self.chars_per_token + 10,
|
|
255
|
+
disclosure_level=level,
|
|
256
|
+
key_fields=key_fields,
|
|
257
|
+
_full_item=k if level == DisclosureLevel.FULL else None,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def extract_anti_pattern_summary(
|
|
261
|
+
self,
|
|
262
|
+
ap: AntiPattern,
|
|
263
|
+
level: DisclosureLevel = DisclosureLevel.SUMMARY,
|
|
264
|
+
) -> MemorySummary:
|
|
265
|
+
"""Extract summary from an anti-pattern."""
|
|
266
|
+
if level == DisclosureLevel.REFERENCE:
|
|
267
|
+
summary = f"Warning: {ap.pattern[:30]}..."
|
|
268
|
+
relevance = "Known pitfall"
|
|
269
|
+
elif level == DisclosureLevel.SUMMARY:
|
|
270
|
+
summary = self._truncate(
|
|
271
|
+
f"Avoid: {ap.pattern}. {ap.why_bad}",
|
|
272
|
+
self.max_summary_length,
|
|
273
|
+
)
|
|
274
|
+
relevance = f"Seen {ap.occurrence_count}x"
|
|
275
|
+
else:
|
|
276
|
+
summary = (
|
|
277
|
+
f"Pattern to avoid: {ap.pattern}\n"
|
|
278
|
+
f"Why bad: {ap.why_bad}\n"
|
|
279
|
+
f"Instead: {ap.better_alternative}"
|
|
280
|
+
)
|
|
281
|
+
relevance = f"Occurred {ap.occurrence_count}x"
|
|
282
|
+
|
|
283
|
+
key_fields = {}
|
|
284
|
+
if level >= DisclosureLevel.KEY_DETAILS:
|
|
285
|
+
key_fields = {
|
|
286
|
+
"pattern": ap.pattern,
|
|
287
|
+
"occurrence_count": ap.occurrence_count,
|
|
288
|
+
"alternative": ap.better_alternative,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return MemorySummary(
|
|
292
|
+
id=ap.id,
|
|
293
|
+
memory_type="anti_pattern",
|
|
294
|
+
summary=summary,
|
|
295
|
+
relevance_hint=relevance,
|
|
296
|
+
estimated_tokens=len(summary) // self.chars_per_token + 10,
|
|
297
|
+
disclosure_level=level,
|
|
298
|
+
key_fields=key_fields,
|
|
299
|
+
_full_item=ap if level == DisclosureLevel.FULL else None,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def extract_preference_summary(
|
|
303
|
+
self,
|
|
304
|
+
p: UserPreference,
|
|
305
|
+
level: DisclosureLevel = DisclosureLevel.SUMMARY,
|
|
306
|
+
) -> MemorySummary:
|
|
307
|
+
"""Extract summary from a user preference."""
|
|
308
|
+
if level == DisclosureLevel.REFERENCE:
|
|
309
|
+
summary = f"Preference: {p.category}"
|
|
310
|
+
relevance = "User constraint"
|
|
311
|
+
elif level == DisclosureLevel.SUMMARY:
|
|
312
|
+
summary = self._truncate(
|
|
313
|
+
f"[{p.category}] {p.preference}",
|
|
314
|
+
self.max_summary_length,
|
|
315
|
+
)
|
|
316
|
+
relevance = f"Priority: {p.priority}"
|
|
317
|
+
else:
|
|
318
|
+
summary = (
|
|
319
|
+
f"Category: {p.category}\n"
|
|
320
|
+
f"Preference: {p.preference}\n"
|
|
321
|
+
f"Context: {p.context or 'General'}"
|
|
322
|
+
)
|
|
323
|
+
relevance = f"User preference, priority {p.priority}"
|
|
324
|
+
|
|
325
|
+
key_fields = {}
|
|
326
|
+
if level >= DisclosureLevel.KEY_DETAILS:
|
|
327
|
+
key_fields = {
|
|
328
|
+
"category": p.category,
|
|
329
|
+
"priority": p.priority,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return MemorySummary(
|
|
333
|
+
id=p.id,
|
|
334
|
+
memory_type="preference",
|
|
335
|
+
summary=summary,
|
|
336
|
+
relevance_hint=relevance,
|
|
337
|
+
estimated_tokens=len(summary) // self.chars_per_token + 10,
|
|
338
|
+
disclosure_level=level,
|
|
339
|
+
key_fields=key_fields,
|
|
340
|
+
_full_item=p if level == DisclosureLevel.FULL else None,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _truncate(self, text: str, max_length: int) -> str:
|
|
344
|
+
"""Truncate text to max length."""
|
|
345
|
+
if len(text) <= max_length:
|
|
346
|
+
return text
|
|
347
|
+
return text[: max_length - 3] + "..."
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class ProgressiveRetrieval:
|
|
351
|
+
"""
|
|
352
|
+
Retrieval with progressive disclosure support.
|
|
353
|
+
|
|
354
|
+
Returns summaries first, allowing agents to request full details
|
|
355
|
+
only for items they need.
|
|
356
|
+
|
|
357
|
+
Usage:
|
|
358
|
+
progressive = ProgressiveRetrieval(retrieval_engine, storage)
|
|
359
|
+
|
|
360
|
+
# Get summaries
|
|
361
|
+
slice = progressive.retrieve_summaries(query, agent, project_id)
|
|
362
|
+
|
|
363
|
+
# Get full details for specific items
|
|
364
|
+
full_heuristic = progressive.get_full_item("heuristic-123", "heuristic")
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def __init__(
|
|
368
|
+
self,
|
|
369
|
+
retrieval_engine: Any, # RetrievalEngine
|
|
370
|
+
storage: Any, # StorageBackend
|
|
371
|
+
default_level: DisclosureLevel = DisclosureLevel.SUMMARY,
|
|
372
|
+
):
|
|
373
|
+
self.engine = retrieval_engine
|
|
374
|
+
self.storage = storage
|
|
375
|
+
self.default_level = default_level
|
|
376
|
+
self.extractor = SummaryExtractor()
|
|
377
|
+
|
|
378
|
+
# Cache for fetched full items
|
|
379
|
+
self._item_cache: Dict[str, Any] = {}
|
|
380
|
+
|
|
381
|
+
def retrieve_summaries(
|
|
382
|
+
self,
|
|
383
|
+
query: str,
|
|
384
|
+
agent: str,
|
|
385
|
+
project_id: str,
|
|
386
|
+
user_id: Optional[str] = None,
|
|
387
|
+
top_k: int = 10,
|
|
388
|
+
level: Optional[DisclosureLevel] = None,
|
|
389
|
+
**kwargs,
|
|
390
|
+
) -> ProgressiveSlice:
|
|
391
|
+
"""
|
|
392
|
+
Retrieve memory summaries (not full content).
|
|
393
|
+
|
|
394
|
+
Returns compact summaries that fit more items in context.
|
|
395
|
+
Use get_full_item() to fetch complete details when needed.
|
|
396
|
+
"""
|
|
397
|
+
level = level or self.default_level
|
|
398
|
+
|
|
399
|
+
# Get full results from engine
|
|
400
|
+
raw_slice = self.engine.retrieve(
|
|
401
|
+
query=query,
|
|
402
|
+
agent=agent,
|
|
403
|
+
project_id=project_id,
|
|
404
|
+
user_id=user_id,
|
|
405
|
+
top_k=top_k,
|
|
406
|
+
**kwargs,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Extract summaries
|
|
410
|
+
heuristic_summaries = [
|
|
411
|
+
self.extractor.extract_heuristic_summary(h, level)
|
|
412
|
+
for h in raw_slice.heuristics
|
|
413
|
+
]
|
|
414
|
+
outcome_summaries = [
|
|
415
|
+
self.extractor.extract_outcome_summary(o, level) for o in raw_slice.outcomes
|
|
416
|
+
]
|
|
417
|
+
knowledge_summaries = [
|
|
418
|
+
self.extractor.extract_knowledge_summary(k, level)
|
|
419
|
+
for k in raw_slice.domain_knowledge
|
|
420
|
+
]
|
|
421
|
+
anti_pattern_summaries = [
|
|
422
|
+
self.extractor.extract_anti_pattern_summary(ap, level)
|
|
423
|
+
for ap in raw_slice.anti_patterns
|
|
424
|
+
]
|
|
425
|
+
preference_summaries = [
|
|
426
|
+
self.extractor.extract_preference_summary(p, level)
|
|
427
|
+
for p in raw_slice.preferences
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
# Calculate totals
|
|
431
|
+
all_summaries = (
|
|
432
|
+
heuristic_summaries
|
|
433
|
+
+ outcome_summaries
|
|
434
|
+
+ knowledge_summaries
|
|
435
|
+
+ anti_pattern_summaries
|
|
436
|
+
+ preference_summaries
|
|
437
|
+
)
|
|
438
|
+
total_tokens = sum(s.estimated_tokens for s in all_summaries)
|
|
439
|
+
|
|
440
|
+
# Cache full items for lazy loading
|
|
441
|
+
for h in raw_slice.heuristics:
|
|
442
|
+
self._item_cache[h.id] = h
|
|
443
|
+
for o in raw_slice.outcomes:
|
|
444
|
+
self._item_cache[o.id] = o
|
|
445
|
+
for k in raw_slice.domain_knowledge:
|
|
446
|
+
self._item_cache[k.id] = k
|
|
447
|
+
for ap in raw_slice.anti_patterns:
|
|
448
|
+
self._item_cache[ap.id] = ap
|
|
449
|
+
for p in raw_slice.preferences:
|
|
450
|
+
self._item_cache[p.id] = p
|
|
451
|
+
|
|
452
|
+
return ProgressiveSlice(
|
|
453
|
+
heuristic_summaries=heuristic_summaries,
|
|
454
|
+
outcome_summaries=outcome_summaries,
|
|
455
|
+
knowledge_summaries=knowledge_summaries,
|
|
456
|
+
anti_pattern_summaries=anti_pattern_summaries,
|
|
457
|
+
preference_summaries=preference_summaries,
|
|
458
|
+
query=query,
|
|
459
|
+
agent=agent,
|
|
460
|
+
total_available=raw_slice.total_items,
|
|
461
|
+
summaries_included=len(all_summaries),
|
|
462
|
+
estimated_summary_tokens=total_tokens,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def get_full_item(
|
|
466
|
+
self,
|
|
467
|
+
memory_id: str,
|
|
468
|
+
memory_type: str,
|
|
469
|
+
) -> Optional[Any]:
|
|
470
|
+
"""
|
|
471
|
+
Get full details for a specific memory item.
|
|
472
|
+
|
|
473
|
+
This is the "fetch on demand" part of progressive disclosure.
|
|
474
|
+
"""
|
|
475
|
+
# Check cache first
|
|
476
|
+
if memory_id in self._item_cache:
|
|
477
|
+
logger.debug(f"Cache hit for {memory_type}:{memory_id}")
|
|
478
|
+
return self._item_cache[memory_id]
|
|
479
|
+
|
|
480
|
+
# Fetch from storage
|
|
481
|
+
logger.debug(f"Fetching {memory_type}:{memory_id} from storage")
|
|
482
|
+
|
|
483
|
+
item = None
|
|
484
|
+
if memory_type == "heuristic":
|
|
485
|
+
item = self.storage.get_heuristic_by_id(memory_id)
|
|
486
|
+
elif memory_type == "outcome":
|
|
487
|
+
item = self.storage.get_outcome_by_id(memory_id)
|
|
488
|
+
elif memory_type == "domain_knowledge":
|
|
489
|
+
item = self.storage.get_domain_knowledge_by_id(memory_id)
|
|
490
|
+
elif memory_type == "anti_pattern":
|
|
491
|
+
item = self.storage.get_anti_pattern_by_id(memory_id)
|
|
492
|
+
elif memory_type == "preference":
|
|
493
|
+
item = self.storage.get_preference_by_id(memory_id)
|
|
494
|
+
|
|
495
|
+
if item:
|
|
496
|
+
self._item_cache[memory_id] = item
|
|
497
|
+
|
|
498
|
+
return item
|
|
499
|
+
|
|
500
|
+
def get_multiple_full_items(
|
|
501
|
+
self,
|
|
502
|
+
memory_ids: List[str],
|
|
503
|
+
memory_type: str,
|
|
504
|
+
) -> List[Any]:
|
|
505
|
+
"""Get full details for multiple items."""
|
|
506
|
+
return [
|
|
507
|
+
item
|
|
508
|
+
for item in (self.get_full_item(mid, memory_type) for mid in memory_ids)
|
|
509
|
+
if item is not None
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
def clear_cache(self) -> None:
|
|
513
|
+
"""Clear the item cache."""
|
|
514
|
+
self._item_cache.clear()
|
|
515
|
+
|
|
516
|
+
def format_summaries_for_context(
|
|
517
|
+
self,
|
|
518
|
+
progressive_slice: ProgressiveSlice,
|
|
519
|
+
include_fetch_hint: bool = True,
|
|
520
|
+
) -> str:
|
|
521
|
+
"""
|
|
522
|
+
Format summaries for inclusion in agent context.
|
|
523
|
+
|
|
524
|
+
Returns a compact string representation suitable for prompts.
|
|
525
|
+
"""
|
|
526
|
+
lines = []
|
|
527
|
+
|
|
528
|
+
if progressive_slice.heuristic_summaries:
|
|
529
|
+
lines.append("## Relevant Patterns")
|
|
530
|
+
for s in progressive_slice.heuristic_summaries:
|
|
531
|
+
lines.append(f"- [{s.id}] {s.summary} ({s.relevance_hint})")
|
|
532
|
+
|
|
533
|
+
if progressive_slice.anti_pattern_summaries:
|
|
534
|
+
lines.append("\n## Warnings")
|
|
535
|
+
for s in progressive_slice.anti_pattern_summaries:
|
|
536
|
+
lines.append(f"- [{s.id}] {s.summary} ({s.relevance_hint})")
|
|
537
|
+
|
|
538
|
+
if progressive_slice.outcome_summaries:
|
|
539
|
+
lines.append("\n## Recent Outcomes")
|
|
540
|
+
for s in progressive_slice.outcome_summaries:
|
|
541
|
+
lines.append(f"- [{s.id}] {s.summary}")
|
|
542
|
+
|
|
543
|
+
if progressive_slice.preference_summaries:
|
|
544
|
+
lines.append("\n## User Preferences")
|
|
545
|
+
for s in progressive_slice.preference_summaries:
|
|
546
|
+
lines.append(f"- [{s.id}] {s.summary}")
|
|
547
|
+
|
|
548
|
+
if progressive_slice.knowledge_summaries:
|
|
549
|
+
lines.append("\n## Domain Knowledge")
|
|
550
|
+
for s in progressive_slice.knowledge_summaries:
|
|
551
|
+
lines.append(f"- [{s.id}] {s.summary}")
|
|
552
|
+
|
|
553
|
+
if include_fetch_hint:
|
|
554
|
+
lines.append(
|
|
555
|
+
f"\n_({progressive_slice.summaries_included} summaries shown, "
|
|
556
|
+
f"~{progressive_slice.estimated_summary_tokens} tokens. "
|
|
557
|
+
f"Request full details by ID if needed.)_"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
return "\n".join(lines)
|