@@ -81,6 +81,9 @@ def is_available(self) -> bool:
8181
8282
8383class PythonRuntime (Runtime ):
84+ def __init__ (self , python_path : Path | None = None ):
85+ self ._exe = str (python_path ) if python_path else sys .executable
86+
8487 @property
8588 def name (self ) -> str :
8689 return "Python"
@@ -90,13 +93,16 @@ def extensions(self) -> tuple[str, ...]:
9093 return (".py" ,)
9194
9295 def build_command (self , path : Path ) -> list [str ]:
93- return [sys . executable , str (path )]
96+ return [self . _exe , str (path )]
9497
9598 def is_available (self ) -> bool :
9699 return True
97100
98101
99102class NodeRuntime (Runtime ):
103+ def __init__ (self ) -> None :
104+ self ._exe = shutil .which ("node" )
105+
100106 @property
101107 def name (self ) -> str :
102108 return "Node.js"
@@ -106,10 +112,12 @@ def extensions(self) -> tuple[str, ...]:
106112 return (".js" , ".ts" )
107113
108114 def build_command (self , path : Path ) -> list [str ]:
109- node = shutil .which ("node" )
110- if not node :
115+ if not self ._exe :
111116 raise RuntimeError ("Node.js not found on PATH (required for .js/.ts evaluators)" )
112- return [node , str (path )]
117+ return [self ._exe , str (path )]
118+
119+ def is_available (self ) -> bool :
120+ return self ._exe is not None
113121
114122
115123_RUNTIMES : list [Runtime ] = [
@@ -203,12 +211,13 @@ class SubprocessBackend(EvaluatorBackend):
203211 """Runs a local code file (.py, .js, .ts, …) as a subprocess.
204212
205213 The correct interpreter is resolved from the file extension via the
206- :data:`_RUNTIMES` registry.
214+ :data:`_RUNTIMES` registry. Pass a pre-configured *runtime* to override
215+ the default (e.g. a :class:`PythonRuntime` with a venv interpreter).
207216 """
208217
209- def __init__ (self , path : Path , timeout : int = 30 ):
218+ def __init__ (self , path : Path , timeout : int = 30 , runtime : Runtime | None = None ):
210219 self ._path = path .resolve ()
211- self ._runtime = _resolve_runtime (self ._path )
220+ self ._runtime = runtime or _resolve_runtime (self ._path )
212221 self ._timeout = timeout
213222
214223 if not self ._path .exists ():
@@ -223,7 +232,7 @@ async def run(self, eval_input: EvalInput, metric_name: str) -> EvalResult:
223232# Executor factory
224233# ---------------------------------------------------------------------------
225234
226- _EXECUTOR_FACTORIES : dict [str , Callable [[ Path , int ] , EvaluatorBackend ]] = {
235+ _EXECUTOR_FACTORIES : dict [str , Callable [... , EvaluatorBackend ]] = {
227236 "local" : lambda path , timeout : SubprocessBackend (path , timeout ),
228237}
229238
@@ -236,7 +245,7 @@ def create_executor(executor_name: str, path: Path, timeout: int = 30) -> Evalua
236245 return factory (path , timeout )
237246
238247
239- def register_executor (name : str , factory : Callable [[ Path , int ] , EvaluatorBackend ]) -> None :
248+ def register_executor (name : str , factory : Callable [... , EvaluatorBackend ]) -> None :
240249 """Register a new executor factory (e.g. for Docker support)."""
241250 _EXECUTOR_FACTORIES [name ] = factory
242251
@@ -425,7 +434,27 @@ async def evaluate_custom_evaluator(
425434 evaluator_def = await get_default_resolver ().resolve (evaluator_def )
426435
427436 if isinstance (evaluator_def , CodeEvaluatorDef ):
428- backend = create_executor (evaluator_def .executor , Path (evaluator_def .path ), evaluator_def .timeout )
437+ evaluator_path = Path (evaluator_def .path )
438+
439+ runtime : Runtime | None = None
440+ if evaluator_path .suffix == ".py" :
441+ from .evaluator .venv import ensure_venv_async
442+
443+ try :
444+ venv_python = await ensure_venv_async (evaluator_path )
445+ except Exception as exc :
446+ logger .error ("Failed to set up venv for '%s': %s" , evaluator_def .name , exc )
447+ return MetricResult (
448+ metric_name = evaluator_def .name ,
449+ error = f"Dependency installation failed: { exc } " ,
450+ )
451+ if venv_python :
452+ runtime = PythonRuntime (python_path = venv_python )
453+
454+ if runtime is not None :
455+ backend = SubprocessBackend (evaluator_path , evaluator_def .timeout , runtime = runtime )
456+ else :
457+ backend = create_executor (evaluator_def .executor , evaluator_path , evaluator_def .timeout )
429458 else :
430459 raise ValueError (f"Unsupported custom evaluator type: { type (evaluator_def ).__name__ } " )
431460
0 commit comments