cognautic-cli 1.1.1__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.
cognautic/memory.py ADDED
@@ -0,0 +1,419 @@
1
+ """
2
+ Memory system for Cognautic CLI - handles conversation history and context persistence
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import uuid
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Any
11
+ from dataclasses import dataclass, asdict
12
+ from rich.console import Console
13
+
14
+ console = Console()
15
+
16
+
17
+ @dataclass
18
+ class Message:
19
+ """Represents a single message in a conversation"""
20
+ role: str # 'user' or 'assistant'
21
+ content: str
22
+ timestamp: str
23
+ metadata: Optional[Dict[str, Any]] = None
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ return asdict(self)
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: Dict[str, Any]) -> 'Message':
30
+ return cls(**data)
31
+
32
+
33
+ @dataclass
34
+ class SessionInfo:
35
+ """Information about a chat session"""
36
+ session_id: str
37
+ title: str
38
+ created_at: str
39
+ last_updated: str
40
+ provider: str
41
+ model: Optional[str] = None
42
+ workspace: Optional[str] = None
43
+ message_count: int = 0
44
+
45
+ def to_dict(self) -> Dict[str, Any]:
46
+ return asdict(self)
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: Dict[str, Any]) -> 'SessionInfo':
50
+ return cls(**data)
51
+
52
+
53
+ class MemoryManager:
54
+ """Manages conversation memory and session persistence"""
55
+
56
+ def __init__(self, base_dir: Optional[str] = None):
57
+ """Initialize memory manager
58
+
59
+ Args:
60
+ base_dir: Base directory for sessions. If None, uses current working directory
61
+ """
62
+ if base_dir:
63
+ self.base_dir = Path(base_dir)
64
+ else:
65
+ self.base_dir = Path.cwd()
66
+
67
+ self.sessions_dir = self.base_dir / ".sessions"
68
+ self.sessions_dir.mkdir(exist_ok=True)
69
+
70
+ # Current session state
71
+ self.current_session: Optional[SessionInfo] = None
72
+ self.current_messages: List[Message] = []
73
+ self.session_file: Optional[Path] = None
74
+
75
+ def create_session(self, provider: str, model: Optional[str] = None,
76
+ workspace: Optional[str] = None, title: Optional[str] = None) -> str:
77
+ """Create a new chat session
78
+
79
+ Args:
80
+ provider: AI provider name
81
+ model: Model name (optional)
82
+ workspace: Current workspace path (optional)
83
+ title: Session title (optional, will be auto-generated if not provided)
84
+
85
+ Returns:
86
+ Session ID
87
+ """
88
+ session_id = str(uuid.uuid4())[:8] # Short session ID
89
+ timestamp = datetime.now().isoformat()
90
+
91
+ if not title:
92
+ title = f"Chat Session {session_id}"
93
+
94
+ self.current_session = SessionInfo(
95
+ session_id=session_id,
96
+ title=title,
97
+ created_at=timestamp,
98
+ last_updated=timestamp,
99
+ provider=provider,
100
+ model=model,
101
+ workspace=workspace,
102
+ message_count=0
103
+ )
104
+
105
+ self.current_messages = []
106
+ self.session_file = self.sessions_dir / f"{session_id}.json"
107
+
108
+ # Save initial session file
109
+ self._save_session()
110
+
111
+ console.print(f"✅ Created new session: {session_id} - {title}", style="green")
112
+ return session_id
113
+
114
+ def load_session(self, session_id: str) -> bool:
115
+ """Load an existing session
116
+
117
+ Args:
118
+ session_id: Session ID to load
119
+
120
+ Returns:
121
+ True if session loaded successfully, False otherwise
122
+ """
123
+ session_file = self.sessions_dir / f"{session_id}.json"
124
+
125
+ if not session_file.exists():
126
+ console.print(f"❌ Session {session_id} not found", style="red")
127
+ return False
128
+
129
+ try:
130
+ with open(session_file, 'r', encoding='utf-8') as f:
131
+ data = json.load(f)
132
+
133
+ self.current_session = SessionInfo.from_dict(data['session_info'])
134
+ self.current_messages = [Message.from_dict(msg) for msg in data['messages']]
135
+ self.session_file = session_file
136
+
137
+ console.print(f"✅ Loaded session: {session_id} - {self.current_session.title}", style="green")
138
+ console.print(f"📊 Messages: {len(self.current_messages)}, Provider: {self.current_session.provider}")
139
+
140
+ return True
141
+
142
+ except Exception as e:
143
+ console.print(f"❌ Error loading session {session_id}: {e}", style="red")
144
+ return False
145
+
146
+ def add_message(self, role: str, content: str, metadata: Optional[Dict[str, Any]] = None):
147
+ """Add a message to the current session
148
+
149
+ Args:
150
+ role: Message role ('user' or 'assistant')
151
+ content: Message content
152
+ metadata: Optional metadata dictionary
153
+ """
154
+ if not self.current_session:
155
+ console.print("❌ No active session. Create or load a session first.", style="red")
156
+ return
157
+
158
+ message = Message(
159
+ role=role,
160
+ content=content,
161
+ timestamp=datetime.now().isoformat(),
162
+ metadata=metadata
163
+ )
164
+
165
+ self.current_messages.append(message)
166
+ self.current_session.message_count = len(self.current_messages)
167
+ self.current_session.last_updated = datetime.now().isoformat()
168
+
169
+ # Save session after each message
170
+ self._save_session()
171
+
172
+ def get_conversation_history(self, limit: Optional[int] = None) -> List[Message]:
173
+ """Get conversation history for the current session
174
+
175
+ Args:
176
+ limit: Maximum number of messages to return (most recent first)
177
+
178
+ Returns:
179
+ List of messages
180
+ """
181
+ if not self.current_messages:
182
+ return []
183
+
184
+ messages = self.current_messages.copy()
185
+ if limit:
186
+ messages = messages[-limit:]
187
+
188
+ return messages
189
+
190
+ def get_context_for_ai(self, limit: int = 10) -> List[Dict[str, str]]:
191
+ """Get conversation context formatted for AI consumption
192
+
193
+ Args:
194
+ limit: Maximum number of recent messages to include
195
+
196
+ Returns:
197
+ List of message dictionaries with role and content
198
+ """
199
+ messages = self.get_conversation_history(limit)
200
+ return [{"role": msg.role, "content": msg.content} for msg in messages]
201
+
202
+ def list_sessions(self) -> List[SessionInfo]:
203
+ """List all available sessions
204
+
205
+ Returns:
206
+ List of SessionInfo objects
207
+ """
208
+ sessions = []
209
+
210
+ for session_file in self.sessions_dir.glob("*.json"):
211
+ try:
212
+ with open(session_file, 'r', encoding='utf-8') as f:
213
+ data = json.load(f)
214
+
215
+ session_info = SessionInfo.from_dict(data['session_info'])
216
+ sessions.append(session_info)
217
+
218
+ except Exception as e:
219
+ console.print(f"❌ Error reading session file {session_file}: {e}", style="red")
220
+
221
+ # Sort by last updated (most recent first)
222
+ sessions.sort(key=lambda s: s.last_updated, reverse=True)
223
+ return sessions
224
+
225
+ def delete_session(self, session_id: str) -> bool:
226
+ """Delete a session
227
+
228
+ Args:
229
+ session_id: Session ID to delete
230
+
231
+ Returns:
232
+ True if deleted successfully, False otherwise
233
+ """
234
+ session_file = self.sessions_dir / f"{session_id}.json"
235
+
236
+ if not session_file.exists():
237
+ console.print(f"❌ Session {session_id} not found", style="red")
238
+ return False
239
+
240
+ try:
241
+ session_file.unlink()
242
+
243
+ # If this was the current session, clear it
244
+ if self.current_session and self.current_session.session_id == session_id:
245
+ self.current_session = None
246
+ self.current_messages = []
247
+ self.session_file = None
248
+
249
+ console.print(f"✅ Deleted session: {session_id}", style="green")
250
+ return True
251
+
252
+ except Exception as e:
253
+ console.print(f"❌ Error deleting session {session_id}: {e}", style="red")
254
+ return False
255
+
256
+ def update_session_info(self, **kwargs):
257
+ """Update current session information
258
+
259
+ Args:
260
+ **kwargs: Fields to update (title, provider, model, workspace)
261
+ """
262
+ if not self.current_session:
263
+ console.print("❌ No active session", style="red")
264
+ return
265
+
266
+ for key, value in kwargs.items():
267
+ if hasattr(self.current_session, key):
268
+ setattr(self.current_session, key, value)
269
+
270
+ self.current_session.last_updated = datetime.now().isoformat()
271
+ self._save_session()
272
+
273
+ def get_current_session(self) -> Optional[SessionInfo]:
274
+ """Get current session info
275
+
276
+ Returns:
277
+ Current SessionInfo or None
278
+ """
279
+ return self.current_session
280
+
281
+ def _save_session(self):
282
+ """Save current session to file with self-healing"""
283
+ if not self.current_session or not self.session_file:
284
+ return
285
+
286
+ try:
287
+ # Self-healing: Create sessions directory if it doesn't exist
288
+ sessions_dir = Path(self.session_file).parent
289
+ sessions_dir.mkdir(parents=True, exist_ok=True)
290
+
291
+ data = {
292
+ 'session_info': self.current_session.to_dict(),
293
+ 'messages': [msg.to_dict() for msg in self.current_messages]
294
+ }
295
+
296
+ with open(self.session_file, 'w', encoding='utf-8') as f:
297
+ json.dump(data, f, indent=2, ensure_ascii=False)
298
+
299
+ except Exception as e:
300
+ console.print(f"❌ Error saving session: {e}", style="red")
301
+ # Self-healing: Try to regenerate session from current state
302
+ self._regenerate_session_file()
303
+
304
+ def generate_session_title(self, first_message: str) -> str:
305
+ """Generate a descriptive title from the first user message
306
+
307
+ Args:
308
+ first_message: First user message in the session
309
+
310
+ Returns:
311
+ Generated title
312
+ """
313
+ # Simple title generation - take first few words
314
+ words = first_message.strip().split()[:6]
315
+ title = " ".join(words)
316
+
317
+ return title[:50] + "..." if len(title) > 50 else title
318
+
319
+ def _regenerate_session_file(self):
320
+ """Self-healing: Regenerate session file from current state"""
321
+ try:
322
+ if not self.current_session:
323
+ # Create a new session if none exists
324
+ self.current_session = SessionInfo(
325
+ session_id=str(uuid.uuid4())[:8],
326
+ title="Recovered Session",
327
+ created_at=datetime.now().isoformat(),
328
+ last_updated=datetime.now().isoformat(),
329
+ provider="unknown",
330
+ message_count=len(self.current_messages)
331
+ )
332
+
333
+ # Ensure sessions directory exists
334
+ if self.session_file:
335
+ sessions_dir = Path(self.session_file).parent
336
+ sessions_dir.mkdir(parents=True, exist_ok=True)
337
+
338
+ # Try to save again
339
+ data = {
340
+ 'session_info': self.current_session.to_dict(),
341
+ 'messages': [msg.to_dict() for msg in self.current_messages]
342
+ }
343
+
344
+ with open(self.session_file, 'w', encoding='utf-8') as f:
345
+ json.dump(data, f, indent=2, ensure_ascii=False)
346
+
347
+ console.print("🔧 Session file regenerated successfully", style="yellow")
348
+
349
+ except Exception as e:
350
+ console.print(f"🔧 Self-healing failed: {e}", style="yellow")
351
+
352
+ def export_session(self, session_id: str, format: str = "json") -> Optional[str]:
353
+ """Export a session to a file
354
+
355
+ Args:
356
+ session_id: Session ID to export
357
+ format: Export format ('json', 'markdown', 'txt')
358
+
359
+ Returns:
360
+ Path to exported file or None if failed
361
+ """
362
+ session_file = self.sessions_dir / f"{session_id}.json"
363
+
364
+ if not session_file.exists():
365
+ console.print(f"❌ Session {session_id} not found", style="red")
366
+ return None
367
+
368
+ try:
369
+ with open(session_file, 'r', encoding='utf-8') as f:
370
+ data = json.load(f)
371
+
372
+ session_info = SessionInfo.from_dict(data['session_info'])
373
+ messages = [Message.from_dict(msg) for msg in data['messages']]
374
+
375
+ export_file = self.sessions_dir / f"{session_id}_export.{format}"
376
+
377
+ if format == "json":
378
+ with open(export_file, 'w', encoding='utf-8') as f:
379
+ json.dump(data, f, indent=2, ensure_ascii=False)
380
+
381
+ elif format == "markdown":
382
+ with open(export_file, 'w', encoding='utf-8') as f:
383
+ f.write(f"# {session_info.title}\n\n")
384
+ f.write(f"**Session ID:** {session_info.session_id}\n")
385
+ f.write(f"**Created:** {session_info.created_at}\n")
386
+ f.write(f"**Provider:** {session_info.provider}\n")
387
+ if session_info.model:
388
+ f.write(f"**Model:** {session_info.model}\n")
389
+ f.write(f"**Messages:** {len(messages)}\n\n")
390
+ f.write("---\n\n")
391
+
392
+ for msg in messages:
393
+ role_emoji = "👤" if msg.role == "user" else "🤖"
394
+ f.write(f"## {role_emoji} {msg.role.title()}\n\n")
395
+ f.write(f"{msg.content}\n\n")
396
+ f.write(f"*{msg.timestamp}*\n\n")
397
+
398
+ elif format == "txt":
399
+ with open(export_file, 'w', encoding='utf-8') as f:
400
+ f.write(f"{session_info.title}\n")
401
+ f.write("=" * len(session_info.title) + "\n\n")
402
+ f.write(f"Session ID: {session_info.session_id}\n")
403
+ f.write(f"Created: {session_info.created_at}\n")
404
+ f.write(f"Provider: {session_info.provider}\n")
405
+ if session_info.model:
406
+ f.write(f"Model: {session_info.model}\n")
407
+ f.write(f"Messages: {len(messages)}\n\n")
408
+ f.write("-" * 50 + "\n\n")
409
+
410
+ for msg in messages:
411
+ f.write(f"{msg.role.upper()}: {msg.content}\n")
412
+ f.write(f"Time: {msg.timestamp}\n\n")
413
+
414
+ console.print(f"✅ Exported session to: {export_file}", style="green")
415
+ return str(export_file)
416
+
417
+ except Exception as e:
418
+ console.print(f"❌ Error exporting session: {e}", style="red")
419
+ return None