"""STEP_REGISTRY -- 파이프라인 스텝의 단일 등록소. 새 스텝을 추가하는 방법은 프레임워크 파일 수정이 아니라 **등록**이다: from solar_eval.pipelines.pipeline import BaseStep from solar_eval.pipelines.registry import register_step @register_step("my_step") class MyStep(BaseStep): async def run(self, sample, ctx): ctx.text = do_something(ctx.text) YAML 은 `type: my_step` 으로 그 스텝을 꽂는다. legacy 표기 (`type: rule` + `rule_type: X`)도 같은 레지스트리를 조회한다 -- rule 은 별도 계층이 아니라 이름일 뿐이다. 해석은 전부 **파이프라인 생성 시점**에 일어나므로, 오타 난 이름은 API 호출 없이 즉시 죽는다. """ from pathlib import Path from typing import Any from solar_eval.core.dataset_loader import DatasetLoader from solar_eval.pipelines.base import BasePipeline from solar_eval.pipelines.errors import UnknownRuleTypeError, UnknownStepTypeError from solar_eval.pipelines.pipeline import BaseStep, Pipeline, PipelineEnv STEP_REGISTRY: dict[str, type[BaseStep]] = {} class EmptyStepsError(ValueError): """multi_step 파이프라인인데 `pipeline_config.steps` 가 비어 있을 때. 빈 steps 로 `Pipeline` 을 만들면 실행 루프(`Pipeline.run` 의 for 문)가 그냥 안 돌고 `sample.output = ctx.text` 가 손 안 댄 원문 그대로 남는다 -- `finish_reason` 도 기본값 "stop" 으로 채워져 정상 완료처럼 보인다. 누락된 설정이 "완벽한 보존"으로 채점되는 걸 막는다. 이미 있는 "모르는 스텝 이름은 생성 시점에 즉시 죽는다"는 원칙(`UnknownStepTypeError`, `pipelines/errors.py`)의 연장선이다: steps 가 비어 있다는 것도 "선언한 게 실행되지 않았다"는 같은 사고다. """ def __init__(self) -> None: super().__init__( "multi_step pipeline declared but pipeline_config.steps is empty -- " "a 0-step pipeline would silently pass input through as output unchanged. " "Add at least one step, or use pipeline_type=single_step." ) def register_step(*names: str): """스텝 클래스를 하나 이상의 이름으로 등록하는 데코레이터.""" def decorator(cls: type[BaseStep]) -> type[BaseStep]: for name in names: existing = STEP_REGISTRY.get(name) if existing is not None and existing is not cls: raise ValueError(f"step name {name!r} already registered to {existing.__name__}") STEP_REGISTRY[name] = cls return cls return decorator def available_steps() -> list[str]: _ensure_builtin_steps() return sorted(STEP_REGISTRY) def _ensure_builtin_steps() -> None: # 등록은 import 부수효과다. 순환을 피하려고 여기서 lazy import 한다. from solar_eval.pipelines import steps # noqa: F401 def resolve_step(config: dict[str, Any], env: PipelineEnv) -> BaseStep: """step config 하나를 등록된 스텝 인스턴스로 해석한다. 모르는 이름은 즉시 raise.""" _ensure_builtin_steps() step_type = config.get("type") if step_type == "rule": # legacy 표기: {type: rule, rule_type: X}. 레지스트리에서 X 를 찾는다. key = config.get("rule_type", config.get("name", "")) cls = STEP_REGISTRY.get(key) if cls is None: raise UnknownRuleTypeError(key, available=available_steps()) else: cls = STEP_REGISTRY.get(step_type or "") if cls is None: raise UnknownStepTypeError(str(step_type), available=available_steps()) return cls(config, env) def create_pipeline( pipeline_type: str, input_fields: list[str], pipeline_config: dict[str, Any] | None = None, prompts: dict[str, str] | None = None, dataset_loader: DatasetLoader | None = None, project_dir: Path | str | None = None, config_dir: Path | str | None = None, ) -> BasePipeline: """파이프라인 팩토리. 실행기는 `Pipeline` 하나뿐이다. `pipeline_type` 은 스텝 목록의 출처만 정한다: - `single_step` → 암묵 스텝 1개(`llm_completion`) -- 번역/요약이 쓰는, input_fields 로 유저 메시지를 만들어 한 번 호출하는 스텝. - `multi_step` → `pipeline_config["steps"]` 를 그대로 해석. 둘 다 같은 실행기 위에서 돈다. (예전의 SingleStepPipeline/MultiStepPipeline 클래스 구분은 제거됨 -- 2026-08-10.) `project_dir` 는 데이터 루트 기준 자산(v24 의 `pmi_lookup`)을, `config_dir` 는 레포의 config 정본 기준 자산(치환 사전 등)을 풀 때 쓰인다. """ config = pipeline_config or {} env = PipelineEnv( prompts=prompts or {}, input_fields=input_fields, text_split_size=config.get("text_split_size", 500), text_split_strategy=config.get("text_split_strategy", "dp"), document_chunking=bool(config.get("document_chunking", False)), document_chunk_concurrency=int(config.get("document_chunk_concurrency", 4)), dataset_loader=dataset_loader, project_dir=Path(project_dir) if project_dir else None, config_dir=Path(config_dir) if config_dir else None, ) if pipeline_type == "single_step": step_configs: list[dict[str, Any]] = [{"name": "llm_completion", "type": "llm_completion"}] elif pipeline_type == "multi_step": step_configs = config.get("steps", []) if not step_configs: # steps 키 부재든 steps: [] 든 위에서 이미 [] 로 통일된다 -- 둘 다 같은 결함 # (설정 누락)이라 구분할 필요 없이 여기서 한 번에 잡는다. raise EmptyStepsError() else: raise ValueError(f"Unknown pipeline type: {pipeline_type}") # 생성 시점에 전 스텝을 해석한다 -- 모르는 이름은 여기서 죽는다 (run 전에). steps = [resolve_step(sc, env) for sc in step_configs] return Pipeline(steps, env)