app.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. #!/usr/bin/env python3
  2. """Production Build Run 的文件型只读可视化服务。"""
  3. from __future__ import annotations
  4. import os
  5. from pathlib import Path
  6. from fastapi import FastAPI, HTTPException, Query
  7. from fastapi.middleware.cors import CORSMiddleware
  8. from fastapi.middleware.gzip import GZipMiddleware
  9. from fastapi.responses import FileResponse, HTMLResponse
  10. from .store import DEFAULT_RUN_ROOT, FileRunStore, media_type
  11. WEB_DIR = Path(__file__).resolve().parent
  12. RUN_ROOT = Path(
  13. os.getenv("PRODUCTION_VISUALIZATION_RUN_ROOT", str(DEFAULT_RUN_ROOT))
  14. )
  15. store = FileRunStore(RUN_ROOT)
  16. app = FastAPI(
  17. title="Production Build · Run Viewer",
  18. version="0.1.0",
  19. )
  20. app.add_middleware(GZipMiddleware, minimum_size=1024)
  21. app.add_middleware(
  22. CORSMiddleware,
  23. allow_origins=["*"],
  24. allow_methods=["GET"],
  25. allow_headers=["*"],
  26. )
  27. @app.middleware("http")
  28. async def _no_cache(request, call_next):
  29. response = await call_next(request)
  30. response.headers["Cache-Control"] = "no-store"
  31. return response
  32. def _run_ref(run_id: int):
  33. try:
  34. return store.get(run_id)
  35. except KeyError as exc:
  36. raise HTTPException(status_code=404, detail="run not found") from exc
  37. @app.get("/", response_class=HTMLResponse)
  38. def index():
  39. return (WEB_DIR / "index.html").read_text(encoding="utf-8")
  40. @app.get("/api/health")
  41. def health():
  42. refs = store.discover()
  43. return {
  44. "ok": True,
  45. "data_source": "run_files",
  46. "run_root": str(store.run_root),
  47. "run_count": len(refs),
  48. }
  49. @app.get("/api/runs")
  50. def list_runs(
  51. status: str | None = None,
  52. page: int = Query(1, ge=1),
  53. page_size: int = Query(50, ge=1, le=200),
  54. ):
  55. runs = [store.brief(ref) for ref in store.discover()]
  56. if status:
  57. runs = [run for run in runs if run["status"] == status]
  58. start = (page - 1) * page_size
  59. return {
  60. "total": len(runs),
  61. "page": page,
  62. "page_size": page_size,
  63. "runs": runs[start : start + page_size],
  64. }
  65. @app.get("/api/runs/{run_id}")
  66. def get_run(run_id: int):
  67. return store.detail(_run_ref(run_id))
  68. @app.get("/api/runs/{run_id}/snapshots")
  69. def get_snapshots(run_id: int):
  70. return store.snapshots(_run_ref(run_id))
  71. @app.get("/api/runs/{run_id}/flat")
  72. def get_flat(run_id: int, round: int | None = None):
  73. return store.flat(_run_ref(run_id), round)
  74. @app.get("/api/runs/{run_id}/annotations")
  75. def get_annotations(run_id: int):
  76. _run_ref(run_id)
  77. return {
  78. "run_id": run_id,
  79. "by_node": {},
  80. "by_call": {},
  81. "docs": {},
  82. }
  83. @app.get("/api/tools")
  84. def get_tools(agent: str = "production"):
  85. return {"agent": agent, "tools": store.tools()}
  86. @app.get("/api/runs/{run_id}/media/{relative_path:path}")
  87. def get_media(run_id: int, relative_path: str):
  88. ref = _run_ref(run_id)
  89. try:
  90. path = store.media_path(ref, relative_path)
  91. except FileNotFoundError as exc:
  92. raise HTTPException(status_code=404, detail="media not found") from exc
  93. return FileResponse(path, media_type=media_type(path))
  94. if __name__ == "__main__":
  95. import uvicorn
  96. uvicorn.run(app, host="127.0.0.1", port=8930)