Source code for factor_agent.web.app

"""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)