Skip to content

Commit a2aaf1b

Browse files
cristipufuclaude
andauthored
fix: use runtime factory for eval entrypoint discovery and show usage help (#1398)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d2dea9c commit a2aaf1b

7 files changed

Lines changed: 495 additions & 160 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.10.3"
3+
version = "2.10.4"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/_cli/cli_eval.py

Lines changed: 135 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import uuid
6+
from pathlib import Path
67
from typing import Any
78

89
import click
@@ -15,8 +16,7 @@
1516
from uipath._cli.middlewares import Middlewares
1617
from uipath.core.events import EventBus
1718
from uipath.core.tracing import UiPathTraceManager
18-
from uipath.eval._helpers import auto_discover_entrypoint
19-
from uipath.eval.helpers import EvalHelpers
19+
from uipath.eval.helpers import EVAL_SETS_DIRECTORY_NAME, EvalHelpers
2020
from uipath.eval.models.evaluation_set import EvaluationSet
2121
from uipath.eval.runtime import UiPathEvalContext, evaluate
2222
from uipath.platform.chat import set_llm_concurrency
@@ -135,6 +135,55 @@ def _resolve_model_settings_override(
135135
return override if override else None
136136

137137

138+
class _EvalDiscoveryError(Exception):
139+
"""Raised when auto-discovery of entrypoint or eval set fails."""
140+
141+
def __init__(self, entrypoints: list[str], eval_sets: list[Path]):
142+
self.entrypoints = entrypoints
143+
self.eval_sets = eval_sets
144+
145+
146+
def _discover_eval_sets() -> list[Path]:
147+
"""Discover available eval set files."""
148+
eval_sets_dir = Path(EVAL_SETS_DIRECTORY_NAME)
149+
if eval_sets_dir.exists():
150+
return sorted(eval_sets_dir.glob("*.json"))
151+
return []
152+
153+
154+
def _show_eval_usage_help(entrypoints: list[str], eval_set_files: list[Path]) -> None:
155+
"""Show available entrypoints and eval sets with usage examples."""
156+
lines: list[str] = []
157+
158+
if entrypoints:
159+
lines.append("Available entrypoints:")
160+
for name in entrypoints:
161+
lines.append(f" - {name}")
162+
else:
163+
lines.append(
164+
"No entrypoints found. "
165+
"Add a 'functions' or 'agents' section to your config file "
166+
"(e.g. uipath.json, langgraph.json)."
167+
)
168+
169+
if eval_set_files:
170+
lines.append("\nAvailable eval sets:")
171+
for f in eval_set_files:
172+
lines.append(f" - {f}")
173+
else:
174+
lines.append(
175+
f"\nNo eval sets found in '{EVAL_SETS_DIRECTORY_NAME}/' directory."
176+
)
177+
178+
lines.append("\nUsage: uipath eval <entrypoint> <eval_set>")
179+
if entrypoints and eval_set_files:
180+
ep_name = entrypoints[0]
181+
es_path = eval_set_files[0]
182+
lines.append(f"Example: uipath eval {ep_name} {es_path}")
183+
184+
click.echo("\n".join(lines))
185+
186+
138187
@click.command()
139188
@click.argument("entrypoint", required=False)
140189
@click.argument("eval_set", required=False)
@@ -266,18 +315,9 @@ def eval(
266315

267316
if result.should_continue:
268317
eval_context = UiPathEvalContext()
269-
270-
eval_context.entrypoint = entrypoint or auto_discover_entrypoint()
271318
eval_context.workers = workers
272319
eval_context.eval_set_run_id = eval_set_run_id
273320
eval_context.enable_mocker_cache = enable_mocker_cache
274-
275-
# Load eval set to resolve the path
276-
eval_set_path = eval_set or EvalHelpers.auto_discover_eval_set()
277-
_, resolved_eval_set_path = EvalHelpers.load_eval_set(
278-
eval_set_path, eval_ids, input_overrides=input_overrides
279-
)
280-
281321
eval_context.report_coverage = report_coverage
282322
eval_context.input_overrides = input_overrides
283323
eval_context.resume = resume
@@ -311,71 +351,102 @@ async def execute_eval():
311351
eval_context.job_id = ctx.job_id
312352

313353
runtime_factory = UiPathRuntimeFactoryRegistry.get(context=ctx)
314-
factory_settings = await runtime_factory.get_settings()
315-
trace_settings = (
316-
factory_settings.trace_settings if factory_settings else None
317-
)
318-
319-
if (
320-
ctx.job_id or should_register_progress_reporter
321-
) and UiPathConfig.is_tracing_enabled:
322-
# Live tracking for Orchestrator or Studio Web
323-
# Uses UIPATH_TRACE_ID from environment for trace correlation
324-
trace_manager.add_span_processor(
325-
LiveTrackingSpanProcessor(
326-
LlmOpsHttpExporter(),
327-
settings=trace_settings,
354+
355+
try:
356+
# Auto-discover entrypoint and eval set using the runtime factory
357+
resolved_entrypoint = entrypoint
358+
eval_set_path = eval_set
359+
360+
available_entrypoints = runtime_factory.discover_entrypoints()
361+
available_eval_sets = _discover_eval_sets()
362+
363+
if not resolved_entrypoint:
364+
if len(available_entrypoints) == 1:
365+
resolved_entrypoint = available_entrypoints[0]
366+
else:
367+
raise _EvalDiscoveryError(
368+
available_entrypoints, available_eval_sets
369+
)
370+
371+
if not eval_set_path:
372+
if len(available_eval_sets) == 1:
373+
eval_set_path = str(available_eval_sets[0])
374+
else:
375+
raise _EvalDiscoveryError(
376+
available_entrypoints, available_eval_sets
377+
)
378+
379+
eval_context.entrypoint = resolved_entrypoint
380+
381+
# Load eval set and resolve the path
382+
loaded_eval_set, resolved_eval_set_path = (
383+
EvalHelpers.load_eval_set(
384+
eval_set_path, eval_ids, input_overrides=input_overrides
328385
)
329386
)
330387

331-
if trace_file:
388+
factory_settings = await runtime_factory.get_settings()
332389
trace_settings = (
333390
factory_settings.trace_settings
334391
if factory_settings
335392
else None
336393
)
337-
trace_manager.add_span_exporter(
338-
JsonLinesFileExporter(trace_file), settings=trace_settings
339-
)
340394

341-
project_id = UiPathConfig.project_id
395+
if (
396+
ctx.job_id or should_register_progress_reporter
397+
) and UiPathConfig.is_tracing_enabled:
398+
# Live tracking for Orchestrator or Studio Web
399+
# Uses UIPATH_TRACE_ID from environment for trace correlation
400+
trace_manager.add_span_processor(
401+
LiveTrackingSpanProcessor(
402+
LlmOpsHttpExporter(),
403+
settings=trace_settings,
404+
)
405+
)
342406

343-
eval_context.execution_id = (
344-
eval_context.job_id
345-
or eval_context.eval_set_run_id
346-
or str(uuid.uuid4())
347-
)
407+
if trace_file:
408+
trace_settings = (
409+
factory_settings.trace_settings
410+
if factory_settings
411+
else None
412+
)
413+
trace_manager.add_span_exporter(
414+
JsonLinesFileExporter(trace_file),
415+
settings=trace_settings,
416+
)
348417

349-
# Load eval set (path is already resolved in cli_eval.py)
350-
eval_context.evaluation_set, _ = EvalHelpers.load_eval_set(
351-
resolved_eval_set_path,
352-
eval_ids,
353-
input_overrides=input_overrides,
354-
)
418+
project_id = UiPathConfig.project_id
355419

356-
# Resolve model settings override from eval set
357-
settings_override = _resolve_model_settings_override(
358-
model_settings_id, eval_context.evaluation_set
359-
)
420+
eval_context.execution_id = (
421+
eval_context.job_id
422+
or eval_context.eval_set_run_id
423+
or str(uuid.uuid4())
424+
)
360425

361-
runtime = await runtime_factory.new_runtime(
362-
entrypoint=eval_context.entrypoint or "",
363-
runtime_id=eval_context.execution_id,
364-
settings=settings_override,
365-
)
426+
eval_context.evaluation_set = loaded_eval_set
366427

367-
eval_context.runtime_schema = await runtime.get_schema()
428+
# Resolve model settings override from eval set
429+
settings_override = _resolve_model_settings_override(
430+
model_settings_id, eval_context.evaluation_set
431+
)
368432

369-
eval_context.evaluators = await EvalHelpers.load_evaluators(
370-
resolved_eval_set_path,
371-
eval_context.evaluation_set,
372-
_get_agent_model(eval_context.runtime_schema),
373-
)
433+
runtime = await runtime_factory.new_runtime(
434+
entrypoint=eval_context.entrypoint or "",
435+
runtime_id=eval_context.execution_id,
436+
settings=settings_override,
437+
)
374438

375-
# Runtime is not required anymore.
376-
await runtime.dispose()
439+
eval_context.runtime_schema = await runtime.get_schema()
440+
441+
eval_context.evaluators = await EvalHelpers.load_evaluators(
442+
resolved_eval_set_path,
443+
eval_context.evaluation_set,
444+
_get_agent_model(eval_context.runtime_schema),
445+
)
446+
447+
# Runtime is not required anymore.
448+
await runtime.dispose()
377449

378-
try:
379450
if project_id:
380451
studio_client = StudioClient(project_id)
381452

@@ -399,11 +470,14 @@ async def execute_eval():
399470
event_bus,
400471
)
401472
finally:
402-
if runtime_factory:
403-
await runtime_factory.dispose()
473+
await runtime_factory.dispose()
404474

405475
asyncio.run(execute_eval())
406476

477+
except _EvalDiscoveryError as e:
478+
_show_eval_usage_help(e.entrypoints, e.eval_sets)
479+
except ValueError as e:
480+
console.error(str(e))
407481
except Exception as e:
408482
console.error(
409483
f"Error occurred: {e or 'Execution failed'}", include_traceback=True
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
from .helpers import auto_discover_entrypoint
2-
3-
__all__ = ["auto_discover_entrypoint"]
1+
"""Helper functions for evaluation process."""

src/uipath/eval/_helpers/helpers.py

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1+
"""Helper functions for evaluation process."""
2+
13
import functools
2-
import json
3-
import os
44
import time
55
from collections.abc import Callable
66
from typing import Any
77

8-
import click
9-
108
from ..models import ErrorEvaluationResult, EvaluationResult
119

1210

@@ -37,52 +35,6 @@ def is_empty_value(value: Any) -> bool:
3735
return False
3836

3937

40-
def auto_discover_entrypoint() -> str:
41-
"""Auto-discover entrypoint from config file.
42-
43-
Returns:
44-
Entrypoint name (key from the functions dict)
45-
46-
Raises:
47-
ValueError: If no entrypoint found or multiple entrypoints exist
48-
"""
49-
from uipath._cli._utils._console import ConsoleLogger
50-
from uipath._utils.constants import UIPATH_CONFIG_FILE
51-
52-
console = ConsoleLogger()
53-
54-
if not os.path.isfile(UIPATH_CONFIG_FILE):
55-
raise ValueError(
56-
f"File '{UIPATH_CONFIG_FILE}' not found. Please run 'uipath init'."
57-
)
58-
59-
with open(UIPATH_CONFIG_FILE, "r", encoding="utf-8") as f:
60-
uipath_config = json.loads(f.read())
61-
62-
entrypoints: dict[str, str] = uipath_config.get("functions", {})
63-
64-
if not entrypoints:
65-
raise ValueError(
66-
f"No entrypoints found in {UIPATH_CONFIG_FILE}. "
67-
"Add a 'functions' section to uipath.json"
68-
)
69-
70-
if len(entrypoints) > 1:
71-
entrypoint_list = list(entrypoints.keys())
72-
raise ValueError(
73-
f"Multiple entrypoints found: {entrypoint_list}. "
74-
"Please specify which entrypoint to use."
75-
)
76-
77-
entrypoint_name = next(iter(entrypoints.keys()))
78-
entrypoint_path = entrypoints[entrypoint_name]
79-
console.info(
80-
f"Auto-discovered entrypoint: {click.style(entrypoint_name, fg='cyan')} "
81-
f"({entrypoint_path})"
82-
)
83-
return entrypoint_name
84-
85-
8638
def track_evaluation_metrics(func: Callable[..., Any]) -> Callable[..., Any]:
8739
"""Decorator to track evaluation metrics and handle errors gracefully."""
8840

src/uipath/eval/helpers.py

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from pathlib import Path
66
from typing import Any
77

8-
import click
98
from pydantic import ValidationError
109

1110
from .evaluators.base_evaluator import GenericBaseEvaluator
@@ -90,49 +89,6 @@ def discriminate_eval_set(data: dict[str, Any]) -> EvaluationSet | LegacyEvaluat
9089
class EvalHelpers:
9190
"""Helper functions for evaluation commands, including loading and parsing evaluation sets and evaluators."""
9291

93-
@staticmethod
94-
def auto_discover_eval_set() -> str:
95-
"""Auto-discover evaluation set from {EVAL_SETS_DIRECTORY_NAME} directory.
96-
97-
Returns:
98-
Path to the evaluation set file
99-
100-
Raises:
101-
ValueError: If no eval set found or multiple eval sets exist
102-
"""
103-
eval_sets_dir = Path(EVAL_SETS_DIRECTORY_NAME)
104-
105-
if not eval_sets_dir.exists():
106-
raise ValueError(
107-
f"No '{EVAL_SETS_DIRECTORY_NAME}' directory found. "
108-
"Please set 'UIPATH_PROJECT_ID' env var and run 'uipath pull'."
109-
)
110-
111-
eval_set_files = list(eval_sets_dir.glob("*.json"))
112-
113-
if not eval_set_files:
114-
raise ValueError(
115-
f"No evaluation set files found in '{EVAL_SETS_DIRECTORY_NAME}' directory. "
116-
)
117-
118-
if len(eval_set_files) > 1:
119-
file_names = [f.name for f in eval_set_files]
120-
raise ValueError(
121-
f"Multiple evaluation sets found: {file_names}. "
122-
f"Please specify which evaluation set to use: 'uipath eval [entrypoint] <eval_set_path>'"
123-
)
124-
125-
eval_set_path = str(eval_set_files[0])
126-
logger.info(
127-
f"Auto-discovered evaluation set: {click.style(eval_set_path, fg='cyan')}"
128-
)
129-
130-
eval_set_path_obj = Path(eval_set_path)
131-
if not eval_set_path_obj.is_file() or eval_set_path_obj.suffix != ".json":
132-
raise ValueError("Evaluation set must be a JSON file")
133-
134-
return eval_set_path
135-
13692
@staticmethod
13793
def load_eval_set(
13894
eval_set_path: str,

0 commit comments

Comments
 (0)