comet-memory 0.1.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.
- comet/__init__.py +27 -0
- comet/cli.py +390 -0
- comet/compacter.py +104 -0
- comet/config.py +60 -0
- comet/consolidator.py +248 -0
- comet/orchestrator.py +337 -0
- comet/retriever.py +209 -0
- comet/schemas.py +68 -0
- comet/sensor.py +84 -0
- comet/skills/__init__.py +5 -0
- comet/skills/__main__.py +4 -0
- comet/skills/cli.py +249 -0
- comet/skills/client.py +90 -0
- comet/skills/session.py +51 -0
- comet/storage.py +196 -0
- comet/templates/__init__.py +38 -0
- comet/vector_index.py +215 -0
- comet_memory-0.1.0.dist-info/METADATA +215 -0
- comet_memory-0.1.0.dist-info/RECORD +22 -0
- comet_memory-0.1.0.dist-info/WHEEL +5 -0
- comet_memory-0.1.0.dist-info/entry_points.txt +3 -0
- comet_memory-0.1.0.dist-info/top_level.txt +1 -0
comet/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CoMeT: Cognitive Memory OS - Dynamic Resolution Memory System"""
|
|
2
|
+
from comet.schemas import MemoryNode, CognitiveLoad, CoMeTState, L1Memory, RetrievalResult
|
|
3
|
+
from comet.sensor import CognitiveSensor
|
|
4
|
+
from comet.compacter import MemoryCompacter
|
|
5
|
+
from comet.storage import MemoryStore
|
|
6
|
+
from comet.vector_index import VectorIndex
|
|
7
|
+
from comet.retriever import Retriever
|
|
8
|
+
from comet.consolidator import Consolidator
|
|
9
|
+
from comet.orchestrator import CoMeT, MessageInput
|
|
10
|
+
from comet.config import scope
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'CoMeT',
|
|
14
|
+
'MemoryNode',
|
|
15
|
+
'CognitiveLoad',
|
|
16
|
+
'CoMeTState',
|
|
17
|
+
'L1Memory',
|
|
18
|
+
'RetrievalResult',
|
|
19
|
+
'CognitiveSensor',
|
|
20
|
+
'MemoryCompacter',
|
|
21
|
+
'MemoryStore',
|
|
22
|
+
'VectorIndex',
|
|
23
|
+
'Retriever',
|
|
24
|
+
'Consolidator',
|
|
25
|
+
'scope',
|
|
26
|
+
]
|
|
27
|
+
|
comet/cli.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""CoMeT CLI: Command-line interface using ato scope as entry point."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Auto-load .env from CoMeT project root
|
|
9
|
+
_DEFAULT_STORE = str(Path.home()/'.comet'/'memory_store')
|
|
10
|
+
|
|
11
|
+
for _env_candidate in [Path.cwd()/'.env', Path.home()/'.comet'/'.env']:
|
|
12
|
+
if _env_candidate.exists():
|
|
13
|
+
for line in _env_candidate.read_text().splitlines():
|
|
14
|
+
line = line.strip()
|
|
15
|
+
if not line or line.startswith('#'):
|
|
16
|
+
continue
|
|
17
|
+
key, _, value = line.partition('=')
|
|
18
|
+
if key and _ == '=':
|
|
19
|
+
os.environ.setdefault(key.strip(), value.strip())
|
|
20
|
+
break
|
|
21
|
+
|
|
22
|
+
from loguru import logger
|
|
23
|
+
|
|
24
|
+
from comet.config import scope
|
|
25
|
+
from comet.orchestrator import CoMeT
|
|
26
|
+
|
|
27
|
+
logger.disable('comet')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SessionManager:
|
|
31
|
+
"""Session management layer.
|
|
32
|
+
|
|
33
|
+
Canonical implementation: comet.skills.session.SessionManager
|
|
34
|
+
This class is kept for backward compatibility.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, store_path: str):
|
|
38
|
+
from comet.skills.session import SessionManager as _SM
|
|
39
|
+
self._impl = _SM(store_path)
|
|
40
|
+
|
|
41
|
+
def create(self, session_id: str = '', title: str = '') -> str:
|
|
42
|
+
return self._impl.create(session_id=session_id, title=title)
|
|
43
|
+
|
|
44
|
+
def increment_node_count(self, session_id: str):
|
|
45
|
+
self._impl.increment_node_count(session_id)
|
|
46
|
+
|
|
47
|
+
def list_recent(self, limit: int = 10) -> list[dict]:
|
|
48
|
+
return self._impl.list_recent(limit)
|
|
49
|
+
|
|
50
|
+
def exists(self, session_id: str) -> bool:
|
|
51
|
+
return self._impl.exists(session_id)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@scope.observe(default=True)
|
|
56
|
+
def cli_defaults(config):
|
|
57
|
+
config.command = ''
|
|
58
|
+
config.text = ''
|
|
59
|
+
config.file = ''
|
|
60
|
+
config.node_id = ''
|
|
61
|
+
config.depth = 2
|
|
62
|
+
config.tag = ''
|
|
63
|
+
config.summary_query = ''
|
|
64
|
+
config.trigger_query = ''
|
|
65
|
+
config.top_k = 5
|
|
66
|
+
# store command fields
|
|
67
|
+
config.summary = ''
|
|
68
|
+
config.trigger = ''
|
|
69
|
+
config.topic_tags = ''
|
|
70
|
+
config.raw_text = ''
|
|
71
|
+
config.recall_mode = 'active'
|
|
72
|
+
# session fields
|
|
73
|
+
config.session_id = ''
|
|
74
|
+
config.session_title = ''
|
|
75
|
+
# fixed storage path (overrides core's relative ./memory_store)
|
|
76
|
+
_store_root = os.environ.get('COMET_STORE', _DEFAULT_STORE)
|
|
77
|
+
config.storage.base_path = _store_root
|
|
78
|
+
config.storage.raw_path = str(Path(_store_root)/'raw')
|
|
79
|
+
config.retrieval.vector_db_path = str(Path(_store_root)/'vectors')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_session_nodes(memo, session_id):
|
|
83
|
+
"""Get all nodes belonging to a session (reads node files, not index)."""
|
|
84
|
+
nodes = []
|
|
85
|
+
for meta in memo._store.list_all():
|
|
86
|
+
node = memo._store.get_node(meta['node_id'])
|
|
87
|
+
if node and node.session_id == session_id:
|
|
88
|
+
nodes.append(node)
|
|
89
|
+
return nodes
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@scope
|
|
93
|
+
def main(config):
|
|
94
|
+
if not config.command:
|
|
95
|
+
_print_usage()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
store_path = config.get('storage', {}).get('base_path', './memory_store')
|
|
99
|
+
session_mgr = SessionManager(store_path)
|
|
100
|
+
|
|
101
|
+
# Commands that don't need CoMeT instance
|
|
102
|
+
if config.command == 'init':
|
|
103
|
+
cmd_init(session_mgr, config)
|
|
104
|
+
return
|
|
105
|
+
if config.command == 'sessions':
|
|
106
|
+
cmd_sessions(session_mgr, config)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
memo = CoMeT(config)
|
|
110
|
+
commands = {
|
|
111
|
+
'add': cmd_add,
|
|
112
|
+
'store': cmd_store,
|
|
113
|
+
'compact': cmd_compact,
|
|
114
|
+
'index': cmd_index,
|
|
115
|
+
'read': cmd_read,
|
|
116
|
+
'search': cmd_search,
|
|
117
|
+
'retrieve': cmd_retrieve,
|
|
118
|
+
'close': cmd_close,
|
|
119
|
+
'tags': cmd_tags,
|
|
120
|
+
'recall': cmd_recall,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
handler = commands.get(config.command)
|
|
124
|
+
if not handler:
|
|
125
|
+
print(f'Unknown command: {config.command}')
|
|
126
|
+
_print_usage()
|
|
127
|
+
return
|
|
128
|
+
handler(memo, config, session_mgr)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_init(session_mgr, config):
|
|
132
|
+
session_id = session_mgr.create(
|
|
133
|
+
session_id=config.session_id,
|
|
134
|
+
title=config.session_title,
|
|
135
|
+
)
|
|
136
|
+
print(f'Session: {session_id}')
|
|
137
|
+
|
|
138
|
+
recent = session_mgr.list_recent(5)
|
|
139
|
+
past = [s for s in recent if s['session_id'] != session_id]
|
|
140
|
+
if past:
|
|
141
|
+
print()
|
|
142
|
+
print('Recent sessions:')
|
|
143
|
+
for s in past:
|
|
144
|
+
title = s.get('title', '')
|
|
145
|
+
title_str = f' — {title}' if title else ''
|
|
146
|
+
print(f' {s["session_id"]} ({s.get("node_count", 0)} nodes){title_str}')
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_sessions(session_mgr, config):
|
|
150
|
+
recent = session_mgr.list_recent(20)
|
|
151
|
+
if not recent:
|
|
152
|
+
print('No sessions found')
|
|
153
|
+
return
|
|
154
|
+
for s in recent:
|
|
155
|
+
title = s.get('title', '')
|
|
156
|
+
title_str = f' — {title}' if title else ''
|
|
157
|
+
print(f'{s["session_id"]} ({s.get("node_count", 0)} nodes, {s["created_at"][:10]}){title_str}')
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_add(memo, config, session_mgr):
|
|
161
|
+
if config.file:
|
|
162
|
+
text = Path(config.file).read_text(encoding='utf-8')
|
|
163
|
+
else:
|
|
164
|
+
text = config.text
|
|
165
|
+
|
|
166
|
+
if not text.strip():
|
|
167
|
+
print('Error: empty input. Use text:="your text" or file:=path')
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
node = memo.add(text)
|
|
171
|
+
if node:
|
|
172
|
+
if config.session_id:
|
|
173
|
+
node.session_id = config.session_id
|
|
174
|
+
memo._store.save_node(node)
|
|
175
|
+
session_mgr.increment_node_count(config.session_id)
|
|
176
|
+
print(f'Compacted → {node.node_id}')
|
|
177
|
+
print(f' Summary: {node.summary}')
|
|
178
|
+
print(f' Trigger: {node.trigger}')
|
|
179
|
+
tags = ', '.join(node.topic_tags)
|
|
180
|
+
print(f' Tags: {tags}')
|
|
181
|
+
else:
|
|
182
|
+
load = memo.last_load
|
|
183
|
+
buf_size = len(memo.l1_buffer)
|
|
184
|
+
if load:
|
|
185
|
+
print(f'Buffered (flow={load.logic_flow}, load={load.load_level}, buffer={buf_size})')
|
|
186
|
+
else:
|
|
187
|
+
print(f'Buffered (buffer={buf_size})')
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_store(memo, config, session_mgr):
|
|
191
|
+
if config.file:
|
|
192
|
+
entries = json.loads(Path(config.file).read_text(encoding='utf-8'))
|
|
193
|
+
if isinstance(entries, dict):
|
|
194
|
+
entries = [entries]
|
|
195
|
+
for entry in entries:
|
|
196
|
+
node = memo.store_direct(
|
|
197
|
+
summary=entry['summary'],
|
|
198
|
+
trigger=entry['trigger'],
|
|
199
|
+
raw_text=entry.get('raw_text', ''),
|
|
200
|
+
topic_tags=entry.get('topic_tags', []),
|
|
201
|
+
recall_mode=entry.get('recall_mode', 'active'),
|
|
202
|
+
session_id=config.session_id or '',
|
|
203
|
+
)
|
|
204
|
+
if config.session_id:
|
|
205
|
+
session_mgr.increment_node_count(config.session_id)
|
|
206
|
+
tags = ', '.join(node.topic_tags)
|
|
207
|
+
linked = ', '.join(node.links) if node.links else 'none'
|
|
208
|
+
print(f'Stored → {node.node_id} | {node.summary} (tags: {tags}, linked: {linked})')
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if not config.summary:
|
|
212
|
+
print('Error: summary required. Use summary:=... or file:=path.json')
|
|
213
|
+
return
|
|
214
|
+
if not config.trigger:
|
|
215
|
+
print('Error: trigger required. Use trigger:=...')
|
|
216
|
+
return
|
|
217
|
+
raw_text = config.raw_text or config.text or ''
|
|
218
|
+
if not raw_text:
|
|
219
|
+
print('Error: raw text required. Use raw_text:=...')
|
|
220
|
+
return
|
|
221
|
+
tags = [t.strip() for t in config.topic_tags.split(',') if t.strip()] if config.topic_tags else []
|
|
222
|
+
node = memo.store_direct(
|
|
223
|
+
summary=config.summary,
|
|
224
|
+
trigger=config.trigger,
|
|
225
|
+
raw_text=raw_text,
|
|
226
|
+
topic_tags=tags,
|
|
227
|
+
recall_mode=config.recall_mode,
|
|
228
|
+
session_id=config.session_id or '',
|
|
229
|
+
)
|
|
230
|
+
if config.session_id:
|
|
231
|
+
session_mgr.increment_node_count(config.session_id)
|
|
232
|
+
print(f'Stored → {node.node_id}')
|
|
233
|
+
print(f' Summary: {node.summary}')
|
|
234
|
+
print(f' Trigger: {node.trigger}')
|
|
235
|
+
tag_str = ', '.join(node.topic_tags)
|
|
236
|
+
print(f' Tags: {tag_str}')
|
|
237
|
+
linked = ', '.join(node.links) if node.links else 'none'
|
|
238
|
+
print(f' Linked: {linked}')
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def cmd_compact(memo, config, session_mgr):
|
|
242
|
+
node = memo.force_compact()
|
|
243
|
+
if node:
|
|
244
|
+
if config.session_id:
|
|
245
|
+
node.session_id = config.session_id
|
|
246
|
+
memo._store.save_node(node)
|
|
247
|
+
session_mgr.increment_node_count(config.session_id)
|
|
248
|
+
print(f'Compacted → {node.node_id}')
|
|
249
|
+
print(f' Summary: {node.summary}')
|
|
250
|
+
print(f' Trigger: {node.trigger}')
|
|
251
|
+
tags = ', '.join(node.topic_tags)
|
|
252
|
+
print(f' Tags: {tags}')
|
|
253
|
+
else:
|
|
254
|
+
print('Nothing to compact (buffer empty)')
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def cmd_index(memo, config, session_mgr):
|
|
258
|
+
if config.session_id:
|
|
259
|
+
filtered = _get_session_nodes(memo, config.session_id)
|
|
260
|
+
if not filtered:
|
|
261
|
+
print(f'No nodes in session: {config.session_id}')
|
|
262
|
+
return
|
|
263
|
+
for node in filtered:
|
|
264
|
+
print(f'[{node.node_id}] {node.summary} | {node.trigger}')
|
|
265
|
+
else:
|
|
266
|
+
result = memo.get_context_window(max_nodes=100)
|
|
267
|
+
if result.strip():
|
|
268
|
+
print(result)
|
|
269
|
+
else:
|
|
270
|
+
print('No memory nodes stored')
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def cmd_read(memo, config, session_mgr):
|
|
274
|
+
if not config.node_id:
|
|
275
|
+
print('Error: node_id required. Use node_id:=mem_xxx')
|
|
276
|
+
return
|
|
277
|
+
result = memo.read_memory(config.node_id, depth=config.depth)
|
|
278
|
+
if result:
|
|
279
|
+
print(result)
|
|
280
|
+
else:
|
|
281
|
+
print(f'Node not found: {config.node_id}')
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def cmd_search(memo, config, session_mgr):
|
|
285
|
+
if not config.tag:
|
|
286
|
+
print('Error: tag required. Use tag:=TagName')
|
|
287
|
+
return
|
|
288
|
+
results = memo.search(config.tag)
|
|
289
|
+
if not results:
|
|
290
|
+
print(f'No nodes found with tag: {config.tag}')
|
|
291
|
+
return
|
|
292
|
+
for node_id in results:
|
|
293
|
+
info = memo.read_memory(node_id, depth=0)
|
|
294
|
+
print(f'[{node_id}] {info}')
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def cmd_retrieve(memo, config, session_mgr):
|
|
298
|
+
if not config.summary_query or not config.trigger_query:
|
|
299
|
+
print('Error: both summary_query and trigger_query required.')
|
|
300
|
+
print(' Use summary_query:="what to find" trigger_query:="why you need it"')
|
|
301
|
+
return
|
|
302
|
+
results = memo.retrieve_dual(config.summary_query, config.trigger_query, top_k=config.top_k)
|
|
303
|
+
if not results:
|
|
304
|
+
print('No relevant memories found')
|
|
305
|
+
return
|
|
306
|
+
for r in results:
|
|
307
|
+
linked = ', '.join(r.node.links) if r.node.links else 'none'
|
|
308
|
+
tags = ', '.join(r.node.topic_tags)
|
|
309
|
+
print(
|
|
310
|
+
f'[{r.node.node_id}] (score={r.relevance_score:.4f})\n'
|
|
311
|
+
f' Summary: {r.node.summary}\n'
|
|
312
|
+
f' Trigger: {r.node.trigger}\n'
|
|
313
|
+
f' Tags: {tags}\n'
|
|
314
|
+
f' Linked: {linked}'
|
|
315
|
+
)
|
|
316
|
+
print()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cmd_recall(memo, config, session_mgr):
|
|
320
|
+
if not config.session_id:
|
|
321
|
+
print('Error: session_id required. Use session_id:=ses_xxx')
|
|
322
|
+
recent = session_mgr.list_recent(5)
|
|
323
|
+
if recent:
|
|
324
|
+
print()
|
|
325
|
+
print('Recent sessions:')
|
|
326
|
+
for s in recent:
|
|
327
|
+
title = s.get('title', '')
|
|
328
|
+
title_str = f' — {title}' if title else ''
|
|
329
|
+
print(f' {s["session_id"]} ({s.get("node_count", 0)} nodes){title_str}')
|
|
330
|
+
return
|
|
331
|
+
session_nodes = _get_session_nodes(memo, config.session_id)
|
|
332
|
+
if not session_nodes:
|
|
333
|
+
print(f'No nodes in session: {config.session_id}')
|
|
334
|
+
return
|
|
335
|
+
print(f'=== Session {config.session_id} ({len(session_nodes)} nodes) ===')
|
|
336
|
+
print()
|
|
337
|
+
for node in session_nodes:
|
|
338
|
+
tags = ', '.join(node.topic_tags)
|
|
339
|
+
linked = ', '.join(node.links) if node.links else 'none'
|
|
340
|
+
print(
|
|
341
|
+
f'[{node.node_id}]\n'
|
|
342
|
+
f' Summary: {node.summary}\n'
|
|
343
|
+
f' Trigger: {node.trigger}\n'
|
|
344
|
+
f' Tags: {tags}\n'
|
|
345
|
+
f' Linked: {linked}'
|
|
346
|
+
)
|
|
347
|
+
print()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def cmd_close(memo, config, session_mgr):
|
|
351
|
+
memo.close_session()
|
|
352
|
+
print('Session closed (buffer compacted + consolidated)')
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def cmd_tags(memo, config, session_mgr):
|
|
356
|
+
tags = memo._store.get_all_tags()
|
|
357
|
+
if tags:
|
|
358
|
+
for tag in sorted(tags):
|
|
359
|
+
print(tag)
|
|
360
|
+
else:
|
|
361
|
+
print('No tags found')
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _print_usage():
|
|
365
|
+
print('CoMeT CLI — Cognitive Memory Tree')
|
|
366
|
+
print()
|
|
367
|
+
print('Usage: python -m comet.cli command:=CMD [options]')
|
|
368
|
+
print()
|
|
369
|
+
print('Session commands (no API key):')
|
|
370
|
+
print(' command:=init [session_id:=ID] [session_title:=Title]')
|
|
371
|
+
print(' command:=sessions List all sessions')
|
|
372
|
+
print(' command:=recall session_id:=ses_xxx Load session context')
|
|
373
|
+
print()
|
|
374
|
+
print('Memory commands (no API key):')
|
|
375
|
+
print(' command:=store session_id:=ses_xxx "summary:=\'...\'" "trigger:=\'...\'" ...')
|
|
376
|
+
print(' command:=index [session_id:=ses_xxx] List nodes (session or all)')
|
|
377
|
+
print(' command:=read node_id:=mem_xxx [depth=N]')
|
|
378
|
+
print(' command:=search tag:=TagName')
|
|
379
|
+
print(' command:=tags')
|
|
380
|
+
print()
|
|
381
|
+
print('LLM commands (requires OPENAI_API_KEY):')
|
|
382
|
+
print(' command:=add session_id:=ses_xxx "text:=\'...\'"')
|
|
383
|
+
print(' command:=retrieve "summary_query:=\'...\'" "trigger_query:=\'...\'"')
|
|
384
|
+
print(' command:=compact / command:=close')
|
|
385
|
+
print()
|
|
386
|
+
print('Quoting: "key:=\'multi word value\'" (shell double, ato single)')
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
if __name__ == '__main__':
|
|
390
|
+
main()
|
comet/compacter.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""MemoryCompacter: L1 -> L2+ structuring with summary + key generation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ato.adict import ADict
|
|
7
|
+
from langchain_openai import ChatOpenAI
|
|
8
|
+
from langchain_core.language_models import BaseChatModel
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from comet.schemas import L1Memory, MemoryNode
|
|
12
|
+
from comet.storage import MemoryStore
|
|
13
|
+
from comet.templates import load_template
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from comet.vector_index import VectorIndex
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CompactedResult(BaseModel):
|
|
20
|
+
"""Structured output for compacting."""
|
|
21
|
+
summary: str = Field(description='Brief topic description (1 line, NO specific numbers/dates)')
|
|
22
|
+
trigger: str = Field(description='Detailed description of when to retrieve this info')
|
|
23
|
+
recall_mode: str = Field(
|
|
24
|
+
default='active',
|
|
25
|
+
description='passive=always in context, active=on-demand, both=always + searchable',
|
|
26
|
+
)
|
|
27
|
+
topic_tags: list[str] = Field(description='1-2 topic tags')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Prompt loaded from templates/compacting.txt
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MemoryCompacter:
|
|
34
|
+
"""
|
|
35
|
+
Slow Layer processor for L1 -> L2+ structuring.
|
|
36
|
+
|
|
37
|
+
Takes accumulated L1 buffer and creates structured MemoryNodes
|
|
38
|
+
with summaries and keys to raw data.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: ADict, store: MemoryStore, vector_index: Optional[VectorIndex] = None):
|
|
42
|
+
self._config = config
|
|
43
|
+
self._store = store
|
|
44
|
+
self._vector_index = vector_index
|
|
45
|
+
self._llm: BaseChatModel = ChatOpenAI(model=config.main_model)
|
|
46
|
+
self._structured_llm = self._llm.with_structured_output(CompactedResult)
|
|
47
|
+
|
|
48
|
+
def compact(
|
|
49
|
+
self,
|
|
50
|
+
l1_buffer: list[L1Memory],
|
|
51
|
+
depth_level: int = 1,
|
|
52
|
+
) -> MemoryNode:
|
|
53
|
+
"""
|
|
54
|
+
Compact L1 buffer into a structured MemoryNode.
|
|
55
|
+
|
|
56
|
+
1. Concatenate L1 contents as raw data
|
|
57
|
+
2. Generate summary + tags via LLM
|
|
58
|
+
3. Save raw data with content_key
|
|
59
|
+
4. Create and save MemoryNode
|
|
60
|
+
"""
|
|
61
|
+
# Build raw data from L1 buffer
|
|
62
|
+
raw_data = '\n\n'.join([
|
|
63
|
+
f"[{mem.timestamp.strftime('%H:%M:%S')}] {mem.content}"
|
|
64
|
+
for mem in l1_buffer
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
# Generate summary via LLM (with existing topic context)
|
|
68
|
+
turns_text = '\n'.join([f"- {mem.content}" for mem in l1_buffer])
|
|
69
|
+
existing_tags = self._store.get_all_tags()
|
|
70
|
+
tags_text = ', '.join(sorted(existing_tags)) if existing_tags else '(없음)'
|
|
71
|
+
prompt = load_template('compacting').format(
|
|
72
|
+
turns=turns_text,
|
|
73
|
+
existing_tags=tags_text,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
result: CompactedResult = self._structured_llm.invoke(prompt)
|
|
77
|
+
|
|
78
|
+
# Generate keys and save raw
|
|
79
|
+
node_id = self._store.generate_node_id()
|
|
80
|
+
content_key = self._store.generate_content_key(prefix='raw')
|
|
81
|
+
raw_location = self._store.save_raw(content_key, raw_data)
|
|
82
|
+
|
|
83
|
+
# Create memory node
|
|
84
|
+
node = MemoryNode(
|
|
85
|
+
node_id=node_id,
|
|
86
|
+
depth_level=depth_level,
|
|
87
|
+
recall_mode=result.recall_mode if result.recall_mode in ('passive', 'active', 'both') else 'active',
|
|
88
|
+
topic_tags=result.topic_tags,
|
|
89
|
+
summary=result.summary,
|
|
90
|
+
trigger=result.trigger,
|
|
91
|
+
content_key=content_key,
|
|
92
|
+
raw_location=raw_location,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Save node
|
|
96
|
+
self._store.save_node(node)
|
|
97
|
+
|
|
98
|
+
# Auto-link + vector index
|
|
99
|
+
self._store.auto_link(node)
|
|
100
|
+
|
|
101
|
+
if self._vector_index:
|
|
102
|
+
self._vector_index.upsert(node, raw_content=raw_data)
|
|
103
|
+
|
|
104
|
+
return node
|
comet/config.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""CoMeT Configuration using ato scope."""
|
|
2
|
+
from ato.scope import Scope
|
|
3
|
+
from ato.adict import ADict
|
|
4
|
+
|
|
5
|
+
scope = Scope()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@scope.observe(default=True)
|
|
9
|
+
def default(config: ADict):
|
|
10
|
+
# SLM for Fast Layer (L1)
|
|
11
|
+
config.slm_model = 'gpt-4o-mini'
|
|
12
|
+
|
|
13
|
+
# Main LLM for Slow Layer (L2+)
|
|
14
|
+
config.main_model = 'gpt-4o'
|
|
15
|
+
|
|
16
|
+
# Compacting thresholds
|
|
17
|
+
config.compacting = ADict(
|
|
18
|
+
load_threshold=4, # load_level >= 4 triggers compacting
|
|
19
|
+
max_l1_buffer=10, # Max items before forced compacting
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Storage settings
|
|
23
|
+
config.storage = ADict(
|
|
24
|
+
type='json', # 'json' or 'redis'
|
|
25
|
+
base_path='./memory_store',
|
|
26
|
+
raw_path='./memory_store/raw',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# RAG retrieval settings
|
|
30
|
+
config.retrieval = ADict(
|
|
31
|
+
embedding_model='text-embedding-3-small',
|
|
32
|
+
vector_backend='chroma',
|
|
33
|
+
vector_db_path='./memory_store/vectors',
|
|
34
|
+
fusion_alpha=0.5,
|
|
35
|
+
rrf_k=5,
|
|
36
|
+
raw_search_weight=0.2,
|
|
37
|
+
top_k=5,
|
|
38
|
+
rerank=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@scope.observe()
|
|
43
|
+
def local_slm(config: ADict):
|
|
44
|
+
"""Use local SLM via Ollama."""
|
|
45
|
+
config.slm_model = 'ollama/gemma2:9b'
|
|
46
|
+
config.slm_base_url = 'http://localhost:11434/v1'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@scope.observe()
|
|
50
|
+
def aggressive(config: ADict):
|
|
51
|
+
"""More aggressive compacting."""
|
|
52
|
+
config.compacting.load_threshold = 3
|
|
53
|
+
config.compacting.max_l1_buffer = 5
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@scope.observe()
|
|
57
|
+
def agent(config: ADict):
|
|
58
|
+
"""Cheaper defaults for agent/skill usage."""
|
|
59
|
+
config.slm_model = 'gpt-4o-mini'
|
|
60
|
+
config.main_model = 'gpt-4o-mini'
|