FEAT WebUI (#61)
* FEAT webui to run RA.Aid from a browser * FEAT startin webui from ra-aid cmd * FEAT updating readme * FEAT adding ADR for webui * FEAT marking webui as alpha feature
This commit is contained in:
parent
1b239f07bf
commit
0e188c961b
|
|
@ -12,3 +12,5 @@ __pycache__/
|
|||
/.idea
|
||||
/htmlcov
|
||||
.envrc
|
||||
appmap.log
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
include README.md
|
||||
include LICENSE
|
||||
include requirements*.txt
|
||||
recursive-include agent_langchain *.py
|
||||
include README.md
|
||||
include CHANGELOG.md
|
||||
recursive-include ra_aid/webui/static *
|
||||
|
|
|
|||
35
README.md
35
README.md
|
|
@ -183,6 +183,9 @@ ra-aid -m "Add new feature" --verbose
|
|||
- `--auto-test`: Automatically run tests after each code change
|
||||
- `--max-test-cmd-retries`: Maximum number of test command retry attempts (default: 3)
|
||||
- `--version`: Show program version number and exit
|
||||
- `--webui`: Launch the web interface (alpha feature)
|
||||
- `--webui-host`: Host to listen on for web interface (default: 0.0.0.0) (alpha feature)
|
||||
- `--webui-port`: Port to listen on for web interface (default: 8080) (alpha feature)
|
||||
|
||||
### Example Tasks
|
||||
|
||||
|
|
@ -259,6 +262,38 @@ Make sure to set your TAVILY_API_KEY environment variable to enable this feature
|
|||
|
||||
Enable with `--chat` to transform ra-aid into an interactive assistant that guides you through research and implementation tasks. Have a natural conversation about what you want to build, explore options together, and dispatch work - all while maintaining context of your discussion. Perfect for when you want to think through problems collaboratively rather than just executing commands.
|
||||
|
||||
### Web Interface
|
||||
|
||||
RA.Aid includes a modern web interface that provides:
|
||||
- Beautiful dark-themed chat interface
|
||||
- Real-time streaming of command output
|
||||
- Request history with quick resubmission
|
||||
- Responsive design that works on all devices
|
||||
|
||||
To launch the web interface:
|
||||
|
||||
```bash
|
||||
# Start with default settings (0.0.0.0:8080)
|
||||
ra-aid --webui
|
||||
|
||||
# Specify custom host and port
|
||||
ra-aid --webui --webui-host 127.0.0.1 --webui-port 3000
|
||||
```
|
||||
|
||||
Command line options for web interface:
|
||||
- `--webui`: Launch the web interface
|
||||
- `--webui-host`: Host to listen on (default: 0.0.0.0)
|
||||
- `--webui-port`: Port to listen on (default: 8080)
|
||||
|
||||
After starting the server, open your web browser to the displayed URL (e.g., http://localhost:8080). The interface provides:
|
||||
- Left sidebar showing request history
|
||||
- Main chat area with real-time output
|
||||
- Input box for typing requests
|
||||
- Automatic reconnection handling
|
||||
- Error reporting and status messages
|
||||
|
||||
All ra-aid commands sent through the web interface automatically use cowboy mode for seamless execution.
|
||||
|
||||
### Command Interruption and Feedback
|
||||
|
||||
<img src="assets/demo-chat-mode-interrupted-1.gif" alt="Command Interrupt Demo" autoplay loop style="display: block; margin: 0 auto; width: 100%; max-width: 800px;">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# A couple thoughts:
|
||||
|
||||
1. I like that you're using websockets. I think a bi-directional message passing is what we will ultimately need, and this sets up the groundwork for that.
|
||||
2. Since rn we're just sending raw ra-aid text output to the front-end, an easy win to make it render nicer would be to put it in a non-wrapping or similar.
|
||||
3. I like the minimal html/js as an initial setup.
|
||||
|
||||
Thoughts for down the line if we merge this (the following would be in future PRs):
|
||||
|
||||
1. We'll ultimately want to stream structured objects from the agent
|
||||
* Rn, the code has a ton of cases like console.print(Panel(panel_content, title="Expert Context", border_style="blue")). We will want to abstract these calls and have something like a send_agent_output function. Depending on config (e.g. CLI use vs web ui use), that would send an object onto the web socket stream, or send output to the console.
|
||||
2. We need a way to send user inputs back to the agent, e.g. anything currently using stdin (we don't use stdin directly rn, but we do have things that use tty or prompt the user for input which ultimately do use stdin)
|
||||
3. Tricky run_shell_command is really powerful, and it essentially passes fully interactive TTY through to the user. If we want to keep the same UX, we could do this potentially by using tty.js on the front-end and streaming the tty over websocket. Alternatively, we could simply disable TTY tools like run_shell_command or restrict them to a non-tty mode (e.g. simple command running, stdout/stderr capture only)
|
||||
4. Ideally we should keep the websocket and any API endpoints friendly to machine endpoints. JSON objects over websocket + any JSON REST endpoints we might need would satisfy this.
|
||||
5. The UX itself should be carefully considered. IMO one of the big benefits of a web UI is to be able to have the agent doing work which I can direct from a mobile device, so I think an efficient mobile-first UI would be ideal. For this, my initial thought was something like react + shadcn.
|
||||
6. Initially I had considered https://socket.io/. In this PR, we're just going straight to websockets, which is cool and has one less dependency. Not sure if socket.io would be worth it, but wanted to mention it. The overall architecture wouldn't change.
|
||||
7. Semi-related: we should have a way to send logs to a directory or to a logging backend.
|
||||
|
||||
@leonj1 I think starting with a small PR like this and then incrementally adding to the web UI is a good approach.
|
||||
|
||||
@sosacrazy126 would be interested in your thoughts since I know you were working on webui as well, want to respect the work you've done.
|
||||
|
|
@ -37,7 +37,11 @@ dependencies = [
|
|||
"pathspec>=0.11.0",
|
||||
"aider-chat>=0.72.1",
|
||||
"tavily-python>=0.5.0",
|
||||
"litellm"
|
||||
"litellm",
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn>=0.24.0",
|
||||
"websockets>=12.0",
|
||||
"jinja2>=3.1.2"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ from ra_aid.tools.memory import _global_memory
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def launch_webui(host: str, port: int):
|
||||
"""Launch the RA.Aid web interface."""
|
||||
from ra_aid.webui import run_server
|
||||
print(f"Starting RA.Aid web interface on http://{host}:{port}")
|
||||
run_server(host=host, port=port)
|
||||
|
||||
def parse_arguments(args=None):
|
||||
VALID_PROVIDERS = [
|
||||
"anthropic",
|
||||
|
|
@ -166,6 +172,23 @@ Examples:
|
|||
default=DEFAULT_MAX_TEST_CMD_RETRIES,
|
||||
help="Maximum number of retries for the test command (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--webui",
|
||||
action="store_true",
|
||||
help="Launch the web interface",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--webui-host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Host to listen on for web interface (default: 0.0.0.0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--webui-port",
|
||||
type=int,
|
||||
default=8080,
|
||||
help="Port to listen on for web interface (default: 8080)",
|
||||
)
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
|
@ -246,6 +269,11 @@ def main():
|
|||
setup_logging(args.verbose)
|
||||
logger.debug("Starting RA.Aid with arguments: %s", args)
|
||||
|
||||
# Launch web interface if requested
|
||||
if args.webui:
|
||||
launch_webui(args.webui_host, args.webui_port)
|
||||
return
|
||||
|
||||
try:
|
||||
# Check dependencies before proceeding
|
||||
check_dependencies()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
"""Web interface module for RA.Aid."""
|
||||
|
||||
from .server import run_server
|
||||
|
||||
__all__ = ['run_server']
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
"""Web interface server implementation for RA.Aid."""
|
||||
|
||||
import asyncio
|
||||
import shutil
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, WebSocket, Request, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.DEBUG) # Set to DEBUG for more info
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Verify ra-aid is available
|
||||
if not shutil.which('ra-aid'):
|
||||
logger.error("ra-aid command not found. Please ensure it's installed and in your PATH")
|
||||
sys.exit(1)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Get the directory containing static files
|
||||
STATIC_DIR = Path(__file__).parent / "static"
|
||||
if not STATIC_DIR.exists():
|
||||
logger.error(f"Static directory not found at {STATIC_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(f"Using static directory: {STATIC_DIR}")
|
||||
|
||||
# Setup templates
|
||||
templates = Jinja2Templates(directory=str(STATIC_DIR))
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket) -> bool:
|
||||
try:
|
||||
logger.debug("Accepting WebSocket connection...")
|
||||
await websocket.accept()
|
||||
logger.debug("WebSocket connection accepted")
|
||||
self.active_connections.append(websocket)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error accepting WebSocket connection: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_message(self, websocket: WebSocket, message: Dict[str, Any]):
|
||||
try:
|
||||
await websocket.send_json(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message: {e}")
|
||||
await self.handle_error(websocket, str(e))
|
||||
|
||||
async def handle_error(self, websocket: WebSocket, error_message: str):
|
||||
try:
|
||||
await websocket.send_json({
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools": {
|
||||
"messages": [{
|
||||
"content": f"Error: {error_message}",
|
||||
"status": "error"
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending error message: {e}")
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def get_root(request: Request):
|
||||
"""Serve the index.html file with port parameter."""
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{"request": request, "server_port": request.url.port or 8080}
|
||||
)
|
||||
|
||||
# Mount static files for js and other assets
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
client_id = id(websocket)
|
||||
logger.info(f"New WebSocket connection attempt from client {client_id}")
|
||||
|
||||
if not await manager.connect(websocket):
|
||||
logger.error(f"Failed to accept WebSocket connection for client {client_id}")
|
||||
return
|
||||
|
||||
logger.info(f"WebSocket connection accepted for client {client_id}")
|
||||
|
||||
try:
|
||||
# Send initial connection success message
|
||||
await manager.send_message(websocket, {
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"agent": {
|
||||
"messages": [{
|
||||
"content": "Connected to RA.Aid server",
|
||||
"status": "info"
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = await websocket.receive_json()
|
||||
logger.debug(f"Received message from client {client_id}: {message}")
|
||||
|
||||
if message["type"] == "request":
|
||||
await manager.send_message(websocket, {
|
||||
"type": "stream_start"
|
||||
})
|
||||
|
||||
try:
|
||||
# Run ra-aid with the request
|
||||
cmd = ["ra-aid", "-m", message["content"], "--cowboy-mode"]
|
||||
logger.info(f"Executing command: {' '.join(cmd)}")
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
logger.info(f"Process started with PID: {process.pid}")
|
||||
|
||||
async def read_stream(stream, is_error=False):
|
||||
while True:
|
||||
line = await stream.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
try:
|
||||
decoded_line = line.decode().strip()
|
||||
if decoded_line:
|
||||
await manager.send_message(websocket, {
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools" if is_error else "agent": {
|
||||
"messages": [{
|
||||
"content": decoded_line,
|
||||
"status": "error" if is_error else "info"
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing output: {e}")
|
||||
|
||||
# Create tasks for reading stdout and stderr
|
||||
stdout_task = asyncio.create_task(read_stream(process.stdout))
|
||||
stderr_task = asyncio.create_task(read_stream(process.stderr, True))
|
||||
|
||||
# Wait for both streams to complete
|
||||
await asyncio.gather(stdout_task, stderr_task)
|
||||
|
||||
# Wait for process to complete
|
||||
return_code = await process.wait()
|
||||
|
||||
if return_code != 0:
|
||||
await manager.handle_error(
|
||||
websocket,
|
||||
f"Process exited with code {return_code}"
|
||||
)
|
||||
|
||||
await manager.send_message(websocket, {
|
||||
"type": "stream_end",
|
||||
"request": message["content"]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing ra-aid: {e}")
|
||||
await manager.handle_error(websocket, str(e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
await manager.handle_error(websocket, str(e))
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info(f"WebSocket client {client_id} disconnected")
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error for client {client_id}: {e}")
|
||||
finally:
|
||||
manager.disconnect(websocket)
|
||||
logger.info(f"WebSocket connection cleaned up for client {client_id}")
|
||||
|
||||
def run_server(host: str = "0.0.0.0", port: int = 8080):
|
||||
"""Run the FastAPI server."""
|
||||
logger.info(f"Starting server on {host}:{port}")
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="debug",
|
||||
ws_max_size=16777216, # 16MB
|
||||
timeout_keep_alive=0 # Disable keep-alive timeout
|
||||
)
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-gray-900">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="server-port" content="{{ server_port }}">
|
||||
<title>RA.Aid Web Interface</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'dark-primary': '#1a1b26',
|
||||
'dark-secondary': '#24283b',
|
||||
'dark-accent': '#7aa2f7',
|
||||
'dark-text': '#c0caf5'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-dark-primary text-dark-text">
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-64 bg-dark-secondary border-r border-gray-700 flex flex-col">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-dark-accent">History</h2>
|
||||
</div>
|
||||
<div id="history-list" class="flex-1 overflow-y-auto p-4 space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Chat Container -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="chat-container">
|
||||
<div id="chat-messages"></div>
|
||||
<div id="stream-output" class="hidden font-mono bg-dark-secondary rounded-lg p-4 text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="border-t border-gray-700 p-4 bg-dark-secondary">
|
||||
<div class="flex space-x-4">
|
||||
<textarea
|
||||
id="user-input"
|
||||
class="flex-1 bg-dark-primary border border-gray-700 rounded-lg p-3 text-dark-text placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-dark-accent resize-none"
|
||||
placeholder="Type your request here..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
<button
|
||||
id="send-button"
|
||||
class="px-6 py-2 bg-dark-accent text-white rounded-lg hover:bg-opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-dark-accent disabled:opacity-50 disabled:cursor-not-allowed h-fit"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add dynamic styles for messages
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.message {
|
||||
@apply mb-4 p-4 rounded-lg max-w-3xl;
|
||||
}
|
||||
.user-message {
|
||||
@apply bg-dark-accent text-white ml-auto;
|
||||
}
|
||||
.system-message {
|
||||
@apply bg-dark-secondary mr-auto;
|
||||
}
|
||||
.error-message {
|
||||
@apply bg-red-900 text-red-100 mr-auto;
|
||||
}
|
||||
.history-item {
|
||||
@apply p-3 rounded-lg hover:bg-dark-primary cursor-pointer transition-colors duration-200 text-sm;
|
||||
}
|
||||
#stream-output:not(:empty) {
|
||||
@apply block;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
class RAWebUI {
|
||||
constructor() {
|
||||
this.messageHistory = [];
|
||||
this.connectionAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.setupElements();
|
||||
this.setupEventListeners();
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
setupElements() {
|
||||
this.userInput = document.getElementById('user-input');
|
||||
this.sendButton = document.getElementById('send-button');
|
||||
this.chatMessages = document.getElementById('chat-messages');
|
||||
this.streamOutput = document.getElementById('stream-output');
|
||||
this.historyList = document.getElementById('history-list');
|
||||
|
||||
// Disable send button initially
|
||||
this.sendButton.disabled = true;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.sendButton.addEventListener('click', () => this.sendMessage());
|
||||
this.userInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async connectWebSocket() {
|
||||
// Don't try to reconnect if we've exceeded the maximum attempts
|
||||
if (this.connectionAttempts >= this.maxReconnectAttempts) {
|
||||
this.appendMessage(
|
||||
'Maximum reconnection attempts reached. Please refresh the page.',
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the server port from the meta tag
|
||||
const serverPort = document.querySelector('meta[name="server-port"]')?.content || '8080';
|
||||
|
||||
// Construct WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.hostname}:${serverPort}/ws`;
|
||||
console.log('Attempting to connect to WebSocket URL:', wsUrl);
|
||||
|
||||
// Close existing connection if any
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
// Create new WebSocket connection
|
||||
console.log('Creating new WebSocket connection...');
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
this.connectionAttempts++;
|
||||
|
||||
// Setup WebSocket event handlers
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connection established successfully');
|
||||
this.connectionAttempts = 0; // Reset counter on successful connection
|
||||
this.sendButton.disabled = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log('WebSocket closed:', event.code, event.reason);
|
||||
this.sendButton.disabled = true;
|
||||
|
||||
// Only attempt reconnect if not a normal closure and within retry limits
|
||||
if (event.code !== 1000 && this.connectionAttempts < this.maxReconnectAttempts) {
|
||||
const delay = Math.min(1000 * Math.pow(2, this.connectionAttempts), 10000);
|
||||
this.appendMessage(
|
||||
`Connection lost. Reconnecting in ${delay/1000} seconds...`,
|
||||
'error'
|
||||
);
|
||||
setTimeout(() => this.connectWebSocket(), delay);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleServerMessage(data);
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error);
|
||||
this.appendMessage('Error processing server message', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to WebSocket:', error);
|
||||
this.appendMessage(
|
||||
`Connection error: ${error.message}. Retrying...`,
|
||||
'error'
|
||||
);
|
||||
|
||||
// Attempt to reconnect with exponential backoff
|
||||
const delay = Math.min(1000 * Math.pow(2, this.connectionAttempts), 10000);
|
||||
setTimeout(() => this.connectWebSocket(), delay);
|
||||
}
|
||||
}
|
||||
|
||||
handleServerMessage(data) {
|
||||
if (data.type === 'stream_start') {
|
||||
this.streamOutput.textContent = '';
|
||||
this.streamOutput.style.display = 'block';
|
||||
} else if (data.type === 'stream_end') {
|
||||
this.streamOutput.style.display = 'none';
|
||||
this.addToHistory(data.request);
|
||||
this.sendButton.disabled = false;
|
||||
} else if (data.type === 'chunk') {
|
||||
this.handleChunk(data.chunk);
|
||||
}
|
||||
}
|
||||
|
||||
handleChunk(chunk) {
|
||||
if (chunk.agent && chunk.agent.messages) {
|
||||
chunk.agent.messages.forEach(msg => {
|
||||
if (msg.content) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
msg.content.forEach(content => {
|
||||
if (content.type === 'text' && content.text.trim()) {
|
||||
this.appendMessage(content.text.trim(), 'system');
|
||||
}
|
||||
});
|
||||
} else if (msg.content.trim()) {
|
||||
this.appendMessage(msg.content.trim(), 'system');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (chunk.tools && chunk.tools.messages) {
|
||||
chunk.tools.messages.forEach(msg => {
|
||||
if (msg.status === 'error' && msg.content) {
|
||||
this.appendMessage(msg.content.trim(), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
appendMessage(content, type) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}-message`;
|
||||
messageDiv.textContent = content;
|
||||
this.chatMessages.appendChild(messageDiv);
|
||||
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
addToHistory(request) {
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.textContent = request.slice(0, 50) + (request.length > 50 ? '...' : '');
|
||||
historyItem.title = request;
|
||||
historyItem.addEventListener('click', () => {
|
||||
this.userInput.value = request;
|
||||
this.userInput.focus();
|
||||
});
|
||||
this.historyList.insertBefore(historyItem, this.historyList.firstChild);
|
||||
this.messageHistory.push(request);
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
console.log('Send button clicked');
|
||||
const message = this.userInput.value.trim();
|
||||
console.log('Message content:', message);
|
||||
|
||||
if (!message) {
|
||||
console.log('Message is empty, not sending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket is not connected');
|
||||
this.appendMessage('Error: Not connected to server. Please wait...', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Sending message to server');
|
||||
this.appendMessage(message, 'user');
|
||||
const payload = { type: 'request', content: message };
|
||||
console.log('Payload:', payload);
|
||||
|
||||
this.ws.send(JSON.stringify(payload));
|
||||
console.log('Message sent successfully');
|
||||
|
||||
this.userInput.value = '';
|
||||
this.sendButton.disabled = true;
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
this.appendMessage(`Error sending message: ${error.message}`, 'error');
|
||||
this.sendButton.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the UI when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.raWebUI = new RAWebUI();
|
||||
});
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# RA.Aid Web Interface
|
||||
|
||||
A modern, dark-themed web interface for RA.Aid that provides:
|
||||
- Beautiful dark mode chat interface
|
||||
- Real-time streaming output with syntax highlighting
|
||||
- Persistent request history with quick resubmission
|
||||
- Responsive design that works on all devices
|
||||
|
||||
## Design
|
||||
|
||||
The interface features a modern dark theme using Tailwind CSS with:
|
||||
- Sleek dark color scheme optimized for readability
|
||||
- Clean, minimalist design focused on content
|
||||
- Smooth transitions and hover effects
|
||||
- Monospace font for code and output display
|
||||
|
||||
## Features
|
||||
|
||||
- **Left Sidebar**: Shows history of previous requests
|
||||
- **Main Chat Area**: Displays conversation and streaming output
|
||||
- **Input Area**: Text input for new requests
|
||||
- **Real-time Updates**: See RA.Aid output as it happens
|
||||
- **Error Handling**: Clear display of any errors that occur
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the required dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Make sure you have RA.Aid installed in your Python environment
|
||||
|
||||
3. Start the web server:
|
||||
```bash
|
||||
# Default: Listen on all interfaces (0.0.0.0) port 8080
|
||||
./server.py
|
||||
|
||||
# Specify custom port
|
||||
./server.py --port 3000
|
||||
|
||||
# Specify custom host and port
|
||||
./server.py --host 127.0.0.1 --port 3000
|
||||
```
|
||||
|
||||
4. Open your web browser and navigate to the server address:
|
||||
```
|
||||
# If running locally with default settings:
|
||||
http://localhost:8080
|
||||
|
||||
# If running on a different port:
|
||||
http://localhost:<port>
|
||||
|
||||
# If accessing from another machine:
|
||||
http://<server-ip>:<port>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Type your request in the input box at the bottom of the screen
|
||||
2. Press Enter or click the Send button to submit
|
||||
3. Watch the real-time output in the chat area
|
||||
4. Previous requests appear in the left sidebar
|
||||
5. Click any previous request to load it into the input box
|
||||
|
||||
## Development
|
||||
|
||||
The web interface consists of:
|
||||
- `index.html`: Main page layout with integrated Tailwind CSS styling
|
||||
- `script.js`: Client-side functionality and WebSocket handling
|
||||
- `server.py`: FastAPI WebSocket server with RA.Aid integration
|
||||
- `requirements.txt`: Python server dependencies
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**:
|
||||
- Tailwind CSS for modern, utility-first styling
|
||||
- Vanilla JavaScript for lightweight client-side operations
|
||||
- WebSocket for real-time communication
|
||||
|
||||
- **Backend**:
|
||||
- FastAPI for high-performance async server
|
||||
- WebSocket support for real-time streaming
|
||||
- Direct integration with ra-aid CLI
|
||||
- Concurrent stdout/stderr handling
|
||||
- Error handling and status reporting
|
||||
|
||||
## Command Integration
|
||||
|
||||
The web interface integrates with ra-aid by:
|
||||
- Executing `ra-aid -m "<message>" --cowboy-mode` for each request
|
||||
- Streaming real-time output through WebSocket connection
|
||||
- Handling both standard output and error streams
|
||||
- Providing error feedback for failed commands
|
||||
|
||||
For example, when you type "What time is it?" in the interface, it executes:
|
||||
```bash
|
||||
ra-aid -m "What time is it?" --cowboy-mode
|
||||
```
|
||||
|
||||
The output is streamed in real-time to the web interface, maintaining the same functionality as the CLI but with a modern web interface.
|
||||
|
||||
## Architecture
|
||||
|
||||
- FastAPI backend with WebSocket support
|
||||
- Static file serving for web assets
|
||||
- Real-time bi-directional communication
|
||||
- Integration with RA.Aid's output streaming
|
||||
- Browser-based frontend with vanilla JavaScript
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-gray-900">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="server-port" content="{{ server_port }}">
|
||||
<title>RA.Aid Web Interface</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'dark-primary': '#1a1b26',
|
||||
'dark-secondary': '#24283b',
|
||||
'dark-accent': '#7aa2f7',
|
||||
'dark-text': '#c0caf5'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-dark-primary text-dark-text">
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-64 bg-dark-secondary border-r border-gray-700 flex flex-col">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-dark-accent">History</h2>
|
||||
</div>
|
||||
<div id="history-list" class="flex-1 overflow-y-auto p-4 space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Chat Container -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="chat-container">
|
||||
<div id="chat-messages"></div>
|
||||
<div id="stream-output" class="hidden font-mono bg-dark-secondary rounded-lg p-4 text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="border-t border-gray-700 p-4 bg-dark-secondary">
|
||||
<div class="flex space-x-4">
|
||||
<textarea
|
||||
id="user-input"
|
||||
class="flex-1 bg-dark-primary border border-gray-700 rounded-lg p-3 text-dark-text placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-dark-accent resize-none"
|
||||
placeholder="Type your request here..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
<button
|
||||
id="send-button"
|
||||
class="px-6 py-2 bg-dark-accent text-white rounded-lg hover:bg-opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-dark-accent disabled:opacity-50 disabled:cursor-not-allowed h-fit"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add dynamic styles for messages
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.message {
|
||||
@apply mb-4 p-4 rounded-lg max-w-3xl;
|
||||
}
|
||||
.user-message {
|
||||
@apply bg-dark-accent text-white ml-auto;
|
||||
}
|
||||
.system-message {
|
||||
@apply bg-dark-secondary mr-auto;
|
||||
}
|
||||
.error-message {
|
||||
@apply bg-red-900 text-red-100 mr-auto;
|
||||
}
|
||||
.history-item {
|
||||
@apply p-3 rounded-lg hover:bg-dark-primary cursor-pointer transition-colors duration-200 text-sm;
|
||||
}
|
||||
#stream-output:not(:empty) {
|
||||
@apply block;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
websockets>=12.0
|
||||
jinja2>=3.1.2
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
class RAWebUI {
|
||||
constructor() {
|
||||
this.messageHistory = [];
|
||||
this.setupElements();
|
||||
this.setupEventListeners();
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
setupElements() {
|
||||
this.userInput = document.getElementById('user-input');
|
||||
this.sendButton = document.getElementById('send-button');
|
||||
this.chatMessages = document.getElementById('chat-messages');
|
||||
this.streamOutput = document.getElementById('stream-output');
|
||||
this.historyList = document.getElementById('history-list');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.sendButton.addEventListener('click', () => this.sendMessage());
|
||||
this.userInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async connectWebSocket() {
|
||||
try {
|
||||
// Get the server port from the response header or default to 8080
|
||||
const serverPort = document.querySelector('meta[name="server-port"]')?.content || '8080';
|
||||
|
||||
// Construct WebSocket URL using the server port
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.hostname}:${serverPort}/ws`;
|
||||
console.log('Attempting to connect to WebSocket URL:', wsUrl);
|
||||
|
||||
console.log('Creating new WebSocket connection...');
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connection established successfully');
|
||||
console.log('Connected to WebSocket server');
|
||||
this.sendButton.disabled = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('Disconnected from WebSocket server');
|
||||
this.sendButton.disabled = true;
|
||||
setTimeout(() => this.connectWebSocket(), 5000);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleServerMessage(data);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to WebSocket:', error);
|
||||
setTimeout(() => this.connectWebSocket(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
handleServerMessage(data) {
|
||||
if (data.type === 'stream_start') {
|
||||
this.streamOutput.textContent = '';
|
||||
this.streamOutput.style.display = 'block';
|
||||
} else if (data.type === 'stream_end') {
|
||||
this.streamOutput.style.display = 'none';
|
||||
this.addToHistory(data.request);
|
||||
} else if (data.type === 'chunk') {
|
||||
this.handleChunk(data.chunk);
|
||||
}
|
||||
}
|
||||
|
||||
handleChunk(chunk) {
|
||||
if (chunk.agent && chunk.agent.messages) {
|
||||
chunk.agent.messages.forEach(msg => {
|
||||
if (msg.content) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
msg.content.forEach(content => {
|
||||
if (content.type === 'text' && content.text.trim()) {
|
||||
this.appendMessage(content.text.trim(), 'system');
|
||||
}
|
||||
});
|
||||
} else if (msg.content.trim()) {
|
||||
this.appendMessage(msg.content.trim(), 'system');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (chunk.tools && chunk.tools.messages) {
|
||||
chunk.tools.messages.forEach(msg => {
|
||||
if (msg.status === 'error' && msg.content) {
|
||||
this.appendMessage(msg.content.trim(), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
appendMessage(content, type) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}-message`;
|
||||
messageDiv.textContent = content;
|
||||
this.chatMessages.appendChild(messageDiv);
|
||||
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
addToHistory(request) {
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.textContent = request.slice(0, 50) + (request.length > 50 ? '...' : '');
|
||||
historyItem.title = request;
|
||||
historyItem.addEventListener('click', () => {
|
||||
this.userInput.value = request;
|
||||
this.userInput.focus();
|
||||
});
|
||||
this.historyList.insertBefore(historyItem, this.historyList.firstChild);
|
||||
this.messageHistory.push(request);
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
console.log('Send button clicked');
|
||||
const message = this.userInput.value.trim();
|
||||
console.log('Message content:', message);
|
||||
|
||||
if (!message) {
|
||||
console.log('Message is empty, not sending');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('WebSocket state:', this.ws.readyState);
|
||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket is not connected');
|
||||
this.appendMessage('Error: WebSocket is not connected. Trying to reconnect...', 'error');
|
||||
this.connectWebSocket();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Sending message to server');
|
||||
this.appendMessage(message, 'user');
|
||||
const payload = { type: 'request', content: message };
|
||||
console.log('Payload:', payload);
|
||||
|
||||
this.ws.send(JSON.stringify(payload));
|
||||
console.log('Message sent successfully');
|
||||
|
||||
this.userInput.value = '';
|
||||
this.sendButton.disabled = true;
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
this.appendMessage(`Error sending message: ${error.message}`, 'error');
|
||||
this.sendButton.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the UI when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.raWebUI = new RAWebUI();
|
||||
});
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, WebSocket, Request, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
# Verify ra-aid is available
|
||||
if not shutil.which('ra-aid'):
|
||||
print("Error: ra-aid command not found. Please ensure it's installed and in your PATH")
|
||||
sys.exit(1)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, replace with specific origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Setup templates
|
||||
templates = Jinja2Templates(directory=Path(__file__).parent)
|
||||
|
||||
# Create a route for the root to serve index.html with port parameter
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def get_root(request: Request):
|
||||
"""Serve the index.html file with port parameter."""
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{"request": request, "server_port": request.url.port or 8080}
|
||||
)
|
||||
|
||||
# Mount static files for js and other assets
|
||||
app.mount("/static", StaticFiles(directory=Path(__file__).parent), name="static")
|
||||
|
||||
# Store WebSocket connections
|
||||
|
||||
# Store active WebSocket connections
|
||||
active_connections: List[WebSocket] = []
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
print(f"New WebSocket connection from {websocket.client}")
|
||||
await websocket.accept()
|
||||
print("WebSocket connection accepted")
|
||||
active_connections.append(websocket)
|
||||
|
||||
try:
|
||||
while True:
|
||||
print("Waiting for message...")
|
||||
message = await websocket.receive_json()
|
||||
print(f"Received message: {message}")
|
||||
|
||||
if message["type"] == "request":
|
||||
print(f"Processing request: {message['content']}")
|
||||
# Notify client that streaming is starting
|
||||
await websocket.send_json({
|
||||
"type": "stream_start"
|
||||
})
|
||||
|
||||
try:
|
||||
# Run ra-aid with the request using -m flag and cowboy mode
|
||||
cmd = ["ra-aid", "-m", message["content"], "--cowboy-mode"]
|
||||
print(f"Executing command: {' '.join(cmd)}")
|
||||
|
||||
# Create subprocess
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
print(f"Process started with PID: {process.pid}")
|
||||
|
||||
# Read output and errors concurrently
|
||||
async def read_stream(stream, is_error=False):
|
||||
stream_type = "stderr" if is_error else "stdout"
|
||||
print(f"Starting to read from {stream_type}")
|
||||
while True:
|
||||
line = await stream.readline()
|
||||
if not line:
|
||||
print(f"End of {stream_type} stream")
|
||||
break
|
||||
|
||||
try:
|
||||
decoded_line = line.decode().strip()
|
||||
print(f"{stream_type} line: {decoded_line}")
|
||||
if decoded_line:
|
||||
await websocket.send_json({
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools" if is_error else "agent": {
|
||||
"messages": [{
|
||||
"content": decoded_line,
|
||||
"status": "error" if is_error else "info"
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error sending output: {e}")
|
||||
|
||||
# Create tasks for reading stdout and stderr
|
||||
stdout_task = asyncio.create_task(read_stream(process.stdout))
|
||||
stderr_task = asyncio.create_task(read_stream(process.stderr, True))
|
||||
|
||||
# Wait for both streams to complete
|
||||
await asyncio.gather(stdout_task, stderr_task)
|
||||
|
||||
# Wait for process to complete
|
||||
return_code = await process.wait()
|
||||
|
||||
if return_code != 0:
|
||||
await websocket.send_json({
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools": {
|
||||
"messages": [{
|
||||
"content": f"Process exited with code {return_code}",
|
||||
"status": "error"
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Notify client that streaming is complete
|
||||
await websocket.send_json({
|
||||
"type": "stream_end",
|
||||
"request": message["content"]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error executing ra-aid: {str(e)}"
|
||||
print(error_msg)
|
||||
await websocket.send_json({
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools": {
|
||||
"messages": [{
|
||||
"content": error_msg,
|
||||
"status": "error"
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
print(f"WebSocket client disconnected")
|
||||
active_connections.remove(websocket)
|
||||
except Exception as e:
|
||||
print(f"WebSocket error: {e}")
|
||||
try:
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"error": str(e)
|
||||
})
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
if websocket in active_connections:
|
||||
active_connections.remove(websocket)
|
||||
print("WebSocket connection cleaned up")
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(request: Request):
|
||||
"""Return server configuration including host and port."""
|
||||
return {
|
||||
"host": request.client.host,
|
||||
"port": request.scope.get("server")[1]
|
||||
}
|
||||
|
||||
def run_server(host: str = "0.0.0.0", port: int = 8080):
|
||||
"""Run the FastAPI server."""
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="RA.Aid Web Interface Server")
|
||||
parser.add_argument("--port", type=int, default=8080,
|
||||
help="Port to listen on (default: 8080)")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0",
|
||||
help="Host to listen on (default: 0.0.0.0)")
|
||||
|
||||
args = parser.parse_args()
|
||||
run_server(host=args.host, port=args.port)
|
||||
Loading…
Reference in New Issue