From 7a29e7867dd6a043c65398b4f2ae8514689386fa Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 21:17:20 +0200 Subject: [PATCH] Add YOLO preflight runtime diagnostics --- CHANGELOG.md | 1 + backend/README.md | 2 +- .../app/services/yolo_preflight_service.py | 40 +++++++++++++++++++ backend/tests/test_sprint13_yolo_preflight.py | 29 +++++++++++++- docs/AI_PIPELINES.md | 4 ++ docs/CODEX_EXECUTION_LOG.md | 13 ++++++ 6 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e702dd7a..dafdcd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added an opt-in Docker/Unraid AI build path (`GEOINTEL_INSTALL_AI=true`) for installing optional PyTorch/Ultralytics dependencies while keeping the default GIS runtime lightweight and import-safe. - Hardened the AI Docker runtime with OpenCV native libraries required by Ultralytics and made YOLO dependency detection use real imports instead of optimistic module discovery. - Added a writable `YOLO_CONFIG_DIR` default under application storage so Ultralytics does not fall back to root user config paths in Docker/Unraid. +- Added YOLO preflight runtime diagnostics for dependency assumption state, model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` versions and CUDA availability without running inference or downloading weights. - Added static regression coverage for the road basemap, attribution, basemap policy notice, database layer selector and persisted operational GIS workflow wiring. ## Sprint 115 QA/QC and Exports usability layout pass (2026-07-04) diff --git a/backend/README.md b/backend/README.md index 4dd1ef23..bfb799e2 100644 --- a/backend/README.md +++ b/backend/README.md @@ -299,7 +299,7 @@ To validate only local model/manifest paths on a machine without optional AI dep python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --assume-dependencies --json ``` -The preflight checks configuration, dependency availability, local model file existence, tile manifest validity, tile count and referenced tile paths. It does not load a YOLO model, run inference or download weights. +The preflight checks configuration, dependency availability, local model file existence, tile manifest validity, tile count and referenced tile paths. JSON output also includes runtime diagnostics for the model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` versions and CUDA availability when dependency checks pass. It does not load a YOLO model, run inference or download weights. ### Run backend diff --git a/backend/app/services/yolo_preflight_service.py b/backend/app/services/yolo_preflight_service.py index bdd67c3c..45815cfe 100644 --- a/backend/app/services/yolo_preflight_service.py +++ b/backend/app/services/yolo_preflight_service.py @@ -1,5 +1,7 @@ from __future__ import annotations +import os +from importlib import metadata from pathlib import Path from typing import Any, Type @@ -42,6 +44,10 @@ class YoloPreflightService: "max_tiles": resolved_settings.yolo_max_tiles, "will_download_models": False, "will_run_inference": False, + "runtime": YoloPreflightService._runtime_details( + settings=resolved_settings, + assume_dependencies=assume_dependencies, + ), } if not resolved_settings.yolo_enabled: @@ -50,6 +56,8 @@ class YoloPreflightService: dependencies_available = True if assume_dependencies else yolo_adapter_class.dependencies_available() result["checks"]["dependencies_available"] = dependencies_available + if dependencies_available and not assume_dependencies: + result["runtime"]["cuda_available"] = YoloPreflightService._torch_cuda_available() if not dependencies_available: result["status"] = "dependency_unavailable" result["message"] = "YOLO dependencies are not installed. Install backend optional extras with geointel-backend[ai]." @@ -115,3 +123,35 @@ class YoloPreflightService: else: result["message"] = "Configured YOLO preflight passed. No model was loaded and no inference was run." return result + + @staticmethod + def _runtime_details(*, settings: Settings, assume_dependencies: bool) -> dict[str, Any]: + model_directory = None + if settings.yolo_model_path: + model_directory = str(Path(settings.yolo_model_path).expanduser().parent) + return { + "dependencies_assumed": assume_dependencies, + "model_directory": model_directory, + "yolo_config_dir": os.environ.get("YOLO_CONFIG_DIR"), + "torch_version": YoloPreflightService._package_version("torch"), + "ultralytics_version": YoloPreflightService._package_version("ultralytics"), + "cuda_available": None, + } + + @staticmethod + def _package_version(package_name: str) -> str | None: + try: + return metadata.version(package_name) + except metadata.PackageNotFoundError: + return None + + @staticmethod + def _torch_cuda_available() -> bool | None: + try: + import torch + except Exception: + return None + try: + return bool(torch.cuda.is_available()) + except Exception: + return None diff --git a/backend/tests/test_sprint13_yolo_preflight.py b/backend/tests/test_sprint13_yolo_preflight.py index 7c57a766..0716f2f9 100644 --- a/backend/tests/test_sprint13_yolo_preflight.py +++ b/backend/tests/test_sprint13_yolo_preflight.py @@ -54,7 +54,9 @@ def _manifest(tmp_path: Path, tile_count: int = 1) -> Path: return manifest_path -def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path) -> None: +def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setenv("YOLO_CONFIG_DIR", str(tmp_path / "ultralytics")) + result = YoloPreflightService.run( settings=Settings(yolo_enabled=False, yolo_model_path=str(tmp_path / "missing.pt")), tile_manifest_path=str(tmp_path / "missing-manifest.json"), @@ -66,6 +68,12 @@ def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path) - assert result["checks"]["dependencies_available"] is None assert result["checks"]["model_file_exists"] is None assert result["checks"]["manifest_valid"] is None + assert result["runtime"]["dependencies_assumed"] is False + assert result["runtime"]["model_directory"] == str(tmp_path) + assert result["runtime"]["yolo_config_dir"] == str(tmp_path / "ultralytics") + assert "torch_version" in result["runtime"] + assert "ultralytics_version" in result["runtime"] + assert "cuda_available" in result["runtime"] def test_yolo_preflight_distinguishes_missing_dependencies_from_missing_model(tmp_path: Path) -> None: @@ -102,6 +110,25 @@ def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_ assert result["tile_count"] == 2 assert result["will_download_models"] is False assert result["will_run_inference"] is False + assert result["runtime"]["dependencies_assumed"] is False + + +def test_yolo_preflight_marks_assumed_dependencies_in_runtime_details(tmp_path: Path) -> None: + model_path = tmp_path / "model.pt" + model_path.write_bytes(b"weights") + manifest_path = _manifest(tmp_path, tile_count=1) + + result = YoloPreflightService.run( + settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4), + tile_manifest_path=str(manifest_path), + yolo_adapter_class=MissingDependencyAdapter, + assume_dependencies=True, + ) + + assert result["status"] == "ready" + assert result["checks"]["dependencies_available"] is True + assert result["runtime"]["dependencies_assumed"] is True + assert result["runtime"]["model_directory"] == str(tmp_path) def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) -> None: diff --git a/docs/AI_PIPELINES.md b/docs/AI_PIPELINES.md index 2179b7df..3d166805 100644 --- a/docs/AI_PIPELINES.md +++ b/docs/AI_PIPELINES.md @@ -70,6 +70,10 @@ The preflight checks: - tile count against `YOLO_MAX_TILES`; - referenced tile file existence. +JSON output also reports runtime diagnostics: whether dependencies were assumed, +the configured model directory, `YOLO_CONFIG_DIR`, installed `torch` and +`ultralytics` versions, and CUDA availability when dependency checks pass. + The preflight does not load the model, does not import Ultralytics unless dependency discovery requires package metadata, does not run inference and never downloads model weights. Sprint 25 adds an explicit local model compatibility smoke: diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index b04e24c5..6337c269 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7,6 +7,7 @@ Changed: - Passed YOLO runtime environment variables and a `/app/models` volume into the all-in-one Unraid container so local PyTorch/Ultralytics models can be mounted explicitly. - Hardened the AI image path after Tower validation showed `torch` imported but `ultralytics` failed on a missing OpenCV native library. The Dockerfiles now include the required OpenCV runtime shared libraries and YOLO dependency detection performs real imports instead of `find_spec` checks. - Added a writable `YOLO_CONFIG_DIR` default under application storage after Tower validation showed Ultralytics otherwise falls back to `/tmp` because root config is not writable in the container. +- Added YOLO preflight runtime diagnostics so operators can see dependency assumption state, model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` package versions and CUDA availability without loading a model, running inference or downloading weights. - Updated `.env.example`, `backend/README.md`, `frontend/README.md`, `scripts/README.md`, `docs/AI_PIPELINES.md`, `docs/TODO.md` and `CHANGELOG.md`. - Added regression coverage in `backend/tests/test_sprint116_operational_gis_map_workflow.py`, `backend/tests/test_sprint8b_yolo_foundation.py` and `backend/tests/test_docker_runtime_config.py`. @@ -37,6 +38,18 @@ Validation: - Tower container check passed: `torch` imported as `2.12.1+cu130`, `torch.cuda.is_available()` returned `False`, `ultralytics` imported as `8.4.87`, and `scripts/yolo_preflight.py --enabled --json` returned `dependencies_available=true`, `status=not_configured`, `will_download_models=false`, `will_run_inference=false` because no local model path is configured yet. - Tower runtime `YOLO_CONFIG_DIR` is `/app/storage/ultralytics`; the directory exists, is writable and Ultralytics writes settings there instead of root config. - Internal browser validation passed against `http://192.168.10.150:1202`: the live shell and Map workspace rendered without console errors, with database layer selection, Operational GIS controls, and both full-run modes visible. +- RED: `python -m pytest backend\tests\test_sprint13_yolo_preflight.py -q` failed before runtime diagnostics were implemented because `runtime` was absent from preflight output. +- `python -m pytest backend\tests\test_sprint13_yolo_preflight.py -q` passed: 8 tests. +- `python -m pytest backend\tests\test_sprint13_yolo_preflight.py backend\tests\test_sprint8b_yolo_foundation.py backend\tests\test_docker_runtime_config.py -q` passed: 39 tests. +- `python -m compileall backend/app` passed. +- `cd backend && python -m pytest -q` passed: 369 tests. +- `cd frontend && npm run typecheck` passed. +- `cd frontend && npm run build` passed. +- `bash scripts/run_readiness_check.sh` passed: 369 backend tests plus frontend typecheck/build and Alembic head. +- `cd backend && python -m alembic heads` passed: `202606120900 (head)`. +- `cd backend && python -m alembic upgrade head --sql` passed. +- `bash -n scripts/live_migration_smoke.sh`, `bash -n scripts/deploy_tower.sh`, `bash -n deploy/unraid/run-dockerman-container.sh` and `bash -n deploy/unraid/all-in-one-start.sh` passed. +- Local Codex host still cannot run `docker compose config` because Docker is not installed in this Windows environment; Tower Docker validation is required after push/deploy. Limitations: - `GEOINTEL_INSTALL_AI=true` installs optional PyTorch/Ultralytics dependencies but still requires a user-provided local model file; GeoIntel does not download weights.