Skip to content

Commit

Permalink
API & UI package
Browse files Browse the repository at this point in the history
  • Loading branch information
aorwall committed Jan 26, 2025
1 parent 98c589a commit aff4fd9
Show file tree
Hide file tree
Showing 12 changed files with 2,169 additions and 2,476 deletions.
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,13 @@ logs
Pipfile
evals
test_results
experiments
experiments

# UI build output
moatless_api/

# IDE
.idea/
.vscode/
*.swp
*.swo
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,37 +202,31 @@ python -m moatless.benchmark.run_evaluation \
# Running the UI and API
The project includes a web UI for visualizing saved trajectory files, built with SvelteKit.
The project includes a web UI for visualizing saved trajectory files, built with SvelteKit. The UI is packaged with the Python package and will be served by the API server.
First, make sure you have the required components installed:
```bash
# Install from PyPI:
pip install "moatless[api]"
# Or if installing from source:
# Using Poetry:
poetry install --with api
```
### Start the API Server
```bash
# If installed from PyPI or using pip:
python -m moatless.api
# If using Poetry:
poetry run moatless-api
moatless-api
```
This will start the FastAPI server on http://localhost:8000.
### Start the UI Development Server
This will start the FastAPI server on http://localhost:8000 and serve the UI at the same address.
### Development Mode
If you want to develop the UI, you can run it in development mode:
```bash
# From the ui directory
cd ui
pnpm install
pnpm run dev
```
The UI will be available at http://localhost:5173. Currently, it provides a view for exploring saved trajectory files.
The UI development server will be available at http://localhost:5173.
# Code Examples
Expand Down
71 changes: 71 additions & 0 deletions build_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
import shutil

def main():
"""Build the UI using pnpm."""
ui_dir = Path(__file__).parent / "ui"
moatless_api_dir = Path(__file__).parent / "moatless_api"
moatless_api_pkg = moatless_api_dir # Package directory
dist_dir = moatless_api_pkg / "ui/dist"

if not ui_dir.exists():
print("UI directory not found")
return

print("Building UI...")
try:
# Install dependencies
subprocess.run(["pnpm", "install"], cwd=ui_dir, check=True)
# Build UI
subprocess.run(["pnpm", "run", "build"], cwd=ui_dir, check=True)

# Create package directories and __init__.py files
moatless_api_dir.mkdir(parents=True, exist_ok=True)
moatless_api_pkg.mkdir(parents=True, exist_ok=True)
(moatless_api_pkg / "ui").mkdir(parents=True, exist_ok=True)

# Create package __init__.py files
(moatless_api_dir / "__init__.py").write_text('"""Package for Moatless API UI files."""\n')
(moatless_api_pkg / "__init__.py").write_text('"""Moatless API package."""\n')
(moatless_api_pkg / "ui" / "__init__.py").write_text('"""UI package."""\n')

# Create setup.py
setup_py = moatless_api_dir / "setup.py"
setup_py.write_text("""
from setuptools import setup, find_packages
setup(
name="moatless_api",
version="0.0.1",
packages=find_packages(),
include_package_data=True,
package_data={
"moatless_api": ["ui/dist/**/*"],
},
)
""")

# Create MANIFEST.in
manifest = moatless_api_dir / "MANIFEST.in"
manifest.write_text("recursive-include moatless_api/ui/dist *")

# Move built files to package directory
if dist_dir.exists():
shutil.rmtree(dist_dir)
dist_dir.parent.mkdir(parents=True, exist_ok=True)
print(f"Moving built files from {ui_dir / 'dist'} to {dist_dir}")
shutil.move(str(ui_dir / "dist"), str(dist_dir))

print("UI build complete")
except subprocess.CalledProcessError as e:
print(f"Failed to build UI: {e}")
sys.exit(1)
except FileNotFoundError:
print("pnpm not found. Please install pnpm to build the UI")
sys.exit(1)

if __name__ == "__main__":
main()
72 changes: 62 additions & 10 deletions moatless/api/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import FastAPI, HTTPException, UploadFile
from fastapi import FastAPI, HTTPException, UploadFile, Request
from typing import List, Dict, Any
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
Expand All @@ -8,6 +8,9 @@
from moatless.artifacts.artifact import ArtifactListItem
from moatless.api.schema import TrajectoryDTO
from moatless.api.trajectory_utils import load_trajectory_from_file, create_trajectory_dto
from fastapi.staticfiles import StaticFiles
import importlib.resources as pkg_resources
from pathlib import Path


logger = logging.getLogger(__name__)
Expand All @@ -19,9 +22,12 @@ def create_api(workspace: Workspace | None = None) -> FastAPI:

# Add CORS middleware with proper configuration
origins = [
"http://localhost:5173", # SvelteKit dev server
"http://127.0.0.1:5173", # Alternative local dev URL (IPv4)
"http://[::1]:5173", # Alternative local dev URL (IPv6)
"http://localhost:5173", # SvelteKit dev server
"http://127.0.0.1:5173", # Alternative local dev URL (IPv4)
"http://[::1]:5173", # Alternative local dev URL (IPv6)
"http://localhost:4173", # SvelteKit preview server
"http://127.0.0.1:4173", # Alternative preview URL (IPv4)
"http://[::1]:4173", # Alternative preview URL (IPv6)
]

api.add_middleware(
Expand All @@ -33,21 +39,23 @@ def create_api(workspace: Workspace | None = None) -> FastAPI:
max_age=3600, # Cache preflight requests for 1 hour
)

if workspace is not None:
# Create API router with /api prefix
router = FastAPI(title="Moatless API")

@api.get("/artifacts", response_model=List[ArtifactListItem])
if workspace is not None:
@router.get("/artifacts", response_model=List[ArtifactListItem])
async def list_all_artifacts():
"""Get all artifacts across all types"""
return workspace.get_all_artifacts()

@api.get("/artifacts/{type}", response_model=List[ArtifactListItem])
@router.get("/artifacts/{type}", response_model=List[ArtifactListItem])
async def list_artifacts(type: str):
try:
return workspace.get_artifacts_by_type(type)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))

@api.get("/artifacts/{type}/{id}", response_model=Dict[str, Any])
@router.get("/artifacts/{type}/{id}", response_model=Dict[str, Any])
async def get_artifact(type: str, id: str):
try:
artifact = workspace.get_artifact(type, id)
Expand All @@ -57,15 +65,15 @@ async def get_artifact(type: str, id: str):
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))

@api.get("/trajectory", response_model=TrajectoryDTO)
@router.get("/trajectory", response_model=TrajectoryDTO)
async def get_trajectory(file_path: str):
"""Get trajectory data from a file path"""
try:
return load_trajectory_from_file(file_path)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))

@api.post("/trajectory/upload", response_model=TrajectoryDTO)
@router.post("/trajectory/upload", response_model=TrajectoryDTO)
async def upload_trajectory(file: UploadFile):
"""Upload and process a trajectory file"""
try:
Expand All @@ -75,4 +83,48 @@ async def upload_trajectory(file: UploadFile):
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid trajectory file: {str(e)}")

# Mount the API router with /api prefix
api.mount("/api", router)

# Only serve UI files if API extras are installed
try:
import fastapi.staticfiles
# Try to find UI files in the installed package
ui_files = pkg_resources.files('moatless_api') / 'ui/dist'
if ui_files.exists():
logger.info(f"Found UI files in package at {ui_files}")

# Serve static files from _app directory
api.mount("/_app", StaticFiles(directory=str(ui_files / "_app")), name="static")

# Create a static files instance for serving index.html
html_app = StaticFiles(directory=str(ui_files), html=True)

@api.get("/{full_path:path}")
async def serve_spa(request: Request, full_path: str):
if full_path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not found")
return await html_app.get_response("index.html", request.scope)
else:
# Fallback to development path
ui_path = Path("ui/dist")
if ui_path.exists():
logger.info(f"Found UI files in development path at {ui_path}")

# Serve static files from _app directory
api.mount("/_app", StaticFiles(directory=str(ui_path / "_app")), name="static")

# Create a static files instance for serving index.html
html_app = StaticFiles(directory=str(ui_path), html=True)

@api.get("/{full_path:path}")
async def serve_spa(request: Request, full_path: str):
if full_path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not found")
return await html_app.get_response("index.html", request.scope)
else:
logger.info("No UI files found")
except ImportError:
logger.info("API extras not installed, UI will not be served")

return api
Loading

0 comments on commit aff4fd9

Please sign in to comment.