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/__init__.py +7 -0
- cognautic/ai_engine.py +2213 -0
- cognautic/auto_continuation.py +196 -0
- cognautic/cli.py +1064 -0
- cognautic/config.py +245 -0
- cognautic/file_tagger.py +194 -0
- cognautic/memory.py +419 -0
- cognautic/provider_endpoints.py +424 -0
- cognautic/rules.py +246 -0
- cognautic/tools/__init__.py +19 -0
- cognautic/tools/base.py +59 -0
- cognautic/tools/code_analysis.py +391 -0
- cognautic/tools/command_runner.py +292 -0
- cognautic/tools/file_operations.py +394 -0
- cognautic/tools/registry.py +115 -0
- cognautic/tools/response_control.py +48 -0
- cognautic/tools/web_search.py +336 -0
- cognautic/utils.py +297 -0
- cognautic/websocket_server.py +485 -0
- cognautic_cli-1.1.1.dist-info/METADATA +604 -0
- cognautic_cli-1.1.1.dist-info/RECORD +25 -0
- cognautic_cli-1.1.1.dist-info/WHEEL +5 -0
- cognautic_cli-1.1.1.dist-info/entry_points.txt +2 -0
- cognautic_cli-1.1.1.dist-info/licenses/LICENSE +21 -0
- cognautic_cli-1.1.1.dist-info/top_level.txt +1 -0
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
|