dev-strender's picture
Replace v24-era demo with v34 pipeline demo (engine-vendored bundle)
9c84f9d verified
Raw History Blame
19.5 kB
import asyncio
import logging
import sys
from datetime import datetime, timezone
from typing import Any, Callable
# Ensure runner logs are visible in CLI
logging.basicConfig(stream=sys.stderr, level=logging.INFO, format="%(message)s")
from solar_eval.core.dataset_loader import DatasetLoader
from solar_eval.evaluators.registry import create_evaluator
from solar_eval.models.enums import RunStatus
from solar_eval.models.sample import EvalSample
from solar_eval.pipelines.registry import create_pipeline
from solar_eval.providers.base import BaseProvider
from solar_eval.stores.base import ResultStore
logger = logging.getLogger(__name__)
OnProgress = Callable[[dict[str, Any]], Any]
def build_eval_sample_from_result(result: dict[str, Any], reference: Any) -> EvalSample:
"""์ €์žฅ๋œ ๊ฒฐ๊ณผ dict(๋ ˆ๊ฑฐ์‹œ 8+trace ํ‚ค)์—์„œ ์ฑ„์ ์šฉ `EvalSample` ์„ ๋˜์‚ด๋ฆฐ๋‹ค.
`insert_run_result` ๊ฐ€ ์“ฐ๋Š” ํ‚ค ์ด๋ฆ„(`golden`/`trace`)์€ `EvalSample` ํ•„๋“œ ์ด๋ฆ„
(`reference`/`artifacts`)๊ณผ ๋‹ค๋ฅด๋‹ค -- eval-store ํ•˜์œ„ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ €์žฅ ์Šคํ‚ค๋งˆ๋Š”
๊ทธ๋Œ€๋กœ ๋‘๊ณ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš ยง9-1) ์ฑ„์  ์ง์ „์—๋งŒ ์—ฌ๊ธฐ์„œ ๋˜๋Œ๋ฆฐ๋‹ค. `execute_run`
๋‚ด๋ถ€ ๋ฃจํ”„์™€ `runs eval` CLI(์žฌ์ฑ„์ , ์ƒˆ ์ถ”๋ก  ์—†์ด ๋””์Šคํฌ์˜ `results.jsonl` ์„
๊ทธ๋Œ€๋กœ ์”€) ์–‘์ชฝ์—์„œ ๊ณต์œ ํ•œ๋‹ค.
Args:
result: `insert_run_result` ์— ๋„˜๊ธด ๊ฒƒ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ(๋˜๋Š” `JsonlStore.
load_existing_results()` ๋กœ ๋””์Šคํฌ์—์„œ ๋‹ค์‹œ ์ฝ์€ ๊ฒƒ). `input`/`output`/
`trace` ํ‚ค๋ฅผ ์ฝ๋Š”๋‹ค.
reference: ์ •๋‹ต(golden) ๊ฐ’. ํ˜ธ์ถœ์ž๊ฐ€ ์ง์ ‘ ๋„˜๊ธด๋‹ค -- ๋‘ ํ˜ธ์ถœ์ž์˜ golden
์ถ”์ถœ ๊ฒฝ๋กœ๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(์•„๋ž˜ ์ฐธ๊ณ ). ์ด ํ•จ์ˆ˜๋Š” ์–ด๋А ์ชฝ์ธ์ง€ ๋ชจ๋ฅธ ์ฑ„
๊ฐ’๋งŒ ๋ฐ›์•„ ๊ทธ๋Œ€๋กœ `sample.reference` ์— ์ฑ„์šด๋‹ค.
- `execute_run` ๋‚ด๋ถ€ ๋ฃจํ”„: `_golden_raw`(๋””์Šคํฌ์— ์•ˆ ๋‚จ๋Š” ์‹คํ–‰ ์ค‘
์ž„์‹œ ํ‚ค, resume ์‹œ์—๋„ ๋งค ์ƒ˜ํ”Œ๋งˆ๋‹ค ์ƒˆ๋กœ ๊ณ„์‚ฐ๋จ)๋ฅผ ๋„˜๊ธด๋‹ค.
- `runs eval` CLI: ๋””์Šคํฌ์— ์ €์žฅ๋œ `golden` ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค
(`_golden_raw` ๋Š” ์• ์ดˆ์— ์ €์žฅ๋˜์ง€ ์•Š์•„ CLI ์ชฝ์—” ์—†๋‹ค).
Returns:
`input`/`output`/`reference`/`artifacts` ๊ฐ€ ์ฑ„์›Œ์ง„ `EvalSample`.
`contexts` ๋Š” ์•„์ง ์•„๋ฌด๋„ ์•ˆ ์จ์„œ ๊ธฐ๋ณธ๊ฐ’(`None`) ๊ทธ๋Œ€๋กœ๋‹ค.
"""
trace = result.get("trace") or {}
return EvalSample(
input=result.get("input"),
output=result.get("output"),
reference=reference,
artifacts=trace,
)
class BatchRunner:
"""Runs inference + evaluation batches with progress tracking."""
def __init__(
self,
store: ResultStore,
inference_provider: BaseProvider,
judge_provider: BaseProvider | None = None,
dataset_loader: DatasetLoader | None = None,
) -> None:
self.store = store
self.inference_provider = inference_provider
self.judge_provider = judge_provider
self.dataset_loader = dataset_loader or DatasetLoader()
async def execute_run(
self,
run_id: str,
project_config: dict[str, Any],
task_config: dict[str, Any],
prompt: dict[str, Any],
on_progress: OnProgress | None = None,
max_workers: int = 5,
limit: int | None = None,
completed_results: list[dict[str, Any]] | None = None,
) -> None:
"""Execute a full inference + evaluation run.
Args:
completed_results: Previously completed results for resume.
Samples with matching sample_idx will be skipped.
์ƒ˜ํ”Œ ๋‹จ์œ„ ์‹คํŒจ(์ถ”๋ก /ํ‰๊ฐ€)๋Š” ์‚ผํ‚ค๊ณ  ์ƒํƒœ(PARTIAL/eval_failed_count)๋กœ ๊ธฐ๋กํ•˜์ง€๋งŒ,
run ์„ ํ†ต์งธ๋กœ ๋ชป ๋Œ๊ฒŒ ๋งŒ๋“œ๋Š” ์˜ˆ์™ธ(๋ฐ์ดํ„ฐ์…‹ ๋กœ๋“œ ์‹คํŒจ, ํŒŒ์ดํ”„๋ผ์ธ/ํ‰๊ฐ€๊ธฐ ์ƒ์„ฑ
์‹คํŒจ, ์Šคํ† ์–ด ์“ฐ๊ธฐ ์‹คํŒจ ๋“ฑ)๋Š” ์ƒํƒœ๋ฅผ FAILED ๋กœ ๋‚จ๊ธด ๋’ค ๊ทธ๋Œ€๋กœ ์žฌ์ „ํŒŒํ•œ๋‹ค --
"๋ฌด์Šจ ์ผ์ด ์žˆ์–ด๋„ ์˜ˆ์™ธ ์—†์ด ๋ฐ˜ํ™˜"์ด ์•„๋‹ˆ๋‹ค. ํ˜ธ์ถœ์ž๋Š” ์ด ํ•จ์ˆ˜๊ฐ€ raise ํ•  ์ˆ˜
์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด์•ผ ํ•œ๋‹ค.
"""
try:
# Build set of already-completed sample indices for resume
completed_by_idx: dict[int, dict[str, Any]] = {}
if completed_results:
for r in completed_results:
completed_by_idx[r["sample_idx"]] = r
# Update status to running
await self.store.update_run(
run_id,
{
"status": RunStatus.RUNNING,
"started_at": datetime.now(timezone.utc),
},
)
# Load dataset
dataset_config = project_config.get("dataset", {})
source = dataset_config.get("source", "huggingface")
repo_name = dataset_config.get("repo", "")
dataset_path = task_config.get("dataset_path", "")
data = self.dataset_loader.load_jsonl(repo_name, dataset_path, source=source)
if limit and limit < len(data):
data = data[:limit]
remaining = len(data) - len(completed_by_idx)
await self.store.update_run(run_id, {"total_samples": len(data)})
if on_progress:
await on_progress({"type": "dataset_loaded", "total": len(data)})
if completed_by_idx:
logger.info(f"Resuming: {len(completed_by_idx)} done, {remaining} remaining")
# Create pipeline
# project_dir: v24 ์˜ tool_calling_judge ๊ฐ€ pmi_lookup ์ƒ๋Œ€๊ฒฝ๋กœ๋ฅผ ํ’€ ๋•Œ๋งŒ
# ์“ด๋‹ค (ยง5-F). dataset_loader.base_dir ์ด projects ๋ฃจํŠธ์ด๋ฏ€๋กœ project ์ด๋ฆ„์„
# ๋ถ™์ด๋ฉด project_dir ์ด ๋œ๋‹ค -- CLI(`_start_local`)๊ฐ€ ์ด๋ฏธ ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ด€๋ก€.
project_name = project_config.get("name")
project_dir = (
self.dataset_loader.base_dir / project_name
if self.dataset_loader.base_dir and project_name
else None
)
# config_dir: ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด project_loader ๊ฐ€ ์ฑ„์›Œ ๋‘” config ์ •๋ณธ
# ๊ฒฝ๋กœ(03-evaluation). ์น˜ํ™˜ ์‚ฌ์ „ ๊ฐ™์€ ์ง€์‹ ์ž์‚ฐ์ด ์—ฌ๊ธฐ์„œ ์˜จ๋‹ค.
pipeline = create_pipeline(
pipeline_type=task_config.get("pipeline", "single_step"),
input_fields=task_config.get("input_fields", []),
pipeline_config=task_config.get("pipeline_config"),
prompts=prompt.get("step_prompts", {}),
dataset_loader=self.dataset_loader,
project_dir=project_dir,
config_dir=project_config.get("config_dir"),
)
# Run inference with concurrency control
semaphore = asyncio.Semaphore(max_workers)
completed = len(completed_by_idx)
async def process_sample(idx: int, sample: dict) -> dict[str, Any]:
nonlocal completed
# Skip already-completed samples (resume)
if idx in completed_by_idx:
existing = completed_by_idx[idx]
golden_field = task_config.get("golden_field")
golden = sample.get(golden_field, "") if golden_field else ""
return {**existing, "_golden_raw": golden}
async with semaphore:
input_data = {
field: sample.get(field, "")
for field in task_config.get("input_fields", [])
}
# Get golden reference
golden_field = task_config.get("golden_field")
golden_fields = task_config.get("golden_fields")
if golden_field:
golden = sample.get(golden_field, "")
elif golden_fields:
golden = {k: sample.get(v, "") for k, v in golden_fields.items()}
else:
golden = ""
eval_sample = EvalSample(input=input_data, reference=golden)
try:
eval_sample = await pipeline.run(
sample=eval_sample,
prompts=prompt.get("system_prompt", ""),
provider=self.inference_provider,
model=prompt.get("model", "solar-pro2"),
temperature=prompt.get("temperature", 0.0),
max_tokens=prompt.get("max_tokens", 8000),
reasoning_effort=prompt.get("reasoning_effort"),
messages=prompt.get("messages"),
)
except Exception as e:
ts = datetime.now().strftime("%H:%M:%S")
logger.warning(f"[{ts}] Sample {idx} failed: {type(e).__name__}: {e}")
raise
# ์ €์žฅ ํ˜•ํƒœ๋Š” ์ด์ „๊ณผ ๊ฐ™์€ 8ํ‚ค dict (eval-store ์˜ resultRowSchema ๊ฐ€
# ์ฝ๋Š” ์ด๋ฆ„๋“ค๊ณผ ํ•˜์œ„ ํ˜ธํ™˜) + trace ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. trace ๋Š” artifacts
# ์ „์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค -- step_outputs ๋ฟ ์•„๋‹ˆ๋ผ v24 ๊ฐ€ ์ฑ„์šฐ๋Š”
# judge_decisions/judge_tool_calls/self_consistency_runs/corrections
# ๋„ ์—ฌ๊ธฐ ์•ˆ ๋„ฃ์œผ๋ฉด ์ €์žฅ ์ง์ „์— ํ†ต์งธ๋กœ ๋ฒ„๋ ค์ง„๋‹ค (์‹ค์ œ judge LLM ํ˜ธ์ถœยท
# self-consistency ๋ฐ˜๋ณต ํ˜ธ์ถœ ๋น„์šฉ์ด ๋‚˜๊ฐ„ ์‚ฐ์ถœ๋ฌผ์ด๋‹ค). ํŠน์ • ํ‚ค๋งŒ
# ํ•˜๋“œ์ฝ”๋”ฉํ•ด ์˜ฎ๊ธฐ๋ฉด ๋‹ค์Œ์— ํŒŒ์ดํ”„๋ผ์ธ์ด ์ƒˆ artifacts ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ• 
# ๋•Œ๋งˆ๋‹ค ์—ฌ๊ธฐ๋ฅผ ๋˜ ๊ณ ์ณ์•ผ ํ•˜๋ฏ€๋กœ ํ†ต์งธ๋กœ ๋„˜๊ธด๋‹ค --
# resultRowSchema.trace ๋Š” .passthrough() ๋ผ ์—ฌ๋ถ„ ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ›๋Š”๋‹ค
# (source-schemas.ts:223-228).
run_result = {
"run_id": run_id,
"sample_idx": idx,
"input": input_data,
"output": eval_sample.output,
"golden": golden,
"input_tokens": eval_sample.artifacts.get("usage", {}).get(
"prompt_tokens", 0
),
"output_tokens": eval_sample.artifacts.get("usage", {}).get(
"completion_tokens", 0
),
"inference_time_ms": eval_sample.artifacts.get("inference_time_ms", 0),
"trace": {
"step_outputs": {},
**eval_sample.artifacts,
},
}
await self.store.insert_run_result(run_result)
completed += 1
ts = datetime.now().strftime("%H:%M:%S")
logger.info(f"[{ts}] Sample {idx} completed ({completed}/{len(data)})")
await self.store.update_run(run_id, {"completed_samples": completed})
if on_progress:
await on_progress(
{
"type": "inference_progress",
"completed": completed,
"total": len(data),
"sample_idx": idx,
}
)
return {**run_result, "_golden_raw": golden}
tasks = [process_sample(i, sample) for i, sample in enumerate(data)]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Filter out exceptions
valid_results = [r for r in results if isinstance(r, dict)]
errors = [(i, r) for i, r in enumerate(results) if isinstance(r, Exception)]
if errors:
logger.warning(f"Run {run_id}: {len(errors)}/{len(results)} samples failed")
for sample_idx, err in errors:
logger.warning(f" Sample {sample_idx} error: {type(err).__name__}: {err}")
if on_progress:
await on_progress({"type": "inference_complete", "total": len(valid_results)})
# Run evaluation
await self.store.update_run(run_id, {"status": RunStatus.EVALUATING})
if on_progress:
await on_progress({"type": "evaluation_start", "total": len(valid_results)})
evaluator = create_evaluator(task_config.get("evaluator", {"type": "llm_judge"}))
# eval_results: ์„ฑ๊ณตํ•œ evaluate() ๋ฐ˜ํ™˜๊ฐ’ + "sample_idx"(์ •๋ณธ, ยง0.5).
# eval_failures: ์ฑ„์  ์ž์ฒด๊ฐ€ ์•ˆ ๋œ ์ƒ˜ํ”Œ -- aggregate() ์—๋Š” ์ ˆ๋Œ€ ์•ˆ ๋„˜๊ธด๋‹ค
# (lcs_diff.aggregate() ์ฒ˜๋Ÿผ r["details"]["tp"] ๋ฅผ ์ง์ ‘ ์ฝ๋Š” ๊ตฌํ˜„์ด ์‹คํŒจ
# ํ•ญ๋ชฉ์„ ๋งŒ๋‚˜๋ฉด KeyError ๋กœ ์ฃฝ๋Š”๋‹ค, ยง0.1).
eval_results: list[dict[str, Any]] = []
eval_failures: list[dict[str, Any]] = []
for i, result in enumerate(valid_results):
# F6: enumerate ์œ„์น˜๊ฐ€ ์•„๋‹ˆ๋ผ result["sample_idx"] ๊ฐ€ ์ •๋ณธ์ด๋‹ค --
# ์ค‘๊ฐ„ ์ƒ˜ํ”Œ์ด ์ถ”๋ก ์—์„œ ์‹คํŒจํ•˜๋ฉด valid_results ์˜ ๋ฆฌ์ŠคํŠธ ์œ„์น˜์™€ ์›๋ณธ
# sample_idx ๊ฐ€ ์–ด๊ธ‹๋‚œ๋‹ค.
sample_idx = result["sample_idx"]
try:
eval_sample = build_eval_sample_from_result(
result, reference=result["_golden_raw"]
)
evaluator.validate_required_fields(eval_sample)
eval_result = await evaluator.evaluate(
sample=eval_sample,
provider=self.judge_provider,
)
eval_results.append({**eval_result, "sample_idx": sample_idx})
if on_progress and i % 10 == 0:
await on_progress(
{
"type": "evaluation_progress",
"completed": i + 1,
"total": len(valid_results),
}
)
except Exception as e:
logger.warning(f"Evaluation failed for sample {sample_idx}: {e}")
eval_failures.append({"sample_idx": sample_idx, "error": str(e)})
# Aggregate and save evaluation -- eval_results ์—๋Š” ์‹คํŒจ ํ•ญ๋ชฉ์ด ์•ˆ ์„ž์—ฌ
# ์žˆ์œผ๋ฏ€๋กœ ๊ธฐ์กด aggregate() ๊ตฌํ˜„์ด ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค.
aggregated = evaluator.aggregate(eval_results)
# ์ฑ„์ ์— ์„ฑ๊ณตํ•œ ์ƒ˜ํ”Œ์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์ ์ˆ˜ ์ž๋ฆฌ๋ฅผ ๋น„์šด๋‹ค -- aggregate() ๋Š” ๋นˆ
# ์ž…๋ ฅ์— 0.0 ์„ ๋Œ๋ ค์ฃผ๋Š”๋ฐ, ๊ทธ๊ฑด "0์ ์„ ๋ฐ›์•˜๋‹ค"๋Š” ์ธก์ •๊ฐ’์ด๋ผ "์ธก์ • ์ž์ฒด๊ฐ€
# ์—†์—ˆ๋‹ค"์™€ ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค. judge ๊ฐ€ ํ†ต์งธ๋กœ ์ฃฝ์€ run ์ด evalhub ์ฐจํŠธ์—์„œ
# ํ’ˆ์งˆ ๊ธ‰๋ฝ์œผ๋กœ ๋ณด์ด๋ฉด F1 ์„ ๋ฐ˜๋งŒ ๊ณ ์นœ ์…ˆ์ด๋‹ค.
overall_score = aggregated.get("overall_score", 0.0) if eval_results else None
eval_id = await self.store.create_evaluation(
{
"run_id": run_id,
"scores": aggregated.get("scores", {}),
"overall_score": overall_score,
"eval_model": "gpt-4o",
"eval_success_count": len(eval_results),
"eval_failed_count": len(eval_failures),
"failed_sample_indices": [f["sample_idx"] for f in eval_failures],
}
)
# Save per-sample eval details -- ์„ฑ๊ณต/์‹คํŒจ ๋‘˜ ๋‹ค ํ•œ ํ–‰์”ฉ ๋‚จ๊ธด๋‹ค.
# results.jsonl ๊ณผ eval_details.jsonl ์ด ํ•ญ์ƒ ๊ฐ™์€ sample_idx ์ง‘ํ•ฉ์„
# ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ํ•ด์„œ ๋ถ€๋ถ„์ ์œผ๋กœ๋งŒ ์ฑ„์ ๋œ run ์—์„œ "์ด ์ƒ˜ํ”Œ์€ ์™œ ์•ˆ ๋ณด์ด์ง€"๋ฅผ
# ์—†์•ค๋‹ค.
eval_detail_docs = []
for er in eval_results:
eval_detail_docs.append(
{
"evaluation_id": eval_id,
"sample_idx": er["sample_idx"],
"category_scores": er.get("category_scores", {}),
"error_counts": {
k: v.get("error_count", 0)
for k, v in er.get("details", {}).items()
if isinstance(v, dict)
},
"severity": er.get("severity", ""),
"score": er.get("score", 0.0),
}
)
for f in eval_failures:
eval_detail_docs.append(
{
"evaluation_id": eval_id,
"sample_idx": f["sample_idx"],
"category_scores": {},
"error_counts": {},
"severity": None,
# 0.0 ์ด ์•„๋‹ˆ๋ผ None -- ์ฑ„์  ์‹คํŒจ๋ฅผ ์ตœ์ € ์ ์ˆ˜์™€ ๊ตฌ๋ถ„ํ•œ๋‹ค (F1).
"score": None,
"error": f["error"],
}
)
await self.store.insert_eval_details(eval_detail_docs)
# Mark run as completed/partial -- ์ถ”๋ก ์ด ์ผ๋ถ€ ์ƒ˜ํ”Œ์—์„œ ์‹คํŒจํ–ˆ์œผ๋ฉด
# PARTIAL, ํ‰๊ฐ€ ์‹คํŒจ๋Š” ์ด ์ƒํƒœ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค(ํ‰๊ฐ€ ์„ฑ๊ณต/์‹คํŒจ๋Š”
# ์œ„ eval_success_count/eval_failed_count ๋กœ๋งŒ ํ‘œํ˜„ํ•œ๋‹ค, ยง0.4). errors
# ์˜ ์ธ๋ฑ์Šค๋Š” asyncio.gather ๊ฐ€ tasks ์ˆœ์„œ๋ฅผ ๋ณด์กดํ•˜๋ฏ€๋กœ ์ด๋ฏธ ์ง„์งœ
# sample_idx ๋‹ค.
final_status = RunStatus.PARTIAL if errors else RunStatus.COMPLETED
await self.store.update_run(
run_id,
{
"status": final_status,
"completed_at": datetime.now(timezone.utc),
"failed_samples": len(errors),
"failed_sample_indices": [i for i, _ in errors],
},
)
if on_progress:
await on_progress(
{
"type": "done",
"overall_score": aggregated.get("overall_score", 0.0),
"scores": aggregated.get("scores", {}),
}
)
except Exception as e:
logger.exception(f"Run {run_id} failed")
await self.store.update_run(
run_id,
{
"status": RunStatus.FAILED,
"completed_at": datetime.now(timezone.utc),
},
)
if on_progress:
await on_progress({"type": "error", "error": str(e)})
# F4: ์ƒํƒœ๋ฅผ FAILED ๋กœ ๋‚จ๊ธฐ๊ณ  ์ง„ํ–‰ ์ƒํ™ฉ๊นŒ์ง€ ์•Œ๋ฆฐ ๋’ค **๊ทธ๋Œ€๋กœ ์žฌ์ „ํŒŒํ•œ๋‹ค**.
# ์—ฌ๊ธฐ์„œ ์‚ผํ‚ค๋ฉด ํ˜ธ์ถœ์ž(CLI)๋Š” "Results saved to ..." ๋ฅผ ์ฐ๊ณ  evalhub ์ ์žฌ๊นŒ์ง€
# ์‹œ๋„ํ•œ ๋‹ค์Œ exit 0 ์„ ๋‚ธ๋‹ค -- ๋ฐ์ดํ„ฐ์…‹์กฐ์ฐจ ๋ชป ์ฝ์€ run ์ด ์„ฑ๊ณต์œผ๋กœ ๋ณด์ธ๋‹ค.
raise