dev-strender's picture
Replace v24-era demo with v34 pipeline demo (engine-vendored bundle)
9c84f9d verified
Raw History Blame
7.05 kB
"""CLI configuration from YAML file and environment variables."""
import logging
import os
from pathlib import Path
from typing import Any
import yaml
DEFAULT_CONFIG_PATH = Path.home() / ".upstage-cloud-dev" / "solar-eval-config.yaml"
logger = logging.getLogger(__name__)
def default_projects_dir() -> Path:
"""CBS 공용 데이터 홈 아래의 프로젝트 루트 (huggingface 캐시와 같은 방식).
- 기본값: ``~/.cache/cbs-data/solar-eval-projects``
- ``CBS_DATA_HOME`` 이 설정되면 데이터 홈 자체를 옮긴다: ``$CBS_DATA_HOME/solar-eval-projects``
- ``XDG_CACHE_HOME`` 이 설정되면 캐시 루트로 존중한다: ``$XDG_CACHE_HOME/cbs-data/solar-eval-projects``
"""
data_home = os.environ.get("CBS_DATA_HOME")
if data_home:
return Path(data_home).expanduser() / "solar-eval-projects"
cache_root = os.environ.get("XDG_CACHE_HOME", "~/.cache")
return Path(cache_root).expanduser() / "cbs-data" / "solar-eval-projects"
def find_repo_root() -> Path | None:
"""이 패키지가 harness 레포 체크아웃 안에 vendored 되어 있으면 레포 루트를 찾는다.
``SOLAR_EVAL_CONFIG_ROOT`` 환경변수가 있으면 그 경로를 쓴다 (테스트/특수 환경용).
레포 밖 standalone 설치면 None — 이때는 데이터 홈 단일 루트로 동작한다.
"""
override = os.environ.get("SOLAR_EVAL_CONFIG_ROOT")
if override:
path = Path(override).expanduser()
return path if path.is_dir() else None
for parent in Path(__file__).resolve().parents:
if (parent / "pnpm-workspace.yaml").is_file():
return parent
return None
def discover_config_dirs(repo_root: Path | None) -> dict[str, Path]:
"""레포의 ``accounts/*/*/03-evaluation/project.yaml`` 에서 프로젝트명 → config 디렉토리 맵.
hydra/DVC 식 분리의 발견 단계: config(project.yaml·prompts·pipelines)는 git 이 정본,
datasets/artifacts 는 데이터 홈이 정본이다.
"""
if repo_root is None:
return {}
found: dict[str, Path] = {}
for manifest in sorted(repo_root.glob("accounts/*/*/03-evaluation/project.yaml")):
try:
with open(manifest) as f:
doc = yaml.safe_load(f)
except Exception as e:
logger.warning("Failed to read %s: %s", manifest, e)
continue
name = doc.get("name") if isinstance(doc, dict) else None
if isinstance(name, str) and name:
found[name] = manifest.parent
return found
def _load_yaml_config(config_path: Path | None = None) -> dict[str, Any]:
"""YAML 설정 파일 로드. 파일 없으면 빈 dict 반환."""
path = config_path or Path(os.environ.get("SOLAR_EVAL_CONFIG", str(DEFAULT_CONFIG_PATH)))
if not path.is_file():
return {}
try:
with open(path) as f:
data = yaml.safe_load(f)
return data if isinstance(data, dict) else {}
except Exception as e:
logger.warning("Failed to load config from %s: %s", path, e)
return {}
class CLIConfig:
"""Configuration for the Solar Eval CLI.
Priority (low → high):
1. Hard-coded defaults (projects_dir: ~/.cache/cbs-data/solar-eval-projects)
2. YAML config file (~/.upstage-cloud-dev/solar-eval-config.yaml)
3. Environment variables (SOLAR_EVAL_URL, CBS_DATA_HOME, SOLAR_EVAL_PROJECTS, etc.)
4. CLI options (--remote, etc.)
"""
def __init__(self, config_path: Path | None = None) -> None:
yaml_cfg = _load_yaml_config(config_path)
yaml_remote = yaml_cfg.get("remote_url") or yaml_cfg.get("remote")
self.remote_url: str | None = os.environ.get("SOLAR_EVAL_URL", yaml_remote)
yaml_timeout = yaml_cfg.get("timeout", 300)
self.timeout = int(os.environ.get("SOLAR_EVAL_TIMEOUT", yaml_timeout))
# SOLAR_EVAL_PROJECTS(구체) > CBS_DATA_HOME(데이터 홈) > YAML projects_dir > 캐시 기본값
env_projects = os.environ.get("SOLAR_EVAL_PROJECTS")
yaml_projects = yaml_cfg.get("projects_dir")
if env_projects:
self.projects_dir = Path(env_projects).expanduser()
explicit_root = True
elif os.environ.get("CBS_DATA_HOME") or not yaml_projects:
self.projects_dir = default_projects_dir()
explicit_root = False
else:
self.projects_dir = Path(yaml_projects).expanduser()
explicit_root = True
# config 오버레이 (hydra/W&B/DVC 패턴): 표준 데이터 홈에서는 레포의
# accounts/*/*/03-evaluation 이 config 정본이다. 명시적으로 지정된 루트
# (SOLAR_EVAL_PROJECTS·YAML projects_dir·--projects-dir)는 과거 아카이브 등
# 자급자족 트리를 가리키는 용도이므로 오버레이하지 않는다.
self.repo_root: Path | None = None if explicit_root else find_repo_root()
self.config_dirs: dict[str, Path] = discover_config_dirs(self.repo_root)
# Load provider API keys from YAML (env vars take precedence)
upstage_cfg = yaml_cfg.get("upstage", {})
if upstage_cfg.get("api_key") and not os.environ.get("UPSTAGE_API_KEY"):
os.environ["UPSTAGE_API_KEY"] = upstage_cfg["api_key"]
openai_cfg = yaml_cfg.get("openai", {})
if openai_cfg.get("api_key") and not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = openai_cfg["api_key"]
@property
def is_remote(self) -> bool:
return self.remote_url is not None
def override_projects_dir(self, path: Path) -> None:
"""``--projects-dir`` 플래그: 지정한 루트를 자급자족 아카이브로 쓴다 (오버레이 해제)."""
self.projects_dir = path
self.repo_root = None
self.config_dirs = {}
def is_repo_managed(self, project: str) -> bool:
"""이 프로젝트의 config 정본이 레포(accounts)에 있는가."""
return project in self.config_dirs
def config_dir(self, project: str) -> Path:
"""config(project.yaml·prompts·pipelines) 루트 — 레포 관리면 레포 쪽, 아니면 데이터 홈."""
return self.config_dirs.get(project) or self.projects_dir / project
def data_dir(self, project: str) -> Path:
"""data(datasets·artifacts) 루트 — 항상 데이터 홈."""
return self.projects_dir / project
def artifacts_dir(self, project: str) -> Path:
"""Run 산출물 디렉토리 — 항상 데이터 홈에 쓴다."""
return self.data_dir(project) / "artifacts"
def prompts_dir(self, project: str) -> Path:
"""프롬프트 디렉토리 — 레포 관리 프로젝트면 git 쪽이 정본."""
return self.config_dir(project) / "prompts"
def pipelines_dir(self, project: str) -> Path:
"""파이프라인 디렉토리 — 레포 관리 프로젝트면 git 쪽이 정본."""
return self.config_dir(project) / "pipelines"
config = CLIConfig()