File size: 6,253 Bytes
dd41633
9c84f9d
 
 
 
 
 
 
dd41633
 
 
 
 
 
9c84f9d
 
 
 
 
 
 
 
 
 
8debb9c
dd41633
9c84f9d
 
 
 
 
dd41633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8debb9c
dd41633
 
 
 
9c84f9d
8debb9c
 
 
9c84f9d
dd41633
 
9c84f9d
 
dd41633
9c84f9d
 
 
dd41633
9c84f9d
 
 
dd41633
 
9c84f9d
 
 
 
 
 
dd41633
 
9c84f9d
 
 
dd41633
9c84f9d
dd41633
 
9c84f9d
 
 
 
 
 
 
 
 
 
 
8debb9c
 
 
 
 
 
dd41633
8debb9c
9c84f9d
 
 
 
 
dd41633
9c84f9d
 
 
 
dd41633
9c84f9d
 
 
8debb9c
9c84f9d
 
 
 
 
 
 
dd41633
 
 
 
9c84f9d
 
 
8debb9c
 
 
dd41633
8debb9c
 
dd41633
8debb9c
dd41633
 
8debb9c
dd41633
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""ꡐ열 ꡬ성(νŒŒμ΄ν”„λΌμΈ Γ— ν”„λ‘¬ν”„νŠΈ Γ— λͺ¨λΈ)을 ν…μŠ€νŠΈ 1건에 μ‹€ν–‰ν•˜λŠ” ν”„λ‘œκ·Έλž˜λ§€ν‹± λŸ¬λ„ˆ.

ꡐ열 λ‘œμ§μ„ μž¬κ΅¬ν˜„ν•˜μ§€ μ•Šκ³  solar-eval 엔진을 κ·ΈλŒ€λ‘œ import ν•œλ‹€ β€” 데λͺ¨κ°€
λ³΄μ—¬μ£ΌλŠ” λ™μž‘μ΄ 평가 run κ³Ό λ°”μ΄νŠΈ λ‹¨μœ„λ‘œ κ°™μ•„μ•Ό ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€ (사본 λ‘œμ§μ€
λ°˜λ“œμ‹œ κ°ˆλΌμ§„λ‹€). λ‘œμ»¬μ—μ„œλŠ” uv μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ˜ solar_eval 을, HF Space μ—μ„œλŠ”
`build_space.py` κ°€ λ²€λ”λ§ν•œ 사본을 μ“΄λ‹€ (sys.path 상 슀크립트 μ˜†μ΄ 먼저라
Space μ—μ„œλŠ” 벀더 사본이 이긴닀).

**ꡬ성은 (pipeline, prompt, model) ν•œ λ¬ΆμŒμ΄λ‹€** β€” λͺ¨λΈλ§Œ κ°ˆμ•„λΌμš°λŠ” μ„ νƒμ§€λŠ”
두지 μ•ŠλŠ”λ‹€. mini4 의 이득은 λͺ¨λΈ ꡐ체가 μ•„λ‹ˆλΌ νŒŒμ΄ν”„λΌμΈΒ·ν”„λ‘¬ν”„νŠΈ ꡬ쑰 λ³€κ²½μ—μ„œ
λ‚˜μ™”κ³ (step1 을 μƒμ„±μ—μ„œ νŒμ •μœΌλ‘œ, step2 λ₯Ό μ–΄νœ˜/λ¬Έλ²•μœΌλ‘œ λΆ„ν• , κ·œμΉ™ μΈ΅ 2개 μΆ”κ°€),
μ˜› combo ν”„λ‘¬ν”„νŠΈμ— mini4 만 κ½‚μœΌλ©΄ 2026-09-16 에 이관 λΆˆκ°€ νŒμ •μ„ 받은 κ·Έ 쑰합이
λœλ‹€. κ·Έλž˜μ„œ μ„Έ 좕을 `Config` ν•˜λ‚˜λ‘œ λ¬Άκ³ , 데λͺ¨λŠ” κ·Έ κ΅¬μ„±λ§Œ λ…ΈμΆœν•œλ‹€.

config μžμ‚°(νŒŒμ΄ν”„λΌμΈ yamlΒ·ν”„λ‘¬ν”„νŠΈΒ·μΉ˜ν™˜ μ‚¬μ „Β·ν™”μ΄νŠΈλ¦¬μŠ€νŠΈ)은
  1. `./assets/`          β€” HF Space λ ˆμ΄μ•„μ›ƒ (build_space.py κ°€ 쑰립)
  2. `../03-evaluation/`  β€” 레포 λ ˆμ΄μ•„μ›ƒ (둜컬 개발)
μˆœμ„œλ‘œ μ°ΎλŠ”λ‹€.
"""

from __future__ import annotations

import asyncio
import time
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import yaml


@dataclass(frozen=True)
class Config:
    """데λͺ¨κ°€ κ³ λ₯Ό 수 μžˆλŠ” ν•œ ꡬ성. `base` λŠ” pipeline 이 extends ν•˜λŠ” 파일(λ²ˆλ“€ λŒ€μƒ)."""

    label: str
    pipeline: str
    prompt: str
    model: str
    note: str
    base: str | None = None


#: 데λͺ¨μ— λ…ΈμΆœλ˜λŠ” ꡬ성. μ§€κΈˆμ€ ν•˜λ‚˜λΏμ΄μ§€λ§Œ tuple 둜 λ‘”λ‹€ β€” `build_space.required_assets`
#: κ°€ 이 λͺ©λ‘μ—μ„œ λ²ˆλ“€ μžμ‚°μ„ μœ λ„ν•˜λ―€λ‘œ, ꡬ성이 λŠ˜μ–΄λ„ 쑰립 μ½”λ“œλ₯Ό κ³ μΉ  ν•„μš”κ°€ μ—†λ‹€.
CONFIGS: tuple[Config, ...] = (
    Config(
        label="solar-mini4 νŒŒμ΄ν”„λΌμΈ",
        pipeline="pipeline_dev_v44",  # = v41 + document_chunking
        prompt="prompt_dev_260917_vsplit",
        model="solar-mini4-preview",
        note=(
            "FT ꡐ열 β†’ νŒμ •ν˜• step1(5KB) β†’ λ¬Έλ§₯ ꡐ정 μ–΄νœ˜/문법 2μŠ€ν… β†’ κ²°μ •λ‘  κ·œμΉ™ 6μΈ΅. "
            "μž…λ ₯ 3.3k 토큰."
        ),
        base="pipeline_dev_v40",
    ),
)

#: 라벨 β†’ Config
BY_LABEL: dict[str, Config] = {c.label: c for c in CONFIGS}
DEFAULT_LABEL = CONFIGS[0].label

# μ—”μ§„μ˜ StepCallback κ³Ό 같은 λͺ¨μ–‘: (event, index, total, step_name, doc_chunk)
StepCallback = Callable[[str, int, int, str, tuple[int, int] | None], None]


def find_config_dir(pipeline: str) -> Path:
    """κ·Έ νŒŒμ΄ν”„λΌμΈ yaml 을 μ‹€μ œλ‘œ κ°€μ§„ μžμ‚° 루트. Space λ²ˆλ“€μ΄ λ ˆν¬λ³΄λ‹€ λ¨Όμ €λ‹€."""
    here = Path(__file__).resolve().parent
    for cand in (here / "assets", here.parent / "03-evaluation"):
        if (cand / "pipelines" / f"{pipeline}.yaml").is_file():
            return cand
    raise FileNotFoundError(
        f"config μžμ‚°μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ β€” {here}/assets λ˜λŠ” ../03-evaluation 에 "
        f"pipelines/{pipeline}.yaml 이 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€ (build_space.py μ°Έμ‘°)."
    )


def build_pipeline(label: str = DEFAULT_LABEL) -> Any:
    """ꡬ성 ν•˜λ‚˜μ˜ νŒŒμ΄ν”„λΌμΈ μΈμŠ€ν„΄μŠ€. μ•±μ—μ„œ κ΅¬μ„±λ³„λ‘œ 1회 λ§Œλ“€μ–΄ μž¬μ‚¬μš©ν•œλ‹€."""
    import solar_eval.pipelines.steps  # noqa: F401 β€” STEP_REGISTRY 등둝
    from solar_eval.core.dataset_loader import DatasetLoader
    from solar_eval.core.pipeline_compose import compose_pipeline
    from solar_eval.models.prompt_version import load_step_prompts
    from solar_eval.pipelines.registry import create_pipeline

    cfg = BY_LABEL[label]
    config_dir = find_config_dir(cfg.pipeline)
    pipelines_dir = config_dir / "pipelines"

    def load_base(name: str) -> dict[str, Any]:
        return yaml.safe_load((pipelines_dir / f"{name}.yaml").read_text(encoding="utf-8"))

    composed = compose_pipeline(load_base(cfg.pipeline), load_base=load_base)
    prompts = load_step_prompts(config_dir / "prompts" / cfg.prompt)

    return create_pipeline(
        "multi_step",
        input_fields=["original"],
        pipeline_config=composed,
        prompts=prompts,
        dataset_loader=DatasetLoader(),
        config_dir=config_dir,
    )


def step_names(pipeline: Any) -> list[str]:
    """UI κ°€ μ§„ν–‰ ν‘œμ‹œλ₯Ό 미리 그릴 수 있게 μŠ€ν… 이름을 μˆœμ„œλŒ€λ‘œ."""
    return [step.name for step in pipeline.steps]


async def _run_async(
    pipeline: Any, text: str, cfg: Config, on_step: StepCallback | None
) -> dict[str, Any]:
    from solar_eval.models.sample import EvalSample
    from solar_eval.providers.upstage import UpstageProvider

    sample = EvalSample(input={"original": text})
    start = time.monotonic()
    # 평가 run κ³Ό 같은 쑰건: temp 0.0, reasoning off (60초 μ§€μ—° μ˜ˆμ‚° μ œμ•½)
    result = await pipeline.run(
        sample,
        prompts="",
        provider=UpstageProvider(),
        model=cfg.model,
        temperature=0.0,
        max_tokens=8000,
        reasoning_effort=None,
        on_step=on_step,
    )
    elapsed = time.monotonic() - start
    return {
        "output": result.output,
        "step_outputs": result.artifacts.get("step_outputs", {}),
        "usage": result.artifacts.get("usage", {}),
        "elapsed_s": elapsed,
        "pipeline_key": cfg.pipeline,
        "prompt_key": cfg.prompt,
        "model": cfg.model,
        "config_label": cfg.label,
    }


def run_proofread(
    pipeline: Any,
    text: str,
    label: str = DEFAULT_LABEL,
    on_step: StepCallback | None = None,
) -> dict[str, Any]:
    """ν…μŠ€νŠΈ 1건 ꡐ열. {output, step_outputs, usage, elapsed_s, pipeline_key, prompt_key, model, config_label}.

    `pipeline` 은 같은 `label` 둜 `build_pipeline` ν•œ μΈμŠ€ν„΄μŠ€μ—¬μ•Ό ν•œλ‹€ β€” 앱이
    κ΅¬μ„±λ³„λ‘œ μΊμ‹œν•œλ‹€. `on_step` 은 μŠ€ν… μ‹œμž‘/μ’…λ£Œμ™€ chunk μ§„ν–‰λ§ˆλ‹€ λΆˆλ¦°λ‹€(UI μ§„ν–‰ ν‘œμ‹œ).
    """
    return asyncio.run(_run_async(pipeline, text, BY_LABEL[label], on_step))