"""The frontend's web service: JSON routes over ``run_view`` and the built page, on localhost only.
``factor-agent-web <folder>`` serves the run directories under a folder on
``http://127.0.0.1:<port>``. It binds the loopback interface and has no
option to bind another: the pages show prompts, code and statistics and
there is no login. On a server, forward the port over SSH.
Routes:
- ``GET /api/runs``: every run under the folder (``list_runs``), the latest written first;
- ``GET /api/run?name=<name>``: one run (``RunView.read``) by its name in the list, 404 for any other name;
- ``GET /api/run/call?name=<name>&number=<number>``: one LLM call of a run (``read_call``) by the number
of its call file, 404 when the run has no call file of that number;
- ``GET /api/run/files?name=<name>``: the config of each start or resume of a run and its snapshot's
files (``read_files``), read only when the page shows them;
- ``GET /api/run/ic?name=<name>&round=<round>&slot=<slot>``: a trial's daily IC and partial IC (``read_ic``),
404 when it was not evaluated or the run kept none;
- ``GET /api/run/round?name=<name>&round=<round>``: one round's plan (``read_round``), its slots, quota and
issues and its Trial graph with each node's ranks, the round in progress included, 404 for a round
neither written nor planned;
- ``GET /api/run/version?name=<name>``: a text that changes whenever the run writes (``run_version``),
which the page polls before reading the run again;
- ``GET /``: the page, built from ``frontend/`` into ``factor_agent/web/static``.
FastAPI and uvicorn come with the ``web`` extra and are imported only when
the app is made, so the module imports without them.
"""
import argparse
from pathlib import Path
from factor_agent.web.run_view import (
RunView, find_run, list_runs, read_call, read_files, read_ic, read_round, run_version,
)
#: The built page: ``npm run build`` in ``frontend/`` writes it, and it is committed.
STATIC = Path(__file__).parent / "static"
#: The interface the service binds: the loopback interface only.
HOST = "127.0.0.1"
[docs]
def create_app(root: str | Path):
"""The web service over the run directories under ``root``.
Parameters
----------
root : str or Path
A folder of run directories, at any depth, or a run directory itself.
Returns
-------
fastapi.FastAPI
Raises
------
ImportError
When the ``web`` extra is not installed.
"""
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
root = Path(root).resolve()
app = FastAPI(title="factor-agent", docs_url=None, redoc_url=None, openapi_url=None)
@app.get("/api/runs")
def runs():
return list_runs(root)
def run_dir(name: str) -> Path:
# Only a listed name is read: a name never reaches the file system unchecked.
found = find_run(root, name)
if found is None:
raise HTTPException(404, f"no run named {name!r} under {root}")
return found
@app.get("/api/run")
def run(name: str):
return RunView.read(run_dir(name), name=name)
@app.get("/api/run/call")
def call(name: str, number: int):
found = read_call(run_dir(name), number)
if found is None:
raise HTTPException(404, f"run {name!r} has no LLM call number {number}")
return found
@app.get("/api/run/files")
def files(name: str):
return read_files(run_dir(name))
@app.get("/api/run/ic")
def ic(name: str, round: int, slot: int):
found = read_ic(run_dir(name), round, slot)
if found is None:
raise HTTPException(404, f"run {name!r} kept no daily IC of round {round}, slot {slot}")
return found
@app.get("/api/run/round")
def plan(name: str, round: int):
found = read_round(run_dir(name), round)
if found is None:
raise HTTPException(404, f"run {name!r} has neither written nor planned round {round}")
return found
@app.get("/api/run/version")
def version(name: str):
return run_version(run_dir(name))
app.mount("/", StaticFiles(directory=STATIC, html=True), name="page")
return app
[docs]
def main(argv: list[str] | None = None) -> None:
"""Serve the run directories under a folder on localhost: the ``factor-agent-web`` console script."""
parser = argparse.ArgumentParser(
prog="factor-agent-web",
description="Serve the run directories under a folder on http://127.0.0.1:<port> (localhost only).",
)
parser.add_argument("root", type=Path, help="a folder of run directories, or a run directory")
parser.add_argument("--port", type=int, default=8000, help="the port to listen on (default: 8000)")
args = parser.parse_args(argv)
try:
import uvicorn
except ImportError:
parser.exit(1, "factor-agent-web: the frontend needs the web extra: uv sync --extra web\n")
if not args.root.is_dir():
parser.exit(1, f"factor-agent-web: {args.root} is not a folder\n")
print(f"factor-agent-web: serving the runs under {args.root.resolve()} on http://{HOST}:{args.port}", flush=True)
uvicorn.run(create_app(args.root), host=HOST, port=args.port)