set up frontend/ infra

This commit is contained in:
AI Christianson 2025-03-13 12:18:54 -04:00
parent c511cefc67
commit fa66066c07
36 changed files with 6616 additions and 576 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ appmap.log
*.swp *.swp
/vsc/node_modules /vsc/node_modules
/vsc/dist /vsc/dist
node_modules/

View File

@ -0,0 +1,14 @@
{
"name": "@ra-aid/common",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}

View File

@ -0,0 +1,4 @@
// Entry point for @ra-aid/common package
export const hello = (): void => {
console.log("Hello from @ra-aid/common");
};

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"moduleResolution": "node",
"declaration": true,
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

6341
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
frontend/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "frontend-monorepo",
"private": true,
"workspaces": [
"common",
"web",
"vsc"
],
"scripts": {
"install-all": "npm install",
"dev:web": "npm --workspace @ra-aid/web run dev"
}
}

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

140
frontend/vsc/dist/extension.js vendored Normal file
View File

@ -0,0 +1,140 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/extension.ts
var extension_exports = {};
__export(extension_exports, {
activate: () => activate,
deactivate: () => deactivate
});
module.exports = __toCommonJS(extension_exports);
var vscode = __toESM(require("vscode"));
var RAWebviewViewProvider = class {
constructor(_extensionUri) {
this._extensionUri = _extensionUri;
}
/**
* Called when a view is first created to initialize the webview
*/
resolveWebviewView(webviewView, context, _token) {
webviewView.webview.options = {
// Enable JavaScript in the webview
enableScripts: true,
// Restrict the webview to only load resources from the extension's directory
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
}
/**
* Creates HTML content for the webview with proper security policies
*/
_getHtmlForWebview(webview) {
const logoUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "RA.png"));
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RA.Aid</title>
<style>
body {
padding: 0;
color: var(--vscode-foreground);
font-size: var(--vscode-font-size);
font-weight: var(--vscode-font-weight);
font-family: var(--vscode-font-family);
background-color: var(--vscode-editor-background);
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
}
.logo {
width: 100px;
height: 100px;
margin-bottom: 20px;
}
h1 {
color: var(--vscode-editor-foreground);
font-size: 1.3em;
margin-bottom: 15px;
}
p {
color: var(--vscode-foreground);
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<img src="${logoUri}" alt="RA.Aid Logo" class="logo">
<h1>RA.Aid</h1>
<p>Your research and development assistant.</p>
<p>More features coming soon!</p>
</div>
</body>
</html>`;
}
};
function getNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function activate(context) {
console.log('Congratulations, your extension "ra-aid" is now active!');
const provider = new RAWebviewViewProvider(context.extensionUri);
const viewRegistration = vscode.window.registerWebviewViewProvider(
"ra-aid.view",
// Must match the view id in package.json
provider
);
context.subscriptions.push(viewRegistration);
const disposable = vscode.commands.registerCommand("ra-aid.helloWorld", () => {
vscode.window.showInformationMessage("Hello World from RA.Aid!");
});
context.subscriptions.push(disposable);
}
function deactivate() {
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
activate,
deactivate
});
//# sourceMappingURL=extension.js.map

6
frontend/vsc/dist/extension.js.map vendored Normal file
View File

@ -0,0 +1,6 @@
{
"version": 3,
"sources": ["../src/extension.ts"],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,aAAwB;AAKxB,IAAM,wBAAN,MAAkE;AAAA,EAChE,YAA6B,eAA2B;AAA3B;AAAA,EAA4B;AAAA;AAAA;AAAA;AAAA,EAKlD,mBACL,aACA,SACA,QACA;AAEA,gBAAY,QAAQ,UAAU;AAAA;AAAA,MAE5B,eAAe;AAAA;AAAA,MAEf,oBAAoB,CAAC,KAAK,aAAa;AAAA,IACzC;AAGA,gBAAY,QAAQ,OAAO,KAAK,mBAAmB,YAAY,OAAO;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA,EAKQ,mBAAmB,SAAiC;AAE1D,UAAM,UAAU,QAAQ,aAAoB,WAAI,SAAS,KAAK,eAAe,UAAU,QAAQ,CAAC;AAMhG,UAAM,QAAQ,SAAS;AAEvB,WAAO;AAAA;AAAA;AAAA;AAAA,0FAI+E,QAAQ,SAAS,sBAAsB,QAAQ,SAAS,uCAAuC,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAsCxK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3B;AACF;AAKA,SAAS,WAAW;AAClB,MAAI,OAAO;AACX,QAAM,WAAW;AACjB,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAQ,SAAS,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,SAAS,MAAM,CAAC;AAAA,EACrE;AACA,SAAO;AACT;AAGO,SAAS,SAAS,SAAkC;AAEzD,UAAQ,IAAI,yDAAyD;AAGrE,QAAM,WAAW,IAAI,sBAAsB,QAAQ,YAAY;AAC/D,QAAM,mBAA0B,cAAO;AAAA,IACrC;AAAA;AAAA,IACA;AAAA,EACF;AACA,UAAQ,cAAc,KAAK,gBAAgB;AAK3C,QAAM,aAAoB,gBAAS,gBAAgB,qBAAqB,MAAM;AAG5E,IAAO,cAAO,uBAAuB,0BAA0B;AAAA,EACjE,CAAC;AAED,UAAQ,cAAc,KAAK,UAAU;AACvC;AAGO,SAAS,aAAa;AAAC;",
"names": []
}

11
frontend/web/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>@ra-aid/web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

20
frontend/web/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "@ra-aid/web",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@ra-aid/common": "1.0.0"
},
"devDependencies": {
"vite": "^4.0.0",
"@vitejs/plugin-react": "^3.0.0",
"typescript": "^5.0.0"
}
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { hello } from '@ra-aid/common';
hello();
const App = () => (
<div>
<h1>Hello from @ra-aid/web using Vite</h1>
</div>
);
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@ -0,0 +1,21 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
// Ensure that Vite treats symlinked packages as local, so HMR works correctly.
alias: {
'@ra-aid/common': path.resolve(__dirname, '../common/src')
}
},
server: {
watch: {
// Watch for changes in the common package.
// This pattern forces Vite to notice file changes in the shared library.
// Adjust the pattern if your common package layout is different.
paths: ['../common/src/**']
}
}
});

View File

@ -1,109 +0,0 @@
# 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

View File

@ -1,90 +0,0 @@
<!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>

View File

@ -1,4 +0,0 @@
fastapi>=0.104.0
uvicorn>=0.24.0
websockets>=12.0
jinja2>=3.1.2

View File

@ -1,163 +0,0 @@
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();
});

View File

@ -1,210 +0,0 @@
#!/usr/bin/env python3
import argparse
import asyncio
import shutil
import sys
from pathlib import Path
from typing import List
import uvicorn
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
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("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 Exception:
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)