Spaces:
Sleeping
Sleeping
| """uofa setup β install / verify the LLM extract runtime (REQ-DIST-003+).""" | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from uofa_cli import setup_bundle, setup_install, setup_state, setup_uninstall, setup_verify | |
| from uofa_cli.output import error, info, step_header | |
| HELP = "install or verify the LLM extract runtime (Ollama + qwen3.5:4b)" | |
| _DEFAULT_MODEL = "qwen3.5:4b" | |
| def add_arguments(parser: argparse.ArgumentParser) -> None: | |
| # Top-level options that select install variants without requiring a | |
| # subcommand keyword. `--bundle <path>` and `--create-bundle <path>` | |
| # mirror the spec wording in REQ-DIST-004. | |
| parser.add_argument( | |
| "--bundle", type=Path, default=None, | |
| help="install from an offline bundle (REQ-DIST-004 air-gapped path)", | |
| ) | |
| parser.add_argument( | |
| "--create-bundle", type=Path, default=None, dest="create_bundle", | |
| help="package the current install into a tar.gz for an offline machine", | |
| ) | |
| parser.add_argument( | |
| "--model", default=_DEFAULT_MODEL, | |
| help=f"model tag to pull (default: {_DEFAULT_MODEL})", | |
| ) | |
| parser.add_argument( | |
| "--port", type=int, default=11434, | |
| help="port for the managed Ollama daemon (default: 11434)", | |
| ) | |
| parser.add_argument( | |
| "--no-byo", action="store_true", | |
| help="skip BYO Ollama detection; install a UofA-managed copy", | |
| ) | |
| parser.add_argument( | |
| "--yes", "-y", action="store_true", | |
| help="skip confirmation prompts (downloads, uninstall)", | |
| ) | |
| parser.add_argument( | |
| "--no-verify", action="store_true", | |
| help="skip the post-install verify step", | |
| ) | |
| sub = parser.add_subparsers(dest="setup_cmd", required=False) | |
| sub.add_parser( | |
| "verify", | |
| help="run a known extraction against a fixture and assert F1 >= 0.95", | |
| ) | |
| sub.add_parser( | |
| "uninstall", | |
| help="remove the UofA-managed Ollama runtime + model (REQ-DIST-007)", | |
| ) | |
| def run(args: argparse.Namespace) -> int: | |
| cmd = getattr(args, "setup_cmd", None) | |
| if cmd == "verify": | |
| return _run_verify(args) | |
| if cmd == "uninstall": | |
| return _run_uninstall(args) | |
| # No subcommand: pick install path based on flags. | |
| if getattr(args, "create_bundle", None) is not None: | |
| return _run_create_bundle(args) | |
| if getattr(args, "bundle", None) is not None: | |
| return _run_install_from_bundle(args) | |
| return _run_install(args) | |
| # ββ install ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _run_install(args: argparse.Namespace) -> int: | |
| step_header("uofa setup β install LLM extract runtime") | |
| prefer_byo = not args.no_byo | |
| byo = setup_state.detect_byo_ollama() if prefer_byo else None | |
| _print_install_summary(byo, args.model, args.port) | |
| if not args.yes and not _confirm("Proceed with install? [y/N] "): | |
| info("Aborted.") | |
| return 1 | |
| started = time.monotonic() | |
| progress_state = {"last_label": ""} | |
| def on_status(msg: str) -> None: | |
| info(msg) | |
| def on_progress(done: int, total: int) -> None: | |
| if not total: | |
| return | |
| pct = (done / total) * 100 | |
| mb_done = done / (1024 * 1024) | |
| mb_total = total / (1024 * 1024) | |
| label = f" {pct:5.1f}% {mb_done:7.1f} / {mb_total:7.1f} MB" | |
| if label != progress_state["last_label"]: | |
| print(label, file=sys.stderr) | |
| progress_state["last_label"] = label | |
| try: | |
| cfg = setup_install.install( | |
| prefer_byo=prefer_byo, | |
| model_tag=args.model, | |
| port=args.port, | |
| on_status=on_status, | |
| on_progress=on_progress, | |
| ) | |
| except Exception as e: | |
| error(f"Install failed: {e}") | |
| return 1 | |
| elapsed = time.monotonic() - started | |
| _print_install_success(cfg, elapsed) | |
| if args.no_verify: | |
| return 0 | |
| info("") | |
| info("Running verify to confirm install...") | |
| verify_result = setup_verify.verify(cfg, on_status=on_status) | |
| if not verify_result.ok: | |
| error(f"Verify failed: {verify_result.diagnostic}") | |
| info("Try `uofa setup verify` again, or `uofa setup` to repair.") | |
| return 1 | |
| info(f"β {verify_result.diagnostic} (in {verify_result.elapsed_seconds:.1f}s)") | |
| return 0 | |
| def _run_verify(args: argparse.Namespace) -> int: | |
| step_header("uofa setup verify β end-to-end smoke test") | |
| cfg = setup_state.load_config() | |
| if cfg is None: | |
| error("No install detected. Run `uofa setup` first.") | |
| return 1 | |
| info(f"Using {cfg.mode} Ollama at {cfg.ollama_binary} (port {cfg.ollama_port}).") | |
| result = setup_verify.verify(cfg, on_status=info) | |
| if result.ok: | |
| info(f"β {result.diagnostic} (in {result.elapsed_seconds:.1f}s)") | |
| info(f" Try next: uofa extract <your-pdf>") | |
| return 0 | |
| error(f"β {result.diagnostic} (after {result.elapsed_seconds:.1f}s)") | |
| return 1 | |
| # ββ create-bundle / install-from-bundle (REQ-DIST-004) βββββββββ | |
| def _run_create_bundle(args: argparse.Namespace) -> int: | |
| step_header("uofa setup --create-bundle β package install for offline transport") | |
| cfg = setup_state.load_config() | |
| if cfg is None: | |
| error("No install to bundle. Run `uofa setup` first.") | |
| return 1 | |
| output = args.create_bundle | |
| if output.is_dir(): | |
| output = output / setup_bundle.default_bundle_filename() | |
| info(f" Bundle target: {output}") | |
| info(f" Source binary: {cfg.ollama_binary}") | |
| info(f" Source models: {cfg.ollama_models_dir or '~/.ollama/models'}") | |
| info(f" Model: {cfg.model_tag}") | |
| if not args.yes and not _confirm("Proceed with bundle creation? [y/N] "): | |
| info("Aborted.") | |
| return 1 | |
| try: | |
| result_path = setup_bundle.create_bundle( | |
| output, cfg=cfg, model_tag=cfg.model_tag, | |
| ) | |
| except FileNotFoundError as e: | |
| error(str(e)) | |
| return 1 | |
| size_mb = result_path.stat().st_size / (1024 * 1024) | |
| info(f"β Wrote {result_path} ({size_mb:.1f} MB)") | |
| info("Transfer this file to the air-gapped machine and run:") | |
| info(f" uofa setup --bundle {result_path.name}") | |
| return 0 | |
| def _run_install_from_bundle(args: argparse.Namespace) -> int: | |
| step_header("uofa setup --bundle β install from offline bundle") | |
| bundle_path = args.bundle | |
| if not bundle_path.is_file(): | |
| error(f"Bundle not found: {bundle_path}") | |
| return 1 | |
| info(f"Reading {bundle_path}") | |
| try: | |
| cfg = setup_bundle.consume_bundle(bundle_path, on_status=info) | |
| except setup_bundle.PlatformMismatchError as e: | |
| error(str(e)) | |
| return 1 | |
| except (FileNotFoundError, ValueError) as e: | |
| error(f"Bundle install failed: {e}") | |
| return 1 | |
| info("") | |
| info("Running verify against the unpacked install...") | |
| result = setup_verify.verify(cfg, on_status=info) | |
| if result.ok: | |
| info(f"β {result.diagnostic} (in {result.elapsed_seconds:.1f}s)") | |
| info(f" Try next: uofa extract <your-pdf>") | |
| return 0 | |
| error(f"β Verify failed: {result.diagnostic}") | |
| return 1 | |
| # ββ uninstall (REQ-DIST-007) βββββββββββββββββββββββββββββββββββ | |
| def _run_uninstall(args: argparse.Namespace) -> int: | |
| step_header("uofa setup uninstall β remove managed runtime + model") | |
| cfg = setup_state.load_config() | |
| plan = setup_uninstall.plan_uninstall(cfg) | |
| if not plan.targets: | |
| info("Nothing to remove.") | |
| return 0 | |
| info("Will remove:") | |
| for target in plan.targets: | |
| info(f" {target}") | |
| info(f"Disk space to free: {plan.mb_to_free:.1f} MB") | |
| if cfg is not None and cfg.mode == "byo": | |
| info(f"Will NOT touch BYO Ollama at {cfg.ollama_binary}") | |
| if not args.yes and not _confirm("Proceed with uninstall? [y/N] "): | |
| info("Aborted.") | |
| return 1 | |
| result = setup_uninstall.uninstall(cfg, on_status=info) | |
| info(f"β Removed {len(result.removed)} target(s); freed {result.bytes_freed / (1024*1024):.1f} MB") | |
| if result.skipped: | |
| for s in result.skipped: | |
| info(f" (skipped {s})") | |
| return 0 | |
| # ββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _print_install_summary(byo: Path | None, model: str, port: int) -> None: | |
| info("") | |
| if byo is not None: | |
| info(f"Detected existing Ollama: {byo}") | |
| info(f" β will register and pull {model} into its model store") | |
| info(" β no UofA-managed binary will be downloaded") | |
| info(" β estimated additional disk: ~3 GB (model only)") | |
| else: | |
| platform_tag = setup_install.detect_wheel_platform_tag() | |
| try: | |
| entry = setup_install.resolve_platform_entry(platform_tag) | |
| url = entry.url | |
| ver = entry.version | |
| info(f"Will install Ollama {ver} for {platform_tag}") | |
| info(f" Source: {url}") | |
| except Exception: | |
| info(f"Will install Ollama for {platform_tag} (manifest lookup failed)") | |
| info(f"Model: {model}") | |
| info(f"Daemon port: {port}") | |
| info(f"Install location: {setup_state.uofa_data_dir()}") | |
| info("Estimated disk usage: ~5 GB total (Ollama runtime + model)") | |
| def _confirm(prompt: str) -> bool: | |
| try: | |
| answer = input(prompt).strip().lower() | |
| except (EOFError, KeyboardInterrupt): | |
| return False | |
| return answer in ("y", "yes") | |
| def _print_install_success(cfg: setup_state.SetupConfig, elapsed: float) -> None: | |
| info("") | |
| info("ββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| info("β uofa setup complete") | |
| info(f" Mode: {cfg.mode}") | |
| info(f" Ollama: {cfg.ollama_binary}") | |
| info(f" Model: {cfg.model_tag}") | |
| info(f" Port: {cfg.ollama_port}") | |
| info(f" Config: {setup_state.config_path()}") | |
| info(f" Elapsed: {elapsed:.1f}s") | |
| info("") | |
| info("Try next:") | |
| info(" uofa extract <your-pdf-or-directory>") | |
| info("ββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |