nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session Manager for NC1709
|
|
3
|
+
Handles conversation persistence and session management
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
10
|
+
from dataclasses import dataclass, field, asdict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Message:
|
|
15
|
+
"""Represents a single message in a conversation"""
|
|
16
|
+
role: str # 'user' or 'assistant'
|
|
17
|
+
content: str
|
|
18
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
19
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Session:
|
|
24
|
+
"""Represents a conversation session"""
|
|
25
|
+
id: str
|
|
26
|
+
name: str
|
|
27
|
+
messages: List[Message] = field(default_factory=list)
|
|
28
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
29
|
+
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
30
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
project_path: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
def add_message(self, role: str, content: str, metadata: Optional[Dict] = None):
|
|
34
|
+
"""Add a message to the session"""
|
|
35
|
+
message = Message(role=role, content=content, metadata=metadata or {})
|
|
36
|
+
self.messages.append(message)
|
|
37
|
+
self.updated_at = datetime.now().isoformat()
|
|
38
|
+
|
|
39
|
+
def get_history(self, limit: Optional[int] = None) -> List[Dict[str, str]]:
|
|
40
|
+
"""Get message history in format suitable for LLM
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
limit: Maximum number of messages to return
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of message dicts with 'role' and 'content'
|
|
47
|
+
"""
|
|
48
|
+
messages = self.messages[-limit:] if limit else self.messages
|
|
49
|
+
return [{"role": m.role, "content": m.content} for m in messages]
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
"""Convert session to dictionary"""
|
|
53
|
+
return {
|
|
54
|
+
"id": self.id,
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"messages": [asdict(m) for m in self.messages],
|
|
57
|
+
"created_at": self.created_at,
|
|
58
|
+
"updated_at": self.updated_at,
|
|
59
|
+
"metadata": self.metadata,
|
|
60
|
+
"project_path": self.project_path
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Session":
|
|
65
|
+
"""Create session from dictionary"""
|
|
66
|
+
messages = [Message(**m) for m in data.get("messages", [])]
|
|
67
|
+
return cls(
|
|
68
|
+
id=data["id"],
|
|
69
|
+
name=data["name"],
|
|
70
|
+
messages=messages,
|
|
71
|
+
created_at=data.get("created_at", datetime.now().isoformat()),
|
|
72
|
+
updated_at=data.get("updated_at", datetime.now().isoformat()),
|
|
73
|
+
metadata=data.get("metadata", {}),
|
|
74
|
+
project_path=data.get("project_path")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def to_markdown(self) -> str:
|
|
78
|
+
"""Export session as markdown
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Markdown formatted conversation
|
|
82
|
+
"""
|
|
83
|
+
lines = [
|
|
84
|
+
f"# Session: {self.name}",
|
|
85
|
+
f"",
|
|
86
|
+
f"**ID:** {self.id}",
|
|
87
|
+
f"**Created:** {self.created_at}",
|
|
88
|
+
f"**Updated:** {self.updated_at}",
|
|
89
|
+
f"",
|
|
90
|
+
"---",
|
|
91
|
+
""
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for msg in self.messages:
|
|
95
|
+
if msg.role == "user":
|
|
96
|
+
lines.append(f"## User ({msg.timestamp})")
|
|
97
|
+
else:
|
|
98
|
+
lines.append(f"## Assistant ({msg.timestamp})")
|
|
99
|
+
|
|
100
|
+
lines.append("")
|
|
101
|
+
lines.append(msg.content)
|
|
102
|
+
lines.append("")
|
|
103
|
+
lines.append("---")
|
|
104
|
+
lines.append("")
|
|
105
|
+
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SessionManager:
|
|
110
|
+
"""Manages conversation sessions"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, storage_path: Optional[str] = None):
|
|
113
|
+
"""Initialize session manager
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
storage_path: Path to store sessions
|
|
117
|
+
"""
|
|
118
|
+
if storage_path:
|
|
119
|
+
self.storage_path = Path(storage_path)
|
|
120
|
+
else:
|
|
121
|
+
self.storage_path = Path.home() / ".nc1709" / "sessions"
|
|
122
|
+
|
|
123
|
+
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
self.index_file = self.storage_path / "index.json"
|
|
125
|
+
|
|
126
|
+
# Currently active session
|
|
127
|
+
self.current_session: Optional[Session] = None
|
|
128
|
+
|
|
129
|
+
# Session index (id -> metadata)
|
|
130
|
+
self._index: Dict[str, Dict[str, Any]] = {}
|
|
131
|
+
self._load_index()
|
|
132
|
+
|
|
133
|
+
def create_session(
|
|
134
|
+
self,
|
|
135
|
+
name: Optional[str] = None,
|
|
136
|
+
project_path: Optional[str] = None
|
|
137
|
+
) -> Session:
|
|
138
|
+
"""Create a new session
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
name: Session name (auto-generated if None)
|
|
142
|
+
project_path: Associated project path
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
New Session instance
|
|
146
|
+
"""
|
|
147
|
+
session_id = str(uuid.uuid4())[:8]
|
|
148
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
149
|
+
|
|
150
|
+
if name is None:
|
|
151
|
+
name = f"session_{timestamp}"
|
|
152
|
+
|
|
153
|
+
session = Session(
|
|
154
|
+
id=session_id,
|
|
155
|
+
name=name,
|
|
156
|
+
project_path=project_path
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Update index
|
|
160
|
+
self._index[session_id] = {
|
|
161
|
+
"id": session_id,
|
|
162
|
+
"name": name,
|
|
163
|
+
"created_at": session.created_at,
|
|
164
|
+
"updated_at": session.updated_at,
|
|
165
|
+
"message_count": 0,
|
|
166
|
+
"project_path": project_path
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Save session
|
|
170
|
+
self._save_session(session)
|
|
171
|
+
self._save_index()
|
|
172
|
+
|
|
173
|
+
return session
|
|
174
|
+
|
|
175
|
+
def load_session(self, session_id: str) -> Optional[Session]:
|
|
176
|
+
"""Load a session by ID
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
session_id: Session ID
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Session or None if not found
|
|
183
|
+
"""
|
|
184
|
+
session_file = self.storage_path / f"{session_id}.json"
|
|
185
|
+
|
|
186
|
+
if not session_file.exists():
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
data = json.loads(session_file.read_text())
|
|
191
|
+
return Session.from_dict(data)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(f"Error loading session {session_id}: {e}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def save_session(self, session: Session):
|
|
197
|
+
"""Save a session
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
session: Session to save
|
|
201
|
+
"""
|
|
202
|
+
self._save_session(session)
|
|
203
|
+
|
|
204
|
+
# Update index
|
|
205
|
+
self._index[session.id] = {
|
|
206
|
+
"id": session.id,
|
|
207
|
+
"name": session.name,
|
|
208
|
+
"created_at": session.created_at,
|
|
209
|
+
"updated_at": session.updated_at,
|
|
210
|
+
"message_count": len(session.messages),
|
|
211
|
+
"project_path": session.project_path
|
|
212
|
+
}
|
|
213
|
+
self._save_index()
|
|
214
|
+
|
|
215
|
+
def delete_session(self, session_id: str) -> bool:
|
|
216
|
+
"""Delete a session
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
session_id: Session ID
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
True if deleted
|
|
223
|
+
"""
|
|
224
|
+
session_file = self.storage_path / f"{session_id}.json"
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
if session_file.exists():
|
|
228
|
+
session_file.unlink()
|
|
229
|
+
|
|
230
|
+
if session_id in self._index:
|
|
231
|
+
del self._index[session_id]
|
|
232
|
+
self._save_index()
|
|
233
|
+
|
|
234
|
+
return True
|
|
235
|
+
except Exception:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def list_sessions(
|
|
239
|
+
self,
|
|
240
|
+
limit: Optional[int] = None,
|
|
241
|
+
project_path: Optional[str] = None
|
|
242
|
+
) -> List[Dict[str, Any]]:
|
|
243
|
+
"""List all sessions
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
limit: Maximum number to return
|
|
247
|
+
project_path: Filter by project
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
List of session metadata dicts
|
|
251
|
+
"""
|
|
252
|
+
sessions = list(self._index.values())
|
|
253
|
+
|
|
254
|
+
# Filter by project
|
|
255
|
+
if project_path:
|
|
256
|
+
sessions = [s for s in sessions if s.get("project_path") == project_path]
|
|
257
|
+
|
|
258
|
+
# Sort by updated_at (newest first)
|
|
259
|
+
sessions.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
|
|
260
|
+
|
|
261
|
+
# Limit
|
|
262
|
+
if limit:
|
|
263
|
+
sessions = sessions[:limit]
|
|
264
|
+
|
|
265
|
+
return sessions
|
|
266
|
+
|
|
267
|
+
def get_latest_session(self, project_path: Optional[str] = None) -> Optional[Session]:
|
|
268
|
+
"""Get the most recent session
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
project_path: Filter by project
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Most recent session or None
|
|
275
|
+
"""
|
|
276
|
+
sessions = self.list_sessions(limit=1, project_path=project_path)
|
|
277
|
+
|
|
278
|
+
if sessions:
|
|
279
|
+
return self.load_session(sessions[0]["id"])
|
|
280
|
+
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def start_session(
|
|
284
|
+
self,
|
|
285
|
+
session_id: Optional[str] = None,
|
|
286
|
+
name: Optional[str] = None,
|
|
287
|
+
project_path: Optional[str] = None
|
|
288
|
+
) -> Session:
|
|
289
|
+
"""Start a session (load existing or create new)
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
session_id: Existing session ID to resume
|
|
293
|
+
name: Name for new session
|
|
294
|
+
project_path: Project path
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Active session
|
|
298
|
+
"""
|
|
299
|
+
if session_id:
|
|
300
|
+
session = self.load_session(session_id)
|
|
301
|
+
if session:
|
|
302
|
+
self.current_session = session
|
|
303
|
+
return session
|
|
304
|
+
|
|
305
|
+
# Create new session
|
|
306
|
+
self.current_session = self.create_session(name=name, project_path=project_path)
|
|
307
|
+
return self.current_session
|
|
308
|
+
|
|
309
|
+
def add_message(
|
|
310
|
+
self,
|
|
311
|
+
role: str,
|
|
312
|
+
content: str,
|
|
313
|
+
metadata: Optional[Dict] = None,
|
|
314
|
+
auto_save: bool = True
|
|
315
|
+
):
|
|
316
|
+
"""Add a message to the current session
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
role: 'user' or 'assistant'
|
|
320
|
+
content: Message content
|
|
321
|
+
metadata: Additional metadata
|
|
322
|
+
auto_save: Automatically save session
|
|
323
|
+
"""
|
|
324
|
+
if self.current_session is None:
|
|
325
|
+
self.current_session = self.create_session()
|
|
326
|
+
|
|
327
|
+
self.current_session.add_message(role, content, metadata)
|
|
328
|
+
|
|
329
|
+
if auto_save:
|
|
330
|
+
self.save_session(self.current_session)
|
|
331
|
+
|
|
332
|
+
def get_current_history(self, limit: Optional[int] = None) -> List[Dict[str, str]]:
|
|
333
|
+
"""Get history from current session
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
limit: Maximum messages
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Message history for LLM
|
|
340
|
+
"""
|
|
341
|
+
if self.current_session is None:
|
|
342
|
+
return []
|
|
343
|
+
|
|
344
|
+
return self.current_session.get_history(limit)
|
|
345
|
+
|
|
346
|
+
def export_session(
|
|
347
|
+
self,
|
|
348
|
+
session_id: str,
|
|
349
|
+
output_path: str,
|
|
350
|
+
format: str = "markdown"
|
|
351
|
+
) -> bool:
|
|
352
|
+
"""Export a session to file
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
session_id: Session ID
|
|
356
|
+
output_path: Output file path
|
|
357
|
+
format: Export format ('markdown' or 'json')
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
True if successful
|
|
361
|
+
"""
|
|
362
|
+
session = self.load_session(session_id)
|
|
363
|
+
|
|
364
|
+
if session is None:
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
output_file = Path(output_path)
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
if format == "markdown":
|
|
371
|
+
content = session.to_markdown()
|
|
372
|
+
else:
|
|
373
|
+
content = json.dumps(session.to_dict(), indent=2)
|
|
374
|
+
|
|
375
|
+
output_file.write_text(content)
|
|
376
|
+
return True
|
|
377
|
+
except Exception as e:
|
|
378
|
+
print(f"Error exporting session: {e}")
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
def search_sessions(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
382
|
+
"""Search sessions by content
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
query: Search query
|
|
386
|
+
limit: Maximum results
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
List of matching sessions with context
|
|
390
|
+
"""
|
|
391
|
+
results = []
|
|
392
|
+
query_lower = query.lower()
|
|
393
|
+
|
|
394
|
+
for session_meta in self.list_sessions():
|
|
395
|
+
session = self.load_session(session_meta["id"])
|
|
396
|
+
if session is None:
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
# Search in messages
|
|
400
|
+
matches = []
|
|
401
|
+
for i, msg in enumerate(session.messages):
|
|
402
|
+
if query_lower in msg.content.lower():
|
|
403
|
+
matches.append({
|
|
404
|
+
"message_index": i,
|
|
405
|
+
"role": msg.role,
|
|
406
|
+
"preview": msg.content[:200]
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
if matches:
|
|
410
|
+
results.append({
|
|
411
|
+
"session": session_meta,
|
|
412
|
+
"matches": matches[:3] # Limit matches per session
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
return results[:limit]
|
|
416
|
+
|
|
417
|
+
def _save_session(self, session: Session):
|
|
418
|
+
"""Save session to disk"""
|
|
419
|
+
session_file = self.storage_path / f"{session.id}.json"
|
|
420
|
+
session_file.write_text(json.dumps(session.to_dict(), indent=2))
|
|
421
|
+
|
|
422
|
+
def _load_index(self):
|
|
423
|
+
"""Load session index from disk"""
|
|
424
|
+
if self.index_file.exists():
|
|
425
|
+
try:
|
|
426
|
+
self._index = json.loads(self.index_file.read_text())
|
|
427
|
+
except Exception:
|
|
428
|
+
self._index = {}
|
|
429
|
+
|
|
430
|
+
def _save_index(self):
|
|
431
|
+
"""Save session index to disk"""
|
|
432
|
+
self.index_file.write_text(json.dumps(self._index, indent=2))
|