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
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket server for real-time communication
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import uuid
|
|
9
|
+
import websockets
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
from websockets.server import WebSocketServerProtocol
|
|
13
|
+
from .ai_engine import AIEngine
|
|
14
|
+
|
|
15
|
+
# Suppress websocket server logging in chat mode
|
|
16
|
+
logging.getLogger(__name__).setLevel(logging.ERROR)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class SessionManager:
|
|
20
|
+
"""Manages WebSocket sessions"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.sessions: Dict[str, Dict[str, Any]] = {}
|
|
24
|
+
self.connections: Dict[str, websockets.server.WebSocketServerProtocol] = {}
|
|
25
|
+
|
|
26
|
+
def create_session(self, websocket: WebSocketServerProtocol) -> str:
|
|
27
|
+
"""Create a new session"""
|
|
28
|
+
session_id = str(uuid.uuid4())
|
|
29
|
+
|
|
30
|
+
self.sessions[session_id] = {
|
|
31
|
+
'id': session_id,
|
|
32
|
+
'created_at': datetime.now().isoformat(),
|
|
33
|
+
'last_activity': datetime.now().isoformat(),
|
|
34
|
+
'context': [],
|
|
35
|
+
'project_path': None,
|
|
36
|
+
'ai_provider': 'openai',
|
|
37
|
+
'ai_model': None
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
self.connections[session_id] = websocket
|
|
41
|
+
return session_id
|
|
42
|
+
|
|
43
|
+
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
44
|
+
"""Get session by ID"""
|
|
45
|
+
return self.sessions.get(session_id)
|
|
46
|
+
|
|
47
|
+
def update_session(self, session_id: str, **kwargs):
|
|
48
|
+
"""Update session data"""
|
|
49
|
+
if session_id in self.sessions:
|
|
50
|
+
self.sessions[session_id].update(kwargs)
|
|
51
|
+
self.sessions[session_id]['last_activity'] = datetime.now().isoformat()
|
|
52
|
+
|
|
53
|
+
def remove_session(self, session_id: str):
|
|
54
|
+
"""Remove a session"""
|
|
55
|
+
self.sessions.pop(session_id, None)
|
|
56
|
+
self.connections.pop(session_id, None)
|
|
57
|
+
|
|
58
|
+
def get_connection(self, session_id: str) -> Optional[WebSocketServerProtocol]:
|
|
59
|
+
"""Get WebSocket connection for session"""
|
|
60
|
+
return self.connections.get(session_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MessageHandler:
|
|
64
|
+
"""Handles WebSocket messages"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, ai_engine: AIEngine, session_manager: SessionManager):
|
|
67
|
+
self.ai_engine = ai_engine
|
|
68
|
+
self.session_manager = session_manager
|
|
69
|
+
|
|
70
|
+
async def handle_message(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
"""Handle incoming message"""
|
|
72
|
+
|
|
73
|
+
message_type = message.get('type')
|
|
74
|
+
|
|
75
|
+
handlers = {
|
|
76
|
+
'chat': self._handle_chat,
|
|
77
|
+
'tool_execute': self._handle_tool_execute,
|
|
78
|
+
'session_config': self._handle_session_config,
|
|
79
|
+
'typing_speed': self._handle_typing_speed,
|
|
80
|
+
'project_analyze': self._handle_project_analyze,
|
|
81
|
+
'project_build': self._handle_project_build
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
handler = handlers.get(message_type)
|
|
85
|
+
if not handler:
|
|
86
|
+
return {
|
|
87
|
+
'type': 'error',
|
|
88
|
+
'error': f'Unknown message type: {message_type}'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
return await handler(session_id, message)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return {
|
|
95
|
+
'type': 'error',
|
|
96
|
+
'error': str(e)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async def _handle_chat(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
100
|
+
"""Handle chat message with real-time streaming"""
|
|
101
|
+
|
|
102
|
+
session = self.session_manager.get_session(session_id)
|
|
103
|
+
if not session:
|
|
104
|
+
return {'type': 'error', 'error': 'Session not found'}
|
|
105
|
+
|
|
106
|
+
user_message = message.get('message', '')
|
|
107
|
+
stream_enabled = message.get('stream', True) # Enable streaming by default
|
|
108
|
+
|
|
109
|
+
if not user_message:
|
|
110
|
+
return {'type': 'error', 'error': 'Empty message'}
|
|
111
|
+
|
|
112
|
+
# Add user message to context
|
|
113
|
+
session['context'].append({
|
|
114
|
+
'role': 'user',
|
|
115
|
+
'content': user_message,
|
|
116
|
+
'timestamp': datetime.now().isoformat()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
# Get WebSocket connection for streaming
|
|
120
|
+
websocket = self.session_manager.get_connection(session_id)
|
|
121
|
+
|
|
122
|
+
if stream_enabled and websocket:
|
|
123
|
+
# Stream response in real-time
|
|
124
|
+
return await self._handle_chat_stream(session_id, session, user_message, websocket)
|
|
125
|
+
else:
|
|
126
|
+
# Non-streaming response (legacy mode)
|
|
127
|
+
return await self._handle_chat_complete(session_id, session, user_message)
|
|
128
|
+
|
|
129
|
+
async def _handle_chat_stream(self, session_id: str, session: Dict[str, Any], user_message: str, websocket: WebSocketServerProtocol) -> Dict[str, Any]:
|
|
130
|
+
"""Handle chat message with real-time character-by-character streaming"""
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Get typing speed from session config (default: 0.001s = fast)
|
|
134
|
+
typing_delay = session.get('typing_delay', 0.001)
|
|
135
|
+
|
|
136
|
+
# Send stream start notification
|
|
137
|
+
await websocket.send(json.dumps({
|
|
138
|
+
'type': 'stream_start',
|
|
139
|
+
'session_id': session_id,
|
|
140
|
+
'timestamp': datetime.now().isoformat()
|
|
141
|
+
}))
|
|
142
|
+
|
|
143
|
+
# Collect full response for context
|
|
144
|
+
full_response = ""
|
|
145
|
+
|
|
146
|
+
# Stream AI response character by character
|
|
147
|
+
async for chunk in self.ai_engine.process_message_stream(
|
|
148
|
+
message=user_message,
|
|
149
|
+
provider=session.get('ai_provider'),
|
|
150
|
+
model=session.get('ai_model'),
|
|
151
|
+
project_path=session.get('project_path'),
|
|
152
|
+
conversation_history=session.get('context', [])
|
|
153
|
+
):
|
|
154
|
+
full_response += chunk
|
|
155
|
+
|
|
156
|
+
# Send each CHARACTER immediately for typewriter effect
|
|
157
|
+
for char in chunk:
|
|
158
|
+
await websocket.send(json.dumps({
|
|
159
|
+
'type': 'stream_chunk',
|
|
160
|
+
'chunk': char, # Single character
|
|
161
|
+
'session_id': session_id
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
# Small delay for smooth typewriter effect (configurable)
|
|
165
|
+
if typing_delay > 0:
|
|
166
|
+
await asyncio.sleep(typing_delay)
|
|
167
|
+
|
|
168
|
+
# Send stream end notification
|
|
169
|
+
await websocket.send(json.dumps({
|
|
170
|
+
'type': 'stream_end',
|
|
171
|
+
'session_id': session_id,
|
|
172
|
+
'timestamp': datetime.now().isoformat()
|
|
173
|
+
}))
|
|
174
|
+
|
|
175
|
+
# Add AI response to context
|
|
176
|
+
session['context'].append({
|
|
177
|
+
'role': 'assistant',
|
|
178
|
+
'content': full_response,
|
|
179
|
+
'timestamp': datetime.now().isoformat()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
# Update session
|
|
183
|
+
self.session_manager.update_session(session_id, context=session['context'])
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
'type': 'chat_response',
|
|
187
|
+
'message': full_response,
|
|
188
|
+
'session_id': session_id,
|
|
189
|
+
'streamed': True
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
# Send error notification
|
|
194
|
+
await websocket.send(json.dumps({
|
|
195
|
+
'type': 'stream_error',
|
|
196
|
+
'error': str(e),
|
|
197
|
+
'session_id': session_id
|
|
198
|
+
}))
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
'type': 'error',
|
|
202
|
+
'error': f'AI processing failed: {str(e)}'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async def _handle_chat_complete(self, session_id: str, session: Dict[str, Any], user_message: str) -> Dict[str, Any]:
|
|
206
|
+
"""Handle chat message with complete (non-streaming) response"""
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
ai_response = await self.ai_engine.process_message(
|
|
210
|
+
message=user_message,
|
|
211
|
+
provider=session.get('ai_provider'),
|
|
212
|
+
model=session.get('ai_model'),
|
|
213
|
+
project_path=session.get('project_path'),
|
|
214
|
+
context=session['context']
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Add AI response to context
|
|
218
|
+
session['context'].append({
|
|
219
|
+
'role': 'assistant',
|
|
220
|
+
'content': ai_response,
|
|
221
|
+
'timestamp': datetime.now().isoformat()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
# Update session
|
|
225
|
+
self.session_manager.update_session(session_id, context=session['context'])
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
'type': 'chat_response',
|
|
229
|
+
'message': ai_response,
|
|
230
|
+
'session_id': session_id,
|
|
231
|
+
'streamed': False
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
return {
|
|
236
|
+
'type': 'error',
|
|
237
|
+
'error': f'AI processing failed: {str(e)}'
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async def _handle_tool_execute(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
241
|
+
"""Handle tool execution request"""
|
|
242
|
+
|
|
243
|
+
tool_name = message.get('tool_name')
|
|
244
|
+
operation = message.get('operation')
|
|
245
|
+
parameters = message.get('parameters', {})
|
|
246
|
+
|
|
247
|
+
if not tool_name or not operation:
|
|
248
|
+
return {
|
|
249
|
+
'type': 'error',
|
|
250
|
+
'error': 'Missing tool_name or operation'
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
# Execute tool through AI engine's tool registry
|
|
255
|
+
result = await self.ai_engine.tool_registry.execute_tool(
|
|
256
|
+
tool_name=tool_name,
|
|
257
|
+
operation=operation,
|
|
258
|
+
**parameters
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
'type': 'tool_result',
|
|
263
|
+
'tool_name': tool_name,
|
|
264
|
+
'operation': operation,
|
|
265
|
+
'result': result.to_dict()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return {
|
|
270
|
+
'type': 'error',
|
|
271
|
+
'error': f'Tool execution failed: {str(e)}'
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async def _handle_session_config(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
275
|
+
"""Handle session configuration"""
|
|
276
|
+
|
|
277
|
+
config = message.get('config', {})
|
|
278
|
+
|
|
279
|
+
# Update session with new configuration
|
|
280
|
+
self.session_manager.update_session(session_id, **config)
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
'type': 'session_updated',
|
|
284
|
+
'session_id': session_id,
|
|
285
|
+
'config': config
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async def _handle_typing_speed(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
289
|
+
"""Handle typing speed configuration"""
|
|
290
|
+
|
|
291
|
+
speed = message.get('speed', 'fast')
|
|
292
|
+
|
|
293
|
+
# Speed presets (in seconds per character)
|
|
294
|
+
speed_map = {
|
|
295
|
+
'instant': 0,
|
|
296
|
+
'fast': 0.001, # ~1000 chars/sec
|
|
297
|
+
'normal': 0.005, # ~200 chars/sec
|
|
298
|
+
'slow': 0.01 # ~100 chars/sec
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Get delay value
|
|
302
|
+
if isinstance(speed, (int, float)):
|
|
303
|
+
typing_delay = float(speed)
|
|
304
|
+
else:
|
|
305
|
+
typing_delay = speed_map.get(speed, 0.001)
|
|
306
|
+
|
|
307
|
+
# Update session
|
|
308
|
+
self.session_manager.update_session(session_id, typing_delay=typing_delay)
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
'type': 'typing_speed_updated',
|
|
312
|
+
'session_id': session_id,
|
|
313
|
+
'speed': speed,
|
|
314
|
+
'delay': typing_delay
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async def _handle_project_analyze(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
318
|
+
"""Handle project analysis request"""
|
|
319
|
+
|
|
320
|
+
project_path = message.get('project_path')
|
|
321
|
+
options = message.get('options', {})
|
|
322
|
+
|
|
323
|
+
if not project_path:
|
|
324
|
+
return {
|
|
325
|
+
'type': 'error',
|
|
326
|
+
'error': 'Missing project_path'
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
result = await self.ai_engine.analyze_project(
|
|
331
|
+
project_path=project_path,
|
|
332
|
+
**options
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
'type': 'project_analysis',
|
|
337
|
+
'project_path': project_path,
|
|
338
|
+
'result': result
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return {
|
|
343
|
+
'type': 'error',
|
|
344
|
+
'error': f'Project analysis failed: {str(e)}'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async def _handle_project_build(self, session_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
348
|
+
"""Handle project build request"""
|
|
349
|
+
|
|
350
|
+
description = message.get('description')
|
|
351
|
+
options = message.get('options', {})
|
|
352
|
+
|
|
353
|
+
if not description:
|
|
354
|
+
return {
|
|
355
|
+
'type': 'error',
|
|
356
|
+
'error': 'Missing project description'
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
result = await self.ai_engine.build_project(
|
|
361
|
+
description=description,
|
|
362
|
+
**options
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
'type': 'project_build',
|
|
367
|
+
'description': description,
|
|
368
|
+
'result': result
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
except Exception as e:
|
|
372
|
+
return {
|
|
373
|
+
'type': 'error',
|
|
374
|
+
'error': f'Project build failed: {str(e)}'
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class WebSocketServer:
|
|
379
|
+
"""WebSocket server for real-time AI interaction"""
|
|
380
|
+
|
|
381
|
+
def __init__(self, ai_engine: AIEngine, host: str = "localhost", port: int = 8765):
|
|
382
|
+
self.ai_engine = ai_engine
|
|
383
|
+
self.host = host
|
|
384
|
+
self.port = port
|
|
385
|
+
self.session_manager = SessionManager()
|
|
386
|
+
self.message_handler = MessageHandler(ai_engine, self.session_manager)
|
|
387
|
+
self.server = None
|
|
388
|
+
|
|
389
|
+
# Setup logging
|
|
390
|
+
logging.basicConfig(level=logging.INFO)
|
|
391
|
+
self.logger = logging.getLogger(__name__)
|
|
392
|
+
|
|
393
|
+
async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
|
|
394
|
+
"""Handle new WebSocket client connection"""
|
|
395
|
+
|
|
396
|
+
# Create session for new connection
|
|
397
|
+
session_id = self.session_manager.create_session(websocket)
|
|
398
|
+
self.logger.info(f"New client connected: {session_id}")
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
# Send welcome message
|
|
402
|
+
welcome_message = {
|
|
403
|
+
'type': 'welcome',
|
|
404
|
+
'session_id': session_id,
|
|
405
|
+
'message': 'Connected to Cognautic AI Assistant',
|
|
406
|
+
'available_providers': self.ai_engine.get_available_providers(),
|
|
407
|
+
'features': {
|
|
408
|
+
'streaming': True,
|
|
409
|
+
'real_time': True,
|
|
410
|
+
'tools': True
|
|
411
|
+
},
|
|
412
|
+
'server_version': '2.0.0'
|
|
413
|
+
}
|
|
414
|
+
await websocket.send(json.dumps(welcome_message))
|
|
415
|
+
|
|
416
|
+
# Handle messages
|
|
417
|
+
async for message in websocket:
|
|
418
|
+
try:
|
|
419
|
+
data = json.loads(message)
|
|
420
|
+
message_type = data.get('type', 'unknown')
|
|
421
|
+
self.logger.info(f"Received message from {session_id}: {message_type}")
|
|
422
|
+
|
|
423
|
+
# For chat messages with streaming, handle in a special way
|
|
424
|
+
if message_type == 'chat' and data.get('stream', True):
|
|
425
|
+
# Process with streaming - responses are sent during processing
|
|
426
|
+
response = await self.message_handler.handle_message(session_id, data)
|
|
427
|
+
# Only send final confirmation after streaming is complete
|
|
428
|
+
if response.get('type') != 'chat_response' or not response.get('streamed'):
|
|
429
|
+
await websocket.send(json.dumps(response))
|
|
430
|
+
else:
|
|
431
|
+
# Process message normally
|
|
432
|
+
response = await self.message_handler.handle_message(session_id, data)
|
|
433
|
+
# Send response
|
|
434
|
+
await websocket.send(json.dumps(response))
|
|
435
|
+
|
|
436
|
+
except json.JSONDecodeError:
|
|
437
|
+
error_response = {
|
|
438
|
+
'type': 'error',
|
|
439
|
+
'error': 'Invalid JSON message'
|
|
440
|
+
}
|
|
441
|
+
await websocket.send(json.dumps(error_response))
|
|
442
|
+
|
|
443
|
+
except Exception as e:
|
|
444
|
+
self.logger.error(f"Error handling message from {session_id}: {str(e)}")
|
|
445
|
+
error_response = {
|
|
446
|
+
'type': 'error',
|
|
447
|
+
'error': f'Message handling failed: {str(e)}'
|
|
448
|
+
}
|
|
449
|
+
await websocket.send(json.dumps(error_response))
|
|
450
|
+
|
|
451
|
+
except websockets.exceptions.ConnectionClosed:
|
|
452
|
+
self.logger.info(f"Client disconnected: {session_id}")
|
|
453
|
+
|
|
454
|
+
except Exception as e:
|
|
455
|
+
self.logger.error(f"Connection error for {session_id}: {str(e)}")
|
|
456
|
+
|
|
457
|
+
finally:
|
|
458
|
+
# Clean up session
|
|
459
|
+
self.session_manager.remove_session(session_id)
|
|
460
|
+
|
|
461
|
+
async def start(self):
|
|
462
|
+
"""Start the WebSocket server"""
|
|
463
|
+
# Start server silently in chat mode
|
|
464
|
+
self.server = await websockets.serve(
|
|
465
|
+
self.handle_client,
|
|
466
|
+
self.host,
|
|
467
|
+
self.port
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Keep server running
|
|
471
|
+
await self.server.wait_closed()
|
|
472
|
+
|
|
473
|
+
async def stop(self):
|
|
474
|
+
"""Stop the WebSocket server"""
|
|
475
|
+
if self.server:
|
|
476
|
+
self.server.close()
|
|
477
|
+
await self.server.wait_closed()
|
|
478
|
+
self.logger.info("WebSocket server stopped")
|
|
479
|
+
|
|
480
|
+
def get_session_info(self) -> Dict[str, Any]:
|
|
481
|
+
"""Get information about active sessions"""
|
|
482
|
+
return {
|
|
483
|
+
'total_sessions': len(self.session_manager.sessions),
|
|
484
|
+
'sessions': list(self.session_manager.sessions.keys())
|
|
485
|
+
}
|