Skip to content
Open
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
32 changes: 32 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Multi-stage build for LLM Council

# Stage 1: Build frontend
FROM node:20-slim AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

# Stage 2: Python runtime
FROM python:3.10-slim
WORKDIR /app

# Install Python dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Copy backend code
COPY backend/ ./backend/

# Copy built frontend from stage 1
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist

# Copy other necessary files
COPY pyproject.toml ./

# Set environment variables
ENV PORT=8080

# Run the application
CMD ["python", "-m", "backend.main"]
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python -m backend.main
50 changes: 45 additions & 5 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
"""FastAPI backend for LLM Council."""

import os
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import List, Dict, Any
import uuid
import json
import asyncio
from pathlib import Path

from . import storage
from .council import run_full_council, generate_conversation_title, stage1_collect_responses, stage2_collect_rankings, stage3_synthesize_final, calculate_aggregate_rankings

app = FastAPI(title="LLM Council API")

# Enable CORS for local development
# CORS configuration - allow localhost for dev and any origin for production
# In production, the frontend is served from the same origin so CORS isn't needed
# but we keep permissive settings for flexibility
cors_origins = [
"http://localhost:5173",
"http://localhost:3000",
"http://localhost:8001",
]

# Add Railway domain if RAILWAY_PUBLIC_DOMAIN is set
railway_domain = os.getenv("RAILWAY_PUBLIC_DOMAIN")
if railway_domain:
cors_origins.append(f"https://{railway_domain}")

app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:3000"],
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand Down Expand Up @@ -50,8 +66,8 @@ class Conversation(BaseModel):
messages: List[Dict[str, Any]]


@app.get("/")
async def root():
@app.get("/api/health")
async def health_check():
"""Health check endpoint."""
return {"status": "ok", "service": "LLM Council API"}

Expand Down Expand Up @@ -194,6 +210,30 @@ async def event_generator():
)


# Mount static files for frontend (must be after all API routes)
# This serves the built frontend from /frontend/dist
static_dir = Path(__file__).parent.parent / "frontend" / "dist"
if static_dir.exists():
from fastapi.responses import FileResponse

# Serve static assets
app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets")

# Catch-all route for SPA - serve index.html for any non-API route
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
# Don't intercept API routes
if full_path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not found")

# Serve index.html for all other routes (SPA routing)
index_file = static_dir / "index.html"
if index_file.exists():
return FileResponse(index_file)
raise HTTPException(status_code=404, detail="Frontend not built")


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)
port = int(os.getenv("PORT", 8001))
uvicorn.run(app, host="0.0.0.0", port=port)
2 changes: 2 additions & 0 deletions frontend/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Development environment - API runs on separate port
VITE_API_BASE=http://localhost:8001
4 changes: 3 additions & 1 deletion frontend/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* API client for the LLM Council backend.
*/

const API_BASE = 'http://localhost:8001';
// In production (same origin), use empty string for relative URLs
// In development, use localhost:8001
const API_BASE = import.meta.env.VITE_API_BASE ?? '';

export const api = {
/**
Expand Down
16 changes: 16 additions & 0 deletions nixpacks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Nixpacks configuration for Railway deployment

# Use both Node and Python providers
providers = ["node", "python"]

[phases.install]
cmds = [
"cd frontend && npm ci",
"pip install -r requirements.txt"
]

[phases.build]
cmds = ["cd frontend && npm run build"]

[start]
cmd = "python -m backend.main"
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "llm-council",
"private": true,
"scripts": {
"build": "cd frontend && npm ci && npm run build",
"dev": "cd frontend && npm run dev"
}
}
10 changes: 10 additions & 0 deletions railway.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "DOCKERFILE"
},
"deploy": {
"healthcheckPath": "/api/health",
"healthcheckTimeout": 300
}
}
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
python-dotenv>=1.0.0
httpx>=0.27.0
pydantic>=2.9.0