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 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'