Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eval_protocol/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def parse_args(args=None):

# Logs command
logs_parser = subparsers.add_parser("logs", help="Serve logs with file watching and real-time updates")
logs_parser.add_argument("--port", type=int, default=8000, help="Port to bind to (default: 8000)")

# Run command (for Hydra-based evaluations)
# This subparser intentionally defines no arguments itself.
Expand Down
7 changes: 4 additions & 3 deletions eval_protocol/cli_commands/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
def logs_command(args):
"""Serve logs with file watching and real-time updates"""

port = args.port
print(f"🚀 Starting Eval Protocol Logs Server")
print(f"🌐 URL: http://localhost:8000")
print(f"🔌 WebSocket: ws://localhost:8000/ws")
print(f"🌐 URL: http://localhost:{port}")
print(f"🔌 WebSocket: ws://localhost:{port}/ws")
print(f"👀 Watching paths: {['current directory']}")
print("Press Ctrl+C to stop the server")
print("-" * 50)

try:
serve_logs()
serve_logs(port=args.port)
return 0
except KeyboardInterrupt:
print("\n🛑 Server stopped by user")
Expand Down
48 changes: 40 additions & 8 deletions eval_protocol/utils/logs_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,20 +319,52 @@ def run(self):
asyncio.run(self.run_async())


server = LogsServer()
app = server.app
def create_app(host: str = "localhost", port: int = 8000, build_dir: Optional[str] = None) -> FastAPI:
"""
Factory function to create a FastAPI app instance.

This allows uvicorn to call it with parameters and avoids top-level variable instantiation.

Args:
host: Host to bind to
port: Port to bind to
build_dir: Optional custom build directory path

Returns:
FastAPI app instance
"""
if build_dir is None:
build_dir = os.path.abspath(
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "vite-app", "dist")
)

server = LogsServer(host=host, port=port, build_dir=build_dir)
return server.app

def serve_logs():

# For backward compatibility and direct usage
def serve_logs(port: Optional[int] = None):
"""
Convenience function to create and run a LogsServer.
"""
global server, app
if server is None:
server = LogsServer()
app = server.app
server = LogsServer(port=port)
server.run()


if __name__ == "__main__":
serve_logs()
import argparse

parser = argparse.ArgumentParser(description="Start the evaluation logs server")
parser.add_argument("--host", default="localhost", help="Host to bind to (default: localhost)")
parser.add_argument("--port", type=int, default=8000, help="Port to bind to (default: 8000)")
parser.add_argument("--build-dir", help="Path to Vite build directory")

args = parser.parse_args()

# Create server with command line arguments
if args.build_dir:
server = LogsServer(host=args.host, port=args.port, build_dir=args.build_dir)
else:
server = LogsServer(host=args.host, port=args.port)

server.run()
45 changes: 38 additions & 7 deletions eval_protocol/utils/vite_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,6 +57,40 @@ def __init__(
# Setup routes
self._setup_routes()

def _inject_config_into_html(self, html_content: str) -> str:
"""Inject server configuration into the HTML content."""
config_script = f"""
<script>
// Server-injected configuration
window.SERVER_CONFIG = {{
host: "{self.host}",
port: "{self.port}",
protocol: "ws",
apiProtocol: "http"
}};
</script>
"""

# Insert the config script before the closing </head> tag
if "</head>" in html_content:
return html_content.replace("</head>", f"{config_script}</head>")
else:
# If no </head> tag, insert at the beginning
return f"{config_script}{html_content}"

def _serve_index_with_config(self) -> HTMLResponse:
"""Serve the index.html file with injected configuration."""
index_path = self.build_dir / self.index_file
if index_path.exists():
with open(index_path, "r", encoding="utf-8") as f:
html_content = f.read()

# Inject server configuration
enhanced_html = self._inject_config_into_html(html_content)
return HTMLResponse(content=enhanced_html)

raise HTTPException(status_code=404, detail="Index file not found")

def _setup_routes(self):
"""Set up the API routes for serving the SPA."""

Expand All @@ -81,18 +115,15 @@ async def serve_spa(path: str):
# For SPA routing, serve index.html for non-existent routes
# but exclude API routes and asset requests
if not path.startswith(("api/", "assets/")):
index_path = self.build_dir / self.index_file
if index_path.exists():
return FileResponse(index_path)
return self._serve_index_with_config()

# If we get here, the file doesn't exist and it's not a SPA route
raise HTTPException(status_code=404, detail="File not found")

@self.app.get("/")
async def root():
"""Serve the main index.html file."""
index_path = self.build_dir / self.index_file
return FileResponse(index_path)
"""Serve the main index.html file with injected configuration."""
return self._serve_index_with_config()

@self.app.get("/health")
async def health():
Expand Down
1 change: 1 addition & 0 deletions vite-app/dist/assets/index-CGYj40Gx.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion vite-app/dist/assets/index-DWfIf2rx.css

This file was deleted.

88 changes: 0 additions & 88 deletions vite-app/dist/assets/index-D_nkLTVA.js

This file was deleted.

1 change: 0 additions & 1 deletion vite-app/dist/assets/index-D_nkLTVA.js.map

This file was deleted.

88 changes: 88 additions & 0 deletions vite-app/dist/assets/index-t_hsfGP1.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions vite-app/dist/assets/index-t_hsfGP1.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions vite-app/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EP | Log Viewer</title>
<link rel="icon" href="/assets/favicon-BkAAWQga.png" />
<script type="module" crossorigin src="/assets/index-D_nkLTVA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DWfIf2rx.css">
<script type="module" crossorigin src="/assets/index-t_hsfGP1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGYj40Gx.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
</html>
73 changes: 73 additions & 0 deletions vite-app/docs/runtime-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Vite App with Runtime Configuration

This Vite app is designed to work with the Python evaluation protocol server and automatically discovers its configuration at runtime.

## Runtime Configuration

The app automatically discovers the server configuration in the following order:

1. **Server-Injected Configuration** (Recommended): The Python server injects configuration directly into the HTML
2. **Location-Based Discovery**: Falls back to discovering configuration from the current URL
3. **Default Values**: Uses localhost:8000 as a last resort

## How It Works

### Server-Side Injection
The Python server (`logs_server.py`) automatically injects configuration into the HTML response:

```html
<script>
window.SERVER_CONFIG = {
host: "localhost",
port: "8000",
protocol: "ws",
apiProtocol: "http"
};
</script>
```

### Frontend Discovery
The frontend automatically reads this configuration and uses it for WebSocket connections:

```typescript
// First, check if server injected configuration is available
if (window.SERVER_CONFIG) {
const serverConfig = window.SERVER_CONFIG;
config.websocket.host = serverConfig.host;
config.websocket.port = serverConfig.port;
// ... etc
}
```

## Usage

### Starting the Server
```bash
# Default port 8000
python -m eval_protocol.utils.logs_server

# Custom port
python -m eval_protocol.utils.logs_server --port 9000

# Custom host and port
python -m eval_protocol.utils.logs_server --host 0.0.0.0 --port 9000

# Custom build directory
python -m eval_protocol.utils.logs_server --build-dir /path/to/dist
```

### Building the Frontend
```bash
cd vite-app
pnpm install
pnpm build
```

The built files will be in the `dist/` directory and automatically served by the Python server.

## Benefits

- **No hard-coded ports**: The frontend automatically adapts to whatever port the server runs on
- **Flexible deployment**: Can run on any port without rebuilding the frontend
- **Automatic discovery**: Works whether served from the same origin or different origins
- **Fallback support**: Gracefully handles cases where server injection isn't available
11 changes: 9 additions & 2 deletions vite-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EvaluationRowSchema, type EvaluationRow } from "./types/eval-protocol";
import { WebSocketServerMessageSchema } from "./types/websocket";
import { GlobalState } from "./GlobalState";
import logoLight from "./assets/logo-light.png";
import { getWebSocketUrl, discoverServerConfig } from "./config";

export const state = new GlobalState();

Expand All @@ -26,7 +27,7 @@ const App = observer(() => {
return; // Already connected
}

const ws = new WebSocket("ws://localhost:8000/ws");
const ws = new WebSocket(getWebSocketUrl());
wsRef.current = ws;

ws.onopen = () => {
Expand Down Expand Up @@ -109,7 +110,13 @@ const App = observer(() => {
};

useEffect(() => {
connectWebSocket();
// Discover server configuration first, then connect
const initializeApp = async () => {
await discoverServerConfig();
connectWebSocket();
};

initializeApp();

return () => {
if (reconnectTimeoutRef.current) {
Expand Down
63 changes: 63 additions & 0 deletions vite-app/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Configuration for the application
export const config = {
// WebSocket connection settings
websocket: {
host: 'localhost', // Will be discovered at runtime
port: '8000', // Will be discovered at runtime
protocol: 'ws',
},
// API settings
api: {
host: 'localhost', // Will be discovered at runtime
port: '8000', // Will be discovered at runtime
protocol: 'http',
},
};

// Helper function to build WebSocket URL
export const getWebSocketUrl = (): string => {
const { protocol, host, port } = config.websocket;
return `${protocol}://${host}:${port}/ws`;
};

// Helper function to build API URL
export const getApiUrl = (): string => {
const { protocol, host, port } = config.api;
return `${protocol}://${host}:${port}`;
};

// Runtime configuration discovery
export const discoverServerConfig = async (): Promise<void> => {
try {
// First, check if server injected configuration is available
if (window.SERVER_CONFIG) {
const serverConfig = window.SERVER_CONFIG;
config.websocket.host = serverConfig.host;
config.websocket.port = serverConfig.port;
config.websocket.protocol = serverConfig.protocol;
config.api.host = serverConfig.host;
config.api.port = serverConfig.port;
config.api.protocol = serverConfig.apiProtocol;
console.log('Using server-injected config:', config);
return;
}

// Fallback: Try to discover server configuration from the current location
const currentHost = window.location.hostname;
const currentPort = window.location.port;
const currentProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';

// Update config with discovered values
config.websocket.host = currentHost;
config.websocket.port = currentPort || (currentProtocol === 'wss:' ? '443' : '80');
config.websocket.protocol = currentProtocol;

config.api.host = currentHost;
config.api.port = currentPort || (currentProtocol === 'wss:' ? '443' : '80');
config.api.protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';

console.log('Using discovered config from location:', config);
} catch (error) {
console.warn('Failed to discover server config, using defaults:', error);
}
};
13 changes: 13 additions & 0 deletions vite-app/src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Global type declarations
declare global {
interface Window {
SERVER_CONFIG?: {
host: string;
port: string;
protocol: string;
apiProtocol: string;
};
}
}

export {};
Loading