@@ -68,8 +68,12 @@ def extensions(self) -> tuple[str, ...]:
6868 """File extensions this runtime handles (e.g. ``(".py",)``)."""
6969
7070 @abc .abstractmethod
71- def build_command (self , path : Path ) -> list [str ]:
72- """Return the argv list to execute *path*."""
71+ def build_command (self , path : Path , python : Path | None = None ) -> list [str ]:
72+ """Return the argv list to execute *path*.
73+
74+ For Python runtimes, *python* may point to a venv interpreter.
75+ Non-Python runtimes ignore this parameter.
76+ """
7377
7478 def is_available (self ) -> bool :
7579 """Return True if the runtime's interpreter is found on the system."""
@@ -89,8 +93,9 @@ def name(self) -> str:
8993 def extensions (self ) -> tuple [str , ...]:
9094 return (".py" ,)
9195
92- def build_command (self , path : Path ) -> list [str ]:
93- return [sys .executable , str (path )]
96+ def build_command (self , path : Path , python : Path | None = None ) -> list [str ]:
97+ exe = str (python ) if python else sys .executable
98+ return [exe , str (path )]
9499
95100 def is_available (self ) -> bool :
96101 return True
@@ -105,7 +110,7 @@ def name(self) -> str:
105110 def extensions (self ) -> tuple [str , ...]:
106111 return (".js" , ".ts" )
107112
108- def build_command (self , path : Path ) -> list [str ]:
113+ def build_command (self , path : Path , python : Path | None = None ) -> list [str ]:
109114 node = shutil .which ("node" )
110115 if not node :
111116 raise RuntimeError ("Node.js not found on PATH (required for .js/.ts evaluators)" )
@@ -203,40 +208,45 @@ class SubprocessBackend(EvaluatorBackend):
203208 """Runs a local code file (.py, .js, .ts, …) as a subprocess.
204209
205210 The correct interpreter is resolved from the file extension via the
206- :data:`_RUNTIMES` registry.
211+ :data:`_RUNTIMES` registry. When *venv_python* is provided, Python
212+ evaluators run inside that virtual environment instead of the host
213+ interpreter.
207214 """
208215
209- def __init__ (self , path : Path , timeout : int = 30 ):
216+ def __init__ (self , path : Path , timeout : int = 30 , venv_python : Path | None = None ):
210217 self ._path = path .resolve ()
211218 self ._runtime = _resolve_runtime (self ._path )
212219 self ._timeout = timeout
220+ self ._venv_python = venv_python
213221
214222 if not self ._path .exists ():
215223 raise FileNotFoundError (f"Evaluator file not found: { self ._path } " )
216224
217225 async def run (self , eval_input : EvalInput , metric_name : str ) -> EvalResult :
218- cmd = self ._runtime .build_command (self ._path )
226+ cmd = self ._runtime .build_command (self ._path , self . _venv_python )
219227 return await _run_subprocess (cmd , eval_input .model_dump_json (), self ._timeout , metric_name )
220228
221229
222230# ---------------------------------------------------------------------------
223231# Executor factory
224232# ---------------------------------------------------------------------------
225233
226- _EXECUTOR_FACTORIES : dict [str , Callable [[ Path , int ] , EvaluatorBackend ]] = {
227- "local" : lambda path , timeout : SubprocessBackend (path , timeout ),
234+ _EXECUTOR_FACTORIES : dict [str , Callable [... , EvaluatorBackend ]] = {
235+ "local" : lambda path , timeout , venv_python = None : SubprocessBackend (path , timeout , venv_python ),
228236}
229237
230238
231- def create_executor (executor_name : str , path : Path , timeout : int = 30 ) -> EvaluatorBackend :
239+ def create_executor (
240+ executor_name : str , path : Path , timeout : int = 30 , venv_python : Path | None = None
241+ ) -> EvaluatorBackend :
232242 """Construct an EvaluatorBackend by executor name (e.g. 'local', 'docker')."""
233243 factory = _EXECUTOR_FACTORIES .get (executor_name )
234244 if factory is None :
235245 raise ValueError (f"Unknown executor '{ executor_name } '. Available: { sorted (_EXECUTOR_FACTORIES .keys ())} " )
236- return factory (path , timeout )
246+ return factory (path , timeout , venv_python )
237247
238248
239- def register_executor (name : str , factory : Callable [[ Path , int ] , EvaluatorBackend ]) -> None :
249+ def register_executor (name : str , factory : Callable [... , EvaluatorBackend ]) -> None :
240250 """Register a new executor factory (e.g. for Docker support)."""
241251 _EXECUTOR_FACTORIES [name ] = factory
242252
@@ -425,7 +435,25 @@ async def evaluate_custom_evaluator(
425435 evaluator_def = await get_default_resolver ().resolve (evaluator_def )
426436
427437 if isinstance (evaluator_def , CodeEvaluatorDef ):
428- backend = create_executor (evaluator_def .executor , Path (evaluator_def .path ), evaluator_def .timeout )
438+ evaluator_path = Path (evaluator_def .path )
439+
440+ # Set up a venv if the evaluator ships a requirements.txt.
441+ venv_python : Path | None = None
442+ if evaluator_path .suffix == ".py" :
443+ from .evaluator .venv import ensure_venv_async
444+
445+ try :
446+ venv_python = await ensure_venv_async (evaluator_path )
447+ except Exception as exc :
448+ logger .error ("Failed to set up venv for '%s': %s" , evaluator_def .name , exc )
449+ return MetricResult (
450+ metric_name = evaluator_def .name ,
451+ error = f"Dependency installation failed: { exc } " ,
452+ )
453+
454+ backend = create_executor (
455+ evaluator_def .executor , evaluator_path , evaluator_def .timeout , venv_python = venv_python
456+ )
429457 else :
430458 raise ValueError (f"Unsupported custom evaluator type: { type (evaluator_def ).__name__ } " )
431459
0 commit comments