| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- """CLI for visualizing agent run logs."""
- from __future__ import annotations
- import argparse
- import webbrowser
- from pathlib import Path
- from supply_agent.logging.visualize import generate_visualization
- # supply_agent/logging/cli.py → project root
- _PROJECT_ROOT = Path(__file__).resolve().parents[2]
- def _has_run_logs(directory: Path) -> bool:
- if not directory.is_dir():
- return False
- return any(directory.glob("run_*.jsonl")) or any(directory.glob("run_*.log"))
- def _resolve_logs_dir(logs_dir: str | Path) -> Path:
- """
- Resolve logs directory.
- Relative paths prefer the SupplyAgent project root (stable regardless of
- CWD). Falls back to CWD only when that location actually contains run logs.
- """
- path = Path(logs_dir)
- if path.is_absolute():
- return path.resolve()
- project_candidate = (_PROJECT_ROOT / path).resolve()
- cwd_candidate = (Path.cwd() / path).resolve()
- if _has_run_logs(project_candidate):
- return project_candidate
- if _has_run_logs(cwd_candidate):
- return cwd_candidate
- if project_candidate.is_dir():
- return project_candidate
- if cwd_candidate.is_dir():
- return cwd_candidate
- return project_candidate
- def _resolve_run_path(run: str | Path) -> Path:
- path = Path(run)
- if path.is_absolute():
- return path.resolve()
- cwd_candidate = (Path.cwd() / path).resolve()
- if cwd_candidate.exists():
- return cwd_candidate
- project_candidate = (_PROJECT_ROOT / path).resolve()
- if project_candidate.exists():
- return project_candidate
- # Stem / missing suffix: try under project logs/
- logs = _resolve_logs_dir("logs")
- for suffix in (".jsonl", ".log", ""):
- candidate = logs / f"{path.name}{suffix}" if suffix else logs / path.name
- if candidate.exists():
- return candidate
- return project_candidate
- def _find_latest_run(logs_dir: Path) -> Path:
- runs = _list_all_runs(logs_dir)
- if not runs:
- raise FileNotFoundError(f"No run_*.jsonl / run_*.log found in {logs_dir}")
- return runs[-1]
- def _list_all_runs(logs_dir: Path) -> list[Path]:
- """
- List all unique agent runs in logs_dir.
- Prefers ``.jsonl`` over ``.log`` when both exist for the same stem.
- Sorted by mtime ascending (oldest first).
- """
- logs_dir = _resolve_logs_dir(logs_dir)
- if not logs_dir.is_dir():
- raise FileNotFoundError(f"Logs directory not found: {logs_dir}")
- by_stem: dict[str, Path] = {}
- for path in logs_dir.glob("run_*.jsonl"):
- by_stem[path.stem] = path
- for path in logs_dir.glob("run_*.log"):
- by_stem.setdefault(path.stem, path)
- return sorted(by_stem.values(), key=lambda p: p.stat().st_mtime)
- def main(argv: list[str] | None = None) -> int:
- """
- Generate HTML visualizations for agent run logs.
- Parameters
- ----------
- argv:
- Optional CLI args. Use ``main()`` or ``main([])`` from Python to
- visualize all runs under the project ``logs/`` directory (CWD-independent).
- """
- parser = argparse.ArgumentParser(
- description="将 Agent 运行日志生成为可视化 HTML 页面",
- )
- parser.add_argument(
- "run",
- nargs="?",
- help="日志路径(.jsonl / .log / 不带后缀的 run id);省略则处理 logs 目录下全部运行",
- )
- parser.add_argument(
- "-o",
- "--output",
- help="输出 HTML 路径(仅单文件模式有效;默认与日志同目录同名 .html)",
- )
- parser.add_argument(
- "--logs-dir",
- default="logs",
- help="日志目录(默认项目根下 logs;无参数时批量生成,或配合 --latest)",
- )
- parser.add_argument(
- "--latest",
- action="store_true",
- help="仅可视化 logs 目录中最新一次运行",
- )
- parser.add_argument(
- "--open",
- action="store_true",
- help="生成后在浏览器中打开(批量模式打开最新一份)",
- )
- # 直接 main() / main(None) 视为无参数批量,避免 IDE / 其它入口的 argv 干扰
- args = parser.parse_args([] if argv is None else argv)
- logs_dir = _resolve_logs_dir(args.logs_dir)
- if args.latest:
- run_paths = [_find_latest_run(logs_dir)]
- elif args.run:
- run_paths = [_resolve_run_path(args.run)]
- else:
- try:
- run_paths = _list_all_runs(logs_dir)
- except FileNotFoundError as exc:
- print(exc)
- return 1
- if not run_paths:
- print(f"未找到可可视化的日志: {logs_dir}/run_*.jsonl|*.log")
- return 1
- if args.output:
- parser.error("批量生成时不能指定 --output,请省略 -o 或指定单个 run 路径")
- outputs: list[Path] = []
- errors = 0
- for run_path in run_paths:
- try:
- out = generate_visualization(
- run_path,
- args.output if len(run_paths) == 1 else None,
- )
- outputs.append(out)
- print(f"已生成可视化页面: {out}")
- except Exception as exc:
- errors += 1
- print(f"跳过 {run_path}: {exc}")
- if not outputs:
- return 1
- if args.open:
- webbrowser.open(outputs[-1].resolve().as_uri())
- if len(outputs) > 1:
- print(f"共生成 {len(outputs)} 个页面" + (f",失败 {errors} 个" if errors else ""))
- return 1 if errors else 0
- if __name__ == "__main__":
- import sys
- raise SystemExit(main(sys.argv[1:]))
|