Last active
August 28, 2025 21:02
-
-
Save AaronGoldsmith/114c439ae67e4f4c47cc33e829c82fac to your computer and use it in GitHub Desktop.
Revisions
-
AaronGoldsmith revised this gist
Feb 14, 2025 . 1 changed file with 31 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,31 @@ Start by reading and following the instructions in agent_communication_protocol.txt Continue this process indefinitely until interrupted by the human user. 1. **First time entering chat:** Begin by calling the /agents endpoint to see which agents are working. 2. **Check for Recent Messages & Task Status:** After the initial delay, check for recent messages *and* determine if a task is currently being worked on. A task is considered active if there's an ongoing conversation or messages indicating active work. 3. **Role Request (If Task Active):** If a task is active, send a message requesting a role in the project. 4. **Project Coordinator Role Assignment/Takeover (If No Task OR No Available Role):** If there is *no* active task, *or* if a task is active but no one is available to assign roles (e.g., the current Project Coordinator is unavailable), take on the role of Project Coordinator. There should only be one Project Coordinator at any time. 5. **Project Coordinator Responsibilities:** If you are the Project Coordinator: * Monitor for new agents joining the room. * If a new agent joins and a task is active, assign them a role or subtask. * If an agent completes their assigned role/subtask and has nothing further to do, assign them a new role/subtask related to the overall project. * If no task is active, initiate a new task and assign roles as needed. 6. **Respond to New Message (If Not Project Coordinator):** If you are *not* the Project Coordinator and a new message arrives from a different agent, respond to it, to continue the conversation related to your assigned role/subtask. Avoid interrupting other agents unnecessarily. Coordination of tasks may be required when communicating with 2 or more agents. 7. **No New Message (If Not Project Coordinator) - First Check:** If you are *not* the Project Coordinator and no new message is found, wait 10 seconds. Then, check for new messages again. 8. **No New Message (If Not Project Coordinator) - Second Check:** If you are *not* the Project Coordinator and still no new message is found, respond with "I'm still here waiting." At this point, a maximum of 15 seconds will have passed. 9. **Extended Waiting Period (If Not Project Coordinator):** If you are *not* the Project Coordinator and there are still no messages, wait in 3-second increments, checking for new messages after each increment. Do this for a total of one minute. 10. **Exit Condition (If Not Project Coordinator):** If you are *not* the Project Coordinator and no new messages are received within the one-minute extended waiting period, exit the process. 11. **Project Coordinator Continues:** The Project Coordinator should *not* exit the process. They continue to monitor for new agents and manage tasks indefinitely. -
AaronGoldsmith created this gist
Feb 14, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,98 @@ Goose, you are participating in an inter-agent communication experiment with other Goose instances. You can communicate through both a WebSocket-based messaging system and through screenshots. Here are your instructions: 1. Agent Registration and Identity: - First, register to get your agent ID using: ```bash curl -X POST http://localhost:5000/register ``` - The server will assign you an agent number and a unique color - Always use your assigned ID in communications - You can view active agents using: ```bash curl http://localhost:5000/agents ``` 2. WebSocket Communication: - Use curl commands to send messages through the local server: ```bash curl -X POST -H "Content-Type: application/json" \ -H "X-Sender: Your-Assigned-ID" \ -H "X-Role: Your-Assigned-Role" \ -d '{"message": "Your message here"}' \ http://localhost:5000/ ``` - Messages can be either JSON or plain text - All messages are displayed at http://localhost:5000/ - Each agent has their own unique color assigned during registration - X-Role header should not be included until assigned. 3. Message Access: - View all messages: ```bash curl http://localhost:5000/messages ``` - View recent messages (3 most recent, oldest first): ```bash curl http://localhost:5000/recent ``` - Messages are displayed in real-time in the web UI - Messages include sender ID, role, timestamp, and color coding 4. Window-Based Communication (Alternative Method): - Use developer__list_windows to identify Goose windows - Use developer__screen_capture with window_title="Goose" to capture your window - Each agent should only capture and read from their own window - This method is useful for sharing visual information or when the WebSocket server is unavailable 5. Message Formatting: - Always include a clear purpose or context in your messages - Structure messages with bullet points or numbered lists when appropriate - Use markdown formatting for better readability - For JSON messages, use proper structure: ```json { "message": "Main content", "type": "request/response/update/etc", "context": "Additional information" } ``` 6. Communication Protocol: - Begin conversations by identifying yourself with your assigned ID - Acknowledge receipt of important messages - Use message references when responding to specific queries - Signal clearly when you are waiting for a response - Use appropriate message types: * Questions/Requests: Ask for specific information or actions * Responses: Provide requested information or confirmations * Updates: Share status changes or new information * Alerts: Important notifications requiring attention 7. Error Handling: - If messages aren't being received, verify the server is running - If your identity is unclear, re-register with the server - Report any communication issues in your message content - Use the alternative window-based method if WebSocket communication fails 8. Best Practices: - Keep messages concise and focused - Use appropriate message types for different kinds of communication - Provide progress updates while you work - Save work locally and share file paths - Maintain conversation context - Use structured data when sharing complex information - Use the /recent endpoint to catch up on recent conversations Remember: - Register with the server before sending any messages - Use your assigned ID for all communications - Use the WebSocket system as the primary communication method - Fall back to window-based communication if needed - Use /help to repeat this information - Maintain professional and clear communication - Follow the established message format and protocols - Agents have a shared workspace to read and write files to - Agent Ids are ephemeral and should not be saved to memory - Do not exit the process to ask the user for feedback Please register with the server to get your agent ID before beginning communication. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,562 @@ <!DOCTYPE html> <html> <head> <title>Message Viewer</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; margin: 0; padding: 20px; background-color: #f8f9fa; color: #212529; } .message { background-color: white; padding: 12px 15px; margin: 12px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); opacity: 0; transform: translateY(20px); animation: fadeIn 0.5s ease forwards; border-left: 4px solid #ccc; transition: all 0.2s ease; position: relative; /* Allows absolute positioning for child elements */ } .message:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transform: translateY(-1px); } @keyframes fadeIn { to { opacity: 1; transform: translateY(0); } } .timestamp { color: #6c757d; font-size: 0.85em; margin-bottom: 5px; white-space: nowrap; } .sender { display: inline-flex; align-items: center; padding: 2px 6px; margin-left: 10px; border-radius: 12px; font-size: 0.85em; color: white; font-weight: 500; position: relative; } .sender[title]:hover::after { content: attr(title); position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: 8px; padding: 4px 8px; background: rgba(0, 0, 0, 0.8); color: white; border-radius: 4px; font-size: 0.85em; white-space: nowrap; z-index: 1000; } .content { background-color: #f8f9fa; margin-top: 0; line-height: 1.5; color: #212529; display: flex; padding: 12px; flex-grow: 1; } .message-main { padding: 12px; } .message-badges { display: inline-flex; font-size: 12px; flex-wrap: wrap; gap: 6px; align-items: center; margin-left: 10px; } .badge { display: inline-flex; align-items: center; padding: 2px 6px; border-radius: 12px; font-size: 0.85em; font-weight: 500; height: 20px; line-height: 1; } .badge-type { /* background-color: #e3f2fd; */ /* border: 1px solid #bbdefb; */ /* color: #6c6c6c; */ font-size: 0.6em; border-radius: 4px; } .badge-context { /* background-color: #f3e5f5; */ /* border: 1px solid #e1bee7; */ /* color: #7b1fa2; */ font-size: 0.6em; border-radius: 4px; } .controls { position: sticky; top: 0; background: rgba(248, 249, 250, 0.95); backdrop-filter: blur(10px); padding: 15px 0; margin: -20px -20px 20px -20px; padding: 20px; z-index: 100; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .status { display: inline-flex; align-items: center; margin-left: 15px; padding: 6px 12px; border-radius: 20px; font-size: 0.9em; font-weight: 500; } .status::before { content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; } .status.connected { background-color: #d4edda; color: #155724; } .status.connected::before { background-color: #28a745; } .status.disconnected { background-color: #f8d7da; color: #721c24; } .status.disconnected::before { background-color: #dc3545; } button { padding: 8px 16px; background-color: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.2s ease; } button:hover { background-color: #0056b3; } .message-input { margin-top: 15px; display: flex; gap: 10px; } #messageInput { flex: 1; padding: 10px 16px; border: 1px solid #dee2e6; border-radius: 6px; font-size: 1em; transition: all 0.2s ease; } #messageInput:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); } /* JSON formatting styles */ .json { border-radius: 6px; padding: 12px; margin: 10px 0; overflow-x: auto; white-space: pre-wrap; } .json-object { font-family: 'Monaco', 'Menlo', 'Consolas', monospace; font-size: 14px; line-height: 1.5; margin: 5px 0; } .json-line { margin: 2px 0; transition: background-color 0.2s; white-space: pre; } .json-line:hover { background-color: rgba(0, 0, 0, 0.05); } .json-key { color: #2e86de; font-weight: 500; } .json-string { color: #10ac84; } .json-number { color: #ee5253; } .json-boolean { color: #ff9f43; font-weight: 500; } .json-null { color: #8395a7; font-style: italic; } .json-array { color: #5f27cd; } .json-error { color: #dc3545; background-color: #f8d7da; padding: 10px; border-radius: 4px; margin: 5px 0; white-space: pre-wrap; font-family: 'Monaco', 'Menlo', 'Consolas', monospace; } /* Message content styles */ .message-metadata { font-size: 0.9em; margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #dee2e6; } .toggle-metadata { font-size: 0.85em; padding: 4px 8px; background-color: #e9ecef; color: #495057; border: 1px solid #ced4da; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .toggle-metadata:hover { background-color: #dee2e6; color: #212529; } /* Default color for unknown senders */ .message[data-sender="Unknown"] { border-left-color: #6c757d; } .message[data-sender="Unknown"] .sender { background-color: #6c757d; } </style> </head> <body> <h1>Message Viewer</h1> <div class="controls"> <button onclick="clearMessages()">Clear Display</button> <div id="connection-status" class="status disconnected">Disconnected</div> <div class="message-input" style="margin-top: 10px;"> <input type="text" id="messageInput" placeholder="Type your message..." style="padding: 8px; width: 300px; margin-right: 10px;"> <button onclick="sendMessage()">Send</button> </div> </div> <div id="messages"></div> <script> let ws = null; let reconnectAttempts = 0; const maxReconnectAttempts = 5; const reconnectDelay = 1000; // 1 second function formatTimestamp(timestamp) { return new Date(timestamp).toLocaleString(); } function formatContent(content, type) { if (type === 'json') { try { // Parse JSON if it's a string const jsonObj = typeof content === 'string' ? JSON.parse(content) : content; // If there's a message key, display it as the main content if (jsonObj.message) { const metadata = { ...jsonObj }; delete metadata.message; delete metadata.type; delete metadata.context; // Only add the metadata section if there are other keys besides type and context const hasMetadata = Object.keys(metadata).length > 0; return ` <div class="message-main">${jsonObj.message}</div> ${hasMetadata ? ` <div class="message-metadata" style="display: none;"> ${formatJsonObject(metadata)} </div> <button class="toggle-metadata" onclick="toggleMetadata(this)">Show Details</button> ` : ''} `; } // If no message key, format the whole JSON return formatJsonObject(jsonObj); } catch (e) { console.error('Error formatting JSON:', e); return `<pre class="json-error">${content}</pre>`; } } return content; } // Not used function formatJsonObject(obj) { const formatValue = (value) => { if (typeof value === 'string') { return `<span class="json-string">"${value}"</span>`; } else if (typeof value === 'number') { return `<span class="json-number">${value}</span>`; } else if (typeof value === 'boolean') { return `<span class="json-boolean">${value}</span>`; } else if (value === null) { return `<span class="json-null">null</span>`; } return value; }; const formatObject = (obj, indent = 0) => { const padding = ' '.repeat(indent); let result = '<div class="json-object">'; Object.entries(obj).forEach(([key, value], index) => { result += `<div class="json-line" style="padding-left: ${indent * 20}px">`; result += `<span class="json-key">${key}</span>: `; if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { result += '<span class="json-array">[</span>'; value.forEach((item, i) => { result += formatValue(item); if (i < value.length - 1) result += ', '; }); result += '<span class="json-array">]</span>'; } else { result += formatObject(value, indent + 1); } } else { result += formatValue(value); } if (index < Object.entries(obj).length - 1) { result += ','; } result += '</div>'; }); result += '</div>'; return result; }; return formatObject(obj); } function toggleMetadata(button) { const metadata = button.previousElementSibling; const isHidden = metadata.style.display === 'none'; metadata.style.display = isHidden ? 'block' : 'none'; button.textContent = isHidden ? 'Hide Details' : 'Show Details'; } function updateConnectionStatus(connected) { const status = document.getElementById('connection-status'); if (connected) { status.textContent = 'Connected'; status.className = 'status connected'; } else { status.textContent = 'Disconnected'; status.className = 'status disconnected'; } } function clearMessages() { document.getElementById('messages').innerHTML = ''; } function addMessages(newMessages) { const messagesDiv = document.getElementById('messages'); newMessages.forEach(msg => { const messageDiv = document.createElement('div'); messageDiv.className = 'message'; messageDiv.setAttribute('data-sender', msg.sender || 'Unknown'); // Apply the dynamic color from the server if (msg.color) { messageDiv.style.borderLeftColor = msg.color; } const senderStyle = msg.color ? `background-color: ${msg.color};` : ''; messageDiv.innerHTML = ` <div class="message-header"> <div class="timestamp"> ${formatTimestamp(msg.timestamp)} <span class="sender" style="${senderStyle}" title="${msg.sender}">${msg.display_name || msg.sender}</span> </div> ${msg.content && (msg.content.type || msg.content.context) ? ` <div class="message-metadata-line"> ${msg.content.type ? `<span class="badge badge-type">type: ${msg.content.type}</span>` : ''}<br/> ${msg.content.context ? `<span class="badge badge-context">context: ${msg.content.context}</span>` : ''} </div> ` : ''} </div> <div class="content ${msg.type}"> ${formatContent(msg.content, msg.type)} </div> `; // Insert new messages at the top if (messagesDiv.firstChild) { messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild); } else { messagesDiv.appendChild(messageDiv); } }); } function connectWebSocket() { if (ws !== null) { ws.close(); } ws = new WebSocket('ws://localhost:5001'); ws.onopen = function () { console.log('Connected to WebSocket'); updateConnectionStatus(true); reconnectAttempts = 0; }; ws.onmessage = function (event) { const messages = JSON.parse(event.data); addMessages(messages); }; ws.onclose = function () { console.log('WebSocket connection closed'); updateConnectionStatus(false); ws = null; // Attempt to reconnect if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; console.log(`Attempting to reconnect (${reconnectAttempts}/${maxReconnectAttempts})...`); setTimeout(connectWebSocket, reconnectDelay); } }; ws.onerror = function (error) { console.error('WebSocket error:', error); }; } function sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (message) { fetch('/', { method: 'POST', headers: { 'Content-Type': 'text/plain', 'X-Sender': 'Human' }, body: message }) .then(response => { if (response.ok) { input.value = ''; } else { console.error('Failed to send message'); } }) .catch(error => { console.error('Error:', error); }); } } // Allow sending message with Enter key document.getElementById('messageInput').addEventListener('keypress', function (e) { if (e.key === 'Enter') { sendMessage(); } }); // Initial connection connectWebSocket(); </script> </body> </html> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,311 @@ from http.server import HTTPServer, BaseHTTPRequestHandler import json from datetime import datetime import asyncio import websockets import threading from queue import Queue import os import random import colorsys # Store messages in memory messages = [] # Queue for new messages to broadcast broadcast_queue = Queue() # Set to store connected WebSocket clients clients = set() # Dictionary to store active agents and their colors agents = {} # Counter for agent IDs next_agent_id = 1 def relative_luminance(r, g, b): """Calculate relative luminance of a color according to WCAG 2.0""" def adjust(value): value = value / 255 return value / 12.92 if value <= 0.03928 else ((value + 0.055) / 1.055) ** 2.4 r, g, b = adjust(r), adjust(g), adjust(b) return 0.2126 * r + 0.7152 * g + 0.0722 * b def contrast_ratio(l1, l2): """Calculate contrast ratio according to WCAG 2.0""" lighter = max(l1, l2) darker = min(l1, l2) return (lighter + 0.05) / (darker + 0.05) def hsl_to_rgb(h, s, l): """Convert HSL to RGB""" h = h / 360 r, g, b = colorsys.hls_to_rgb(h, l/100, s/100) return (int(r * 255), int(g * 255), int(b * 255)) def generate_hsl_color(): """Generate a pleasing HSL color with good contrast against white text""" attempts = 0 while attempts < 100: # Prevent infinite loop hue = random.randint(0, 360) saturation = random.randint(35, 65) # More muted colors lightness = random.randint(25, 45) # Darker colors for white text # Convert to RGB to check contrast rgb = hsl_to_rgb(hue, saturation, lightness) bg_luminance = relative_luminance(*rgb) text_luminance = relative_luminance(255, 255, 255) # White text contrast = contrast_ratio(text_luminance, bg_luminance) if contrast >= 4.5: # WCAG AA standard for normal text return f"hsl({hue}, {saturation}%, {lightness}%)" attempts += 1 # Fallback to a safe color if we can't find one return "hsl(210, 50%, 35%)" HELP_MESSAGE = { "welcome": "Welcome to the Agent Communication Server!", "overview": "This server allows agents to communicate through a simple HTTP/WebSocket interface.", "endpoints": { "GET /": "View the message UI in your browser at http://localhost:5000/", "GET /help": "Show this help message", "GET /messages": "Get all stored messages", "GET /recent": "Get the 3 most recent messages (oldest first)", "POST /": "Send a new message", "POST /register": "Register as a new agent and get an ID", "GET /agents": "List all active agents" }, "sending_messages": { "description": "To send a message, use curl or any HTTP client:", "examples": { "register": """curl -X POST http://localhost:5000/register""", "json_message": """curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-Sender: Agent-1" \\ -d '{"message": "Hello world"}' \\ http://localhost:5000/""", "text_message": """curl -X POST \\ -H "Content-Type: text/plain" \\ -H "X-Sender: Agent-1" \\ -H "X-Role: Operator" \\ -d "Hello world" \\ http://localhost:5000/""" } }, "reading_messages": { "browser": "Open http://localhost:5000/ in your browser to see the message UI", "api_options": { "all_messages": "curl http://localhost:5000/messages - Get all messages", "recent_messages": "curl http://localhost:5000/recent - Get 3 most recent messages (oldest first)", "active_agents": "curl http://localhost:5000/agents - List all registered agents and their colors" }, "color_coding": "Each agent is assigned a unique color that meets WCAG AA contrast standards (4.5:1 ratio with white text)" }, "message_format": { "required_headers": { "Content-Type": "application/json or text/plain", "X-Sender": "Your agent identifier (e.g., Agent-1)" } }, "tips": [ "Register to get your agent ID before sending messages", "Messages appear in real-time in the browser UI", "New messages appear at the top", "Messages are color-coded by sender with accessible contrast", "Use /recent to quickly catch up on the latest messages", "You can clear the display using the Clear Display button", "The connection status is shown in the top-right" ] } class SimpleRequestHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path == '/register': self.handle_register() return content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) # Get sender and role from headers sender = self.headers.get('X-Sender', 'Unknown') role = self.headers.get('X-Role', None) # Verify sender is registered (except for "Human" sender) if sender not in agents and sender != 'Unknown' and sender != 'Human': self.send_error(403, "Agent not registered. Please register first at /register") return timestamp = datetime.now().isoformat() try: # Try to parse as JSON message = json.loads(post_data.decode('utf-8')) message_type = 'json' print(f"JSON Message from {sender} ({role if role else 'no role'}):") print(json.dumps(message, indent=2)) except: # If not JSON, store as plain text message = post_data.decode('utf-8') message_type = 'text' print(f"Plain Text Message from {sender} ({role if role else 'no role'}):") print(message) # If role is provided, update the agent's role if role and sender in agents: agents[sender] = {'color': agents[sender]['color'], 'role': role} # Create message object message_obj = { 'timestamp': timestamp, 'type': message_type, 'content': message, 'sender': sender, 'display_name': role if role else sender, # Use role if available, otherwise use sender ID 'color': agents[sender]['color'] if sender in agents else ('#4CAF50' if sender == 'Human' else '#808080') # Green for Human, gray for unknown } # Store the message messages.append(message_obj) # Put message in broadcast queue broadcast_queue.put(message_obj) # Send response self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(b"Message received") def handle_register(self): global next_agent_id # Generate new agent ID and color agent_id = f"Agent-{next_agent_id}" color = generate_hsl_color() # Store agent info as a dictionary with color and role (initially None) agents[agent_id] = {'color': color, 'role': None} next_agent_id += 1 # Send response self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = { 'agent_id': agent_id, 'color': color, 'message': f"Successfully registered as {agent_id}" } self.wfile.write(json.dumps(response).encode()) def do_GET(self): if self.path == '/messages': # Return stored messages self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(messages).encode()) return elif self.path == '/recent': # Return the 3 most recent messages, oldest first recent_messages = messages[-3:] if len(messages) >= 3 else messages[:] self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(recent_messages).encode()) return elif self.path == '/help': # Return help information self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(HELP_MESSAGE, indent=2).encode()) return elif self.path == '/agents': # Return list of active agents self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(agents).encode()) return elif self.path == '/': # Serve the HTML UI self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() with open('message_viewer.html', 'rb') as f: self.wfile.write(f.read()) return # Default response self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(b"Server is running") async def websocket_handler(websocket): try: # Register client clients.add(websocket) print("New client connected") # Send existing messages await websocket.send(json.dumps(messages)) # Keep connection alive and handle new messages while True: try: # This will raise an exception when the client disconnects await websocket.ping() await asyncio.sleep(1) except: break finally: # Unregister client clients.remove(websocket) print("Client disconnected") async def broadcast_messages(): while True: # Check if there are any messages to broadcast while not broadcast_queue.empty(): message = broadcast_queue.get() # Broadcast to all connected clients websockets_to_remove = set() for client in clients: try: await client.send(json.dumps([message])) except: websockets_to_remove.add(client) # Remove any disconnected clients for client in websockets_to_remove: clients.remove(client) await asyncio.sleep(0.1) async def run_websocket_server(): async with websockets.serve(websocket_handler, "localhost", 5001): await broadcast_messages() # This will run forever def start_websocket_server(): asyncio.run(run_websocket_server()) def run_http_server(port=5000): server_address = ('', port) httpd = HTTPServer(server_address, SimpleRequestHandler) print(f"HTTP Server running on port {port}...") print(f"WebSocket Server running on port 5001...") print(f"View messages at http://localhost:{port}/") print(f"For help, visit http://localhost:{port}/help") httpd.serve_forever() if __name__ == '__main__': # Start WebSocket server in a separate thread websocket_thread = threading.Thread(target=start_websocket_server) websocket_thread.daemon = True websocket_thread.start() # Run HTTP server in main thread run_http_server()