File size: 3,760 Bytes
9c84f9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3dfc93b
 
9c84f9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""`llm_completion` μŠ€ν… -- input_fields 둜 μœ μ € λ©”μ‹œμ§€λ₯Ό λ§Œλ“€μ–΄ ν•œ 번 호좜.

`single_step.py`(SingleStepPipeline) μ—μ„œ 둜직 이동 (2026-08-10, λ™μž‘ 동일).
λ²ˆμ—­/μš”μ•½μ²˜λŸΌ "μž…λ ₯ ν•„λ“œλ“€ β†’ μ™„μ„±λ¬Έ ν•˜λ‚˜" ν˜•νƒœμ˜ νƒœμŠ€ν¬κ°€ μ“΄λ‹€.
`pipeline: single_step` 선언은 이 μŠ€ν… ν•˜λ‚˜μ§œλ¦¬ νŒŒμ΄ν”„λΌμΈμœΌλ‘œ ν•΄μ„λœλ‹€.
"""

import logging
from typing import Any

from solar_eval.models.sample import EvalSample
from solar_eval.pipelines.pipeline import BaseStep, StepContext
from solar_eval.pipelines.registry import register_step

logger = logging.getLogger(__name__)


@register_step("llm_completion")
class LLMCompletionStep(BaseStep):
    """Single LLM call step. Used for translation and summarization."""

    uses_model = True

    async def run(self, sample: EvalSample, ctx: StepContext) -> None:
        input_data = self._input_fields_dict(sample.input)

        if ctx.messages:
            # Structured messages mode: substitute {{field}} placeholders
            resolved = self._resolve_messages(ctx.messages, input_data)
        else:
            # Legacy mode: system prompt + auto-built user message
            user_content = self._build_user_message(input_data)
            resolved = [
                {"role": "system", "content": ctx.system_prompt},
                {"role": "user", "content": user_content},
            ]

        result = await ctx.provider.chat_completion(
            messages=resolved,
            model=self.config.get("model", ctx.model),
            temperature=self.config.get("temperature", ctx.temperature),
            max_tokens=self.config.get("max_tokens", ctx.max_tokens),
            reasoning_effort=self.config.get("reasoning_effort", ctx.reasoning_effort),
        )

        ctx.text = result["content"]
        ctx.add_usage(result.get("usage", {}))
        sample.artifacts["think_content"] = result.get("think_content", "")
        sample.artifacts["finish_reason"] = result.get("finish_reason", "stop")

    def _input_fields_dict(self, sample_input: Any) -> dict[str, Any]:
        """`sample.input` 을 {ν•„λ“œλͺ…: κ°’} dict 둜 μ •κ·œν™”ν•œλ‹€.

        λŸ¬λ„ˆλŠ” `task_config`의 `input_fields` μ „μ²΄λ‘œ λ§Œλ“  dict λ₯Ό κ·ΈλŒ€λ‘œ
        `sample.input` 에 λ‹΄μ•„ λ„˜κΈ΄λ‹€(단일 ν•„λ“œλ“  닀쀑 ν•„λ“œλ“ ). μ—¬κΈ°μ„œλŠ”
        κ·Έ κ΄€λ‘€λ₯Ό κ·ΈλŒ€λ‘œ λ°›λ˜, λ‹€λ₯Έ shape 이 λ“€μ–΄μ˜€λ©΄ 쑰용히 λ¬΄μ‹œν•˜μ§€ μ•Šκ³ 
        λ°”λ‘œ μ‹€νŒ¨μ‹œν‚¨λ‹€.
        """
        if sample_input is None:
            return {}
        if isinstance(sample_input, dict):
            return sample_input
        raise TypeError(
            "LLMCompletionStep expects sample.input to be a dict of "
            f"{{field: value}} (runner.py convention); got {type(sample_input)}"
        )

    def _resolve_messages(
        self, messages: list[dict[str, str]], input_data: dict[str, Any]
    ) -> list[dict[str, str]]:
        """Substitute {{field_name}} placeholders in message content."""
        resolved = []
        for msg in messages:
            content = msg["content"]
            for field, value in input_data.items():
                content = content.replace(f"{{{{{field}}}}}", str(value))
            resolved.append({"role": msg["role"], "content": content})
        return resolved

    def _build_user_message(self, input_data: dict[str, Any]) -> str:
        input_fields = self.env.input_fields
        if len(input_fields) == 1:
            return str(input_data.get(input_fields[0], ""))
        # Multiple fields: format as labeled sections
        parts = []
        for field in input_fields:
            value = input_data.get(field, "")
            parts.append(f"{field}:\n{value}")
        return "\n\n".join(parts)