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.
@@ -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
+ }