proofread-demo / solar_eval /core /pipeline_compose.py
dev-strender's picture
Replace v24-era demo with v34 pipeline demo (engine-vendored bundle)
9c84f9d verified
Raw History Blame Contribute Delete
8.81 kB
"""νŒŒμ΄ν”„λΌμΈ 쑰립 β€” `extends` 상속 + dot-path override.
μ™œ: ν•œ μΆ•λ§Œ λ°”κΎΌ μ‹€ν—˜μ΄ νŒŒμ΄ν”„λΌμΈ 파일 **전체 사본**으둜 μŒ“μ—¬ μ™”λ‹€.
`pipeline_dev_v24a/b/c` λŠ” 109μ€„μ§œλ¦¬ v24 사본인데 μ‹€μ œ μ°¨μ΄λŠ”
repeat_temperatures: [0.0, 0.2] β†’ [0.0, 0.0] / [0.0, 0.3] / [0.0, 0.5]
ν•œ 쀄뿐이닀. μ‚¬λ³ΈμœΌλ‘œ 두면 (1) μ‹€ν—˜μ˜ μ‹€μ œ λ³€μˆ˜κ°€ diff λ₯Ό λ– μ•Όλ§Œ 보이고,
(2) base λ₯Ό κ³ μΉ  λ•Œ 사본듀이 쑰용히 κ°ˆλΌμ§„λ‹€. μ‹€ν—˜μ˜ μ •μ˜λŠ” 사본이 μ•„λ‹ˆλΌ
**diff** μ—¬μ•Ό ν•œλ‹€.
두 κ°€μ§€ ν‘œν˜„μ„ μ§€μ›ν•œλ‹€. λ‘˜ λ‹€ 같은 dot-path 문법을 μ“΄λ‹€:
# 파일둜 λ‚¨κΈ°λŠ” μ‹€ν—˜ (μž¬μ‚¬μš©Β·μΈμš© λŒ€μƒ)
name: pipeline_dev_v24a
extends: pipeline_dev_v24
overrides:
steps.basic_correction.repeat_temperatures: [0.0, 0.0]
# 파일 없이 ν•œ 번 λŒλ €λ³΄λŠ” μ‹€ν—˜
solar-eval runs start ... -P pipeline_dev_v24 \\
--set steps.basic_correction.repeat_temperatures='[0.0, 0.5]'
쑰립은 **λ‘œλ“œ μ‹œμ μ— λλ‚œλ‹€** β€” μ‹€ν–‰ μ—”μ§„(`STEP_REGISTRY`)은 `extends` λ₯Ό
μ•„μ˜ˆ λͺ¨λ₯΄κ³ , μ™„μ„±λœ steps λͺ©λ‘λ§Œ λ°›λŠ”λ‹€. κ·Έλž˜μ„œ 이 κΈ°λŠ₯은 μ—”μ§„μ˜ μΌλ°˜μ„±μ—
영ν–₯을 μ£Όμ§€ μ•ŠλŠ”λ‹€(λ²ˆμ—­Β·μš”μ•½ λ“± λ‹€λ₯Έ ν”„λ‘œμ νŠΈλ„ κ·ΈλŒ€λ‘œ μ“Έ 수 μžˆλ‹€).
상속은 **1λ‹¨κ³„λ‘œ μ œν•œ**ν•œλ‹€. base κ°€ 또 base λ₯Ό κ°–λŠ” 체인은 hydra κ³„μ—΄μ—μ„œ
반볡적으둜 λ¬Έμ œκ°€ 된 지점이고("이 값이 μ–΄λ””μ„œ μ™”λŠ”κ°€"λ₯Ό 좔적 λΆˆκ°€), PoC 규λͺ¨μ—μ„œ
μ–»λŠ” 게 μ—†λ‹€.
"""
import copy
import logging
from collections.abc import Callable, Mapping
from typing import Any
import yaml
logger = logging.getLogger(__name__)
#: dot-path μ—μ„œ μŠ€ν… λͺ©λ‘μ„ κ°€λ¦¬ν‚€λŠ” μ΅œμƒμœ„ ν‚€.
STEPS_KEY = "steps"
class PipelineCompositionError(ValueError):
"""`extends` ν•΄μ„μ΄λ‚˜ override 적용이 μ‹€νŒ¨ν–ˆμ„ λ•Œ.
쑰용히 λ„˜μ–΄κ°€μ§€ μ•ŠλŠ”λ‹€ β€” μ˜€νƒ€ λ‚œ override λŠ” "아무 효과 μ—†λŠ” μ‹€ν—˜"을 λ§Œλ“€κ³ ,
그건 잘λͺ»λœ 결둠으둜 λ°”λ‘œ 이어진닀 (이 엔진이 κ³Όκ±° μ‘°μš©ν•œ no-op μŠ€ν…μœΌλ‘œ
같은 λ°©μ‹μœΌλ‘œ λ‹€μ³€λ‹€: `pipelines/errors.py` μ°Έκ³ ).
"""
def compose_pipeline(
raw: Mapping[str, Any],
*,
load_base: Callable[[str], Mapping[str, Any]] | None = None,
overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""νŒŒμ΄ν”„λΌμΈ YAML ν•œ μž₯을 μ‹€ν–‰ κ°€λŠ₯ν•œ μ„€μ •μœΌλ‘œ μ‘°λ¦½ν•œλ‹€.
Args:
raw: νŒŒμ΄ν”„λΌμΈ YAML 을 νŒŒμ‹±ν•œ dict.
load_base: `extends` λŒ€μƒ 이름을 λ°›μ•„ κ·Έ YAML dict λ₯Ό λŒλ €μ£ΌλŠ” ν•¨μˆ˜.
`extends` λ₯Ό μ“΄ νŒŒμ΄ν”„λΌμΈμ„ μ‘°λ¦½ν•˜λ €λ©΄ λ°˜λ“œμ‹œ ν•„μš”ν•˜λ‹€.
overrides: 파일 λ°”κΉ₯μ—μ„œ λ“€μ–΄μ˜¨ override (CLI `--set`). YAML 의
`overrides` 보닀 **λ‚˜μ€‘μ—** μ μš©λœλ‹€.
Returns:
`extends`/`overrides` ν‚€κ°€ 걷힌, 해석이 λλ‚œ μ„€μ • dict.
Raises:
PipelineCompositionError: base λ₯Ό λͺ» μ°Ύκ±°λ‚˜, 상속이 2단계 μ΄μƒμ΄κ±°λ‚˜,
override κ²½λ‘œκ°€ μ‹€μž¬ν•˜μ§€ μ•ŠλŠ” μŠ€ν…μ„ 가리킬 λ•Œ.
"""
base_name = raw.get("extends")
if base_name is not None and not isinstance(base_name, str):
raise PipelineCompositionError(f"`extends` must be a pipeline name, got {base_name!r}")
if base_name:
if load_base is None:
raise PipelineCompositionError(
f"Pipeline extends {base_name!r} but no loader was provided"
)
base = dict(load_base(base_name))
if base.get("extends"):
raise PipelineCompositionError(
f"Multi-level inheritance is not supported: {base_name!r} itself extends "
f"{base['extends']!r}. Flatten it or extend the root pipeline directly."
)
composed = _merge_base(base, raw)
else:
composed = copy.deepcopy({k: v for k, v in raw.items() if k != "extends"})
file_overrides = composed.pop("overrides", None) or raw.get("overrides") or {}
_apply_overrides(composed, file_overrides, origin="pipeline `overrides`")
_apply_overrides(composed, overrides or {}, origin="--set")
return composed
def _merge_base(base: dict[str, Any], child: Mapping[str, Any]) -> dict[str, Any]:
"""base μœ„μ— child 의 μ΅œμƒμœ„ ν‚€λ₯Ό μ–ΉλŠ”λ‹€.
`steps` λŠ” child κ°€ ν†΅μ§Έλ‘œ μ„ μ–Έν–ˆμ„ λ•Œλ§Œ κ΅μ²΄ν•œλ‹€ β€” λΆ€λΆ„ 병합은 override 의
일이고, μ—¬κΈ°μ„œκΉŒμ§€ ν•˜λ©΄ "steps κ°€ μ–΄λ””μ„œ μ™”λŠ”κ°€"κ°€ 흐렀진닀.
**κΉŠμ€ 볡사가 ν•„μˆ˜λ‹€**: μ–•κ²Œ λ³΅μ‚¬ν•˜λ©΄ μžμ‹μ˜ override κ°€ base 의 step dict λ₯Ό
μ œμžλ¦¬μ—μ„œ 고쳐, 같은 base λ₯Ό μ“°λŠ” λ‹€λ₯Έ μžμ‹κΉŒμ§€ μ˜€μ—Όλœλ‹€.
"""
merged = copy.deepcopy(base)
for key, value in child.items():
if key in ("extends", "overrides"):
continue
merged[key] = copy.deepcopy(value)
return merged
def _apply_overrides(config: dict[str, Any], overrides: Mapping[str, Any], *, origin: str) -> None:
"""dot-path override λ₯Ό μ œμžλ¦¬μ— μ μš©ν•œλ‹€."""
if not isinstance(overrides, Mapping):
raise PipelineCompositionError(
f"{origin} must be a mapping of dot-path β†’ value, got {type(overrides).__name__}"
)
for path, value in overrides.items():
try:
_set_path(config, str(path), value)
except PipelineCompositionError as e:
raise PipelineCompositionError(f"{origin}: {e}") from None
def _set_path(config: dict[str, Any], path: str, value: Any) -> None:
"""`steps.<step>.<field>...` λ˜λŠ” `<field>...` κ²½λ‘œμ— 값을 μ“΄λ‹€."""
parts = [p for p in path.split(".") if p]
if not parts:
raise PipelineCompositionError("empty override path")
if parts[0] == STEPS_KEY:
if len(parts) < 3:
raise PipelineCompositionError(
f"{path!r} β€” step override needs at least "
f"'{STEPS_KEY}.<step_name>.<field>' (e.g. "
f"'{STEPS_KEY}.basic_correction.reasoning_effort')"
)
target = _find_step(config, parts[1], path)
_assign(target, parts[2:], value, path)
return
_assign(config, parts, value, path)
def _find_step(config: Mapping[str, Any], step_name: str, path: str) -> dict[str, Any]:
"""μ΄λ¦„μœΌλ‘œ μŠ€ν…μ„ μ°ΎλŠ”λ‹€. μ—†μœΌλ©΄ 후보λ₯Ό μ‹€μ–΄ raise (μ˜€νƒ€λ₯Ό 쑰용히 λ„˜κΈ°μ§€ μ•ŠλŠ”λ‹€)."""
steps = config.get(STEPS_KEY)
if not isinstance(steps, list):
raise PipelineCompositionError(f"{path!r} β€” pipeline has no `{STEPS_KEY}` list")
matches = [s for s in steps if isinstance(s, Mapping) and s.get("name") == step_name]
if not matches:
names = [str(s.get("name")) for s in steps if isinstance(s, Mapping)]
raise PipelineCompositionError(
f"{path!r} β€” no step named {step_name!r} (steps: {', '.join(names)})"
)
if len(matches) > 1:
raise PipelineCompositionError(
f"{path!r} β€” {step_name!r} matches {len(matches)} steps; step names must be unique "
f"to be addressable by override"
)
return matches[0] # type: ignore[return-value]
def _assign(target: dict[str, Any], parts: list[str], value: Any, path: str) -> None:
"""쀑첩 dict λ₯Ό 따라 λ‚΄λ €κ°€λ©° λ§ˆμ§€λ§‰ 킀에 값을 μ“΄λ‹€. 쀑간 dict λŠ” λ§Œλ“€μ–΄ μ€€λ‹€."""
cursor: Any = target
for key in parts[:-1]:
nxt = cursor.get(key)
if nxt is None:
nxt = {}
cursor[key] = nxt
elif not isinstance(nxt, dict):
raise PipelineCompositionError(
f"{path!r} β€” {key!r} holds {type(nxt).__name__}, cannot descend into it"
)
cursor = nxt
cursor[parts[-1]] = value
def parse_set_option(raw_values: tuple[str, ...] | list[str]) -> dict[str, Any]:
"""CLI `--set key=value` λͺ©λ‘μ„ override 맡으둜 λ°”κΎΌλ‹€.
값은 YAML 둜 νŒŒμ‹±ν•΄ νƒ€μž…μ„ μ‚΄λ¦°λ‹€ β€” `high` λŠ” λ¬Έμžμ—΄, `2` λŠ” μ •μˆ˜,
`[0.0, 0.5]` λŠ” λ¦¬μŠ€νŠΈκ°€ λœλ‹€. νŒŒμ‹±μ— μ‹€νŒ¨ν•˜λ©΄ λ¬Έμžμ—΄ κ·ΈλŒ€λ‘œ λ‘”λ‹€.
Raises:
PipelineCompositionError: `=` κ°€ μ—†λŠ” ν•­λͺ©μ΄ μžˆμ„ λ•Œ.
"""
overrides: dict[str, Any] = {}
for item in raw_values:
if "=" not in item:
raise PipelineCompositionError(
f"--set expects 'path=value', got {item!r} "
f"(e.g. --set steps.basic_correction.reasoning_effort=high)"
)
key, _, raw_value = item.partition("=")
key = key.strip()
if not key:
raise PipelineCompositionError(f"--set has an empty path: {item!r}")
try:
overrides[key] = yaml.safe_load(raw_value)
except yaml.YAMLError:
overrides[key] = raw_value
return overrides