OpenSpace/openspace/config/constants.py
Ayush7614 a7bfd6cc26 fix: scan dashboard workflows from data-home logs too
Installed dashboards could not see recordings under site-packages logs.
Include get_data_home()/logs paths and stop using PROJECT_ROOT as the
default cloud reporter workspace root after pip install.
2026-07-29 02:55:43 +05:30

146 lines
4.7 KiB
Python

"""Shared path and config-name constants for OpenSpace.
``PROJECT_ROOT`` is the directory that *contains* the ``openspace`` package.
In a source checkout that is the repository root; after a normal ``pip install``
it resolves to ``site-packages``. Mutable runtime state must never be written
there — use :func:`get_data_home` / :func:`get_default_db_path` instead.
"""
from __future__ import annotations
import os
from pathlib import Path
CONFIG_GROUNDING = "config_grounding.json"
CONFIG_SECURITY = "config_security.json"
CONFIG_MCP = "config_mcp.json"
CONFIG_DEV = "config_dev.json"
CONFIG_AGENTS = "config_agents.json"
LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
# Directory that contains the ``openspace`` package (repo root or site-packages).
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
# Package directory itself (``.../openspace``).
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
_DATA_HOME_ENV = "OPENSPACE_HOME"
_DATA_DIR_ENV = "OPENSPACE_DATA_DIR"
def is_source_checkout(root: Path | None = None) -> bool:
"""Return True when *root* looks like a git/source checkout of OpenSpace.
Editable installs keep writing under the repo ``.openspace/`` directory.
Wheel/site-package installs do not — those must use a user data home.
"""
candidate = Path(root) if root is not None else PROJECT_ROOT
return (candidate / "pyproject.toml").is_file() and (
candidate / "openspace" / "__init__.py"
).is_file()
def get_data_home(*, create: bool = False) -> Path:
"""Resolve the writable OpenSpace data directory.
Resolution order:
1. ``OPENSPACE_HOME`` — explicit data home override
2. ``OPENSPACE_DATA_DIR`` — alias for the same override
3. ``<repo>/.openspace`` when running from a source/editable checkout
4. ``~/.openspace`` for installed (site-packages) environments
The returned path is the data home itself (already named ``.openspace`` or
an explicit override). Callers should put databases/caches directly under it
(for example ``get_data_home() / "openspace.db"``).
"""
for env_name in (_DATA_HOME_ENV, _DATA_DIR_ENV):
raw = os.environ.get(env_name, "").strip()
if raw:
path = Path(raw).expanduser().resolve()
if create:
path.mkdir(parents=True, exist_ok=True)
return path
if is_source_checkout():
path = (PROJECT_ROOT / ".openspace").resolve()
else:
path = (Path.home() / ".openspace").resolve()
if create:
path.mkdir(parents=True, exist_ok=True)
return path
def get_default_db_path(*, create: bool = True) -> Path:
"""Default shared SQLite path (``openspace.db`` under the data home)."""
db_dir = get_data_home(create=create)
return db_dir / "openspace.db"
def get_cache_dir(name: str, *, create: bool = False) -> Path:
"""Return a named cache directory under the data home."""
path = get_data_home(create=create) / name
if create:
path.mkdir(parents=True, exist_ok=True)
return path
def get_log_dir(*, create: bool = True) -> Path:
"""Writable log directory (never under site-packages)."""
path = get_data_home(create=create) / "logs"
if create:
path.mkdir(parents=True, exist_ok=True)
return path
def get_workflow_roots() -> list[Path]:
"""Directories the dashboard should scan for workflow/run artifacts.
Always includes writable data-home log paths. When running from a source
checkout, also includes the historical repo-relative roots so local
development keeps finding existing recordings and benchmark runs.
"""
data_logs = get_log_dir(create=False)
roots: list[Path] = [
data_logs / "recordings",
data_logs / "trajectories",
]
if is_source_checkout():
roots.extend(
[
PROJECT_ROOT / "logs" / "recordings",
PROJECT_ROOT / "logs" / "trajectories",
PROJECT_ROOT / "benchmarks" / "gdpval" / "results",
PROJECT_ROOT / "benchmarks" / "terminal_bench" / "runs",
]
)
# Preserve order while dropping duplicates (same path via symlink/resolve).
seen: set[Path] = set()
unique: list[Path] = []
for root in roots:
key = root.resolve() if root.exists() else root
if key in seen:
continue
seen.add(key)
unique.append(root)
return unique
__all__ = [
"CONFIG_GROUNDING",
"CONFIG_SECURITY",
"CONFIG_MCP",
"CONFIG_DEV",
"CONFIG_AGENTS",
"LOG_LEVELS",
"PROJECT_ROOT",
"PACKAGE_ROOT",
"is_source_checkout",
"get_data_home",
"get_default_db_path",
"get_cache_dir",
"get_log_dir",
"get_workflow_roots",
]