uipath 2.1.71__py3-none-any.whl → 2.1.73__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ """Mocker Factory."""
2
+
3
+ from uipath._cli._evals._models._evaluation_set import (
4
+ EvaluationItem,
5
+ LLMMockingStrategy,
6
+ MockitoMockingStrategy,
7
+ )
8
+ from uipath._cli._evals.mocks.llm_mocker import LLMMocker
9
+ from uipath._cli._evals.mocks.mocker import Mocker
10
+ from uipath._cli._evals.mocks.mockito_mocker import MockitoMocker
11
+
12
+
13
+ class MockerFactory:
14
+ """Mocker factory."""
15
+
16
+ @staticmethod
17
+ def create(evaluation_item: EvaluationItem) -> Mocker:
18
+ """Create a mocker instance."""
19
+ match evaluation_item.mocking_strategy:
20
+ case LLMMockingStrategy():
21
+ return LLMMocker(evaluation_item)
22
+ case MockitoMockingStrategy():
23
+ return MockitoMocker(evaluation_item)
24
+ case _:
25
+ raise ValueError("Unknown mocking strategy")
@@ -0,0 +1,62 @@
1
+ """Mockito mocker implementation.
2
+
3
+ https://mockito-python.readthedocs.io/en/latest/
4
+ """
5
+
6
+ from typing import Any, Callable
7
+
8
+ from hydra.utils import instantiate
9
+ from mockito import invocation, mocking # type: ignore[import-untyped]
10
+
11
+ from uipath._cli._evals._models._evaluation_set import (
12
+ EvaluationItem,
13
+ MockingAnswerType,
14
+ MockitoMockingStrategy,
15
+ )
16
+ from uipath._cli._evals.mocks.mocker import Mocker, R, T
17
+
18
+
19
+ class Stub:
20
+ """Stub interface."""
21
+
22
+ def __getattr__(self, item):
23
+ """Return a wrapper function that raises an exception."""
24
+
25
+ def func(*_args, **_kwargs):
26
+ """Not Implemented."""
27
+ raise NotImplementedError()
28
+
29
+ return func
30
+
31
+
32
+ class MockitoMocker(Mocker):
33
+ """Mockito Mocker."""
34
+
35
+ def __init__(self, evaluation_item: EvaluationItem):
36
+ """Instantiate a mockito mocker."""
37
+ self.evaluation_item = evaluation_item
38
+ assert isinstance(self.evaluation_item.mocking_strategy, MockitoMockingStrategy)
39
+
40
+ self.stub = Stub()
41
+ mock_obj = mocking.Mock(self.stub)
42
+
43
+ for behavior in self.evaluation_item.mocking_strategy.behaviors:
44
+ stubbed = invocation.StubbedInvocation(mock_obj, behavior.function)(
45
+ *instantiate(behavior.arguments.args, _convert_="object"),
46
+ **instantiate(behavior.arguments.kwargs, _convert_="object"),
47
+ )
48
+ for answer in behavior.then:
49
+ if answer.type == MockingAnswerType.RETURN:
50
+ stubbed = stubbed.thenReturn(
51
+ instantiate(answer.model_dump(), _convert_="object")["value"]
52
+ )
53
+ elif answer.type == MockingAnswerType.RAISE:
54
+ stubbed = stubbed.thenRaise(
55
+ instantiate(answer.model_dump(), _convert_="object")["value"]
56
+ )
57
+
58
+ async def response(
59
+ self, func: Callable[[T], R], params: dict[str, Any], *args: T, **kwargs
60
+ ) -> R:
61
+ """Respond with mocked response."""
62
+ return getattr(self.stub, params["name"])(*args, **kwargs)
@@ -0,0 +1,136 @@
1
+ """Mocking interface."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import inspect
6
+ import logging
7
+ import threading
8
+ from contextvars import ContextVar
9
+ from typing import Any, Callable, Optional
10
+
11
+ from pydantic import TypeAdapter
12
+ from pydantic_function_models import ValidatedFunction # type: ignore[import-untyped]
13
+
14
+ from uipath._cli._evals._models._evaluation_set import EvaluationItem
15
+ from uipath._cli._evals.mocks.mocker import Mocker
16
+ from uipath._cli._evals.mocks.mocker_factory import MockerFactory
17
+
18
+ evaluation_context: ContextVar[Optional[EvaluationItem]] = ContextVar(
19
+ "evaluation", default=None
20
+ )
21
+
22
+ mocker_context: ContextVar[Optional[Mocker]] = ContextVar("mocker", default=None)
23
+
24
+
25
+ def set_evaluation_item(item: EvaluationItem) -> None:
26
+ """Set an evaluation item within an evaluation set."""
27
+ evaluation_context.set(item)
28
+ try:
29
+ mocker_context.set(MockerFactory.create(item))
30
+ except Exception:
31
+ logger.warning(f"Failed to create mocker for evaluation {item.name}")
32
+ mocker_context.set(None)
33
+
34
+
35
+ async def get_mocked_response(
36
+ func: Callable[[Any], Any], params: dict[str, Any], *args, **kwargs
37
+ ) -> Any:
38
+ """Get a mocked response."""
39
+ mocker = mocker_context.get()
40
+ evaluation_item = evaluation_context.get()
41
+ if mocker is None or evaluation_item is None:
42
+ # TODO raise a new UiPath exception type
43
+ raise RuntimeError(f"Evaluation item {func.__name__} has not been evaluated")
44
+ else:
45
+ return await mocker.response(func, params, *args, **kwargs)
46
+
47
+
48
+ _event_loop = None
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ def run_coroutine(coro):
53
+ """Run a coroutine synchronously."""
54
+ global _event_loop
55
+ if not _event_loop or not _event_loop.is_running():
56
+ _event_loop = asyncio.new_event_loop()
57
+ threading.Thread(target=_event_loop.run_forever, daemon=True).start()
58
+ future = asyncio.run_coroutine_threadsafe(coro, _event_loop)
59
+ return future.result()
60
+
61
+
62
+ def mocked_response_decorator(func, params: dict[str, Any]):
63
+ """Mocked response decorator."""
64
+
65
+ async def mock_response_generator(*args, **kwargs):
66
+ mocked_response = await get_mocked_response(func, params, *args, **kwargs)
67
+ return_type: Any = func.__annotations__.get("return", None)
68
+
69
+ if return_type is not None:
70
+ mocked_response = TypeAdapter(return_type).validate_python(mocked_response)
71
+ return mocked_response
72
+
73
+ is_async = inspect.iscoroutinefunction(func)
74
+ if is_async:
75
+
76
+ @functools.wraps(func)
77
+ async def decorated_func(*args, **kwargs):
78
+ try:
79
+ return await mock_response_generator(*args, **kwargs)
80
+ except Exception:
81
+ logger.warning(
82
+ f"Failed to mock response for {func.__name__}. Falling back to func."
83
+ )
84
+ return await func(*args, **kwargs)
85
+ else:
86
+
87
+ @functools.wraps(func)
88
+ def decorated_func(*args, **kwargs):
89
+ try:
90
+ return run_coroutine(mock_response_generator(*args, **kwargs))
91
+ except Exception:
92
+ logger.warning(
93
+ f"Failed to mock response for {func.__name__}. Falling back to func."
94
+ )
95
+ return func(*args, **kwargs)
96
+
97
+ return decorated_func
98
+
99
+
100
+ def mockable(
101
+ name: Optional[str] = None,
102
+ description: Optional[str] = None,
103
+ **kwargs,
104
+ ):
105
+ """Decorate a function to be a mockable."""
106
+
107
+ def decorator(func):
108
+ params = {
109
+ "name": name or func.__name__,
110
+ "description": description or func.__doc__,
111
+ "input_schema": get_input_schema(func),
112
+ "output_schema": get_output_schema(func),
113
+ **kwargs,
114
+ }
115
+ return mocked_response_decorator(func, params)
116
+
117
+ return decorator
118
+
119
+
120
+ def get_output_schema(func):
121
+ """Retrieves the JSON schema for a function's return type hint."""
122
+ try:
123
+ adapter = TypeAdapter(inspect.signature(func).return_annotation)
124
+ return adapter.json_schema()
125
+ except Exception:
126
+ logger.warning(f"Unable to extract output schema for function {func.__name__}")
127
+ return {}
128
+
129
+
130
+ def get_input_schema(func):
131
+ """Retrieves the JSON schema for a function's input type."""
132
+ try:
133
+ return ValidatedFunction(func).model.model_json_schema()
134
+ except Exception:
135
+ logger.warning(f"Unable to extract input schema for function {func.__name__}")
136
+ return {}
@@ -525,6 +525,7 @@ class UiPathBaseRuntime(ABC):
525
525
  dir=self.context.runtime_dir,
526
526
  file=self.context.logs_file,
527
527
  job_id=self.context.job_id,
528
+ execution_id=self.context.execution_id,
528
529
  is_debug_run=self.is_debug_run(),
529
530
  log_handler=self.context.log_handler,
530
531
  )
@@ -1,8 +1,14 @@
1
1
  import logging
2
2
  import os
3
3
  import sys
4
+ from contextvars import ContextVar
4
5
  from typing import Optional, TextIO, Union, cast
5
6
 
7
+ # Context variable to track current execution_id
8
+ current_execution_id: ContextVar[Optional[str]] = ContextVar(
9
+ "current_execution_id", default=None
10
+ )
11
+
6
12
 
7
13
  class PersistentLogsHandler(logging.FileHandler):
8
14
  """A simple log handler that always writes to a single file without rotation."""
@@ -20,6 +26,30 @@ class PersistentLogsHandler(logging.FileHandler):
20
26
  self.setFormatter(self.formatter)
21
27
 
22
28
 
29
+ class ExecutionContextFilter(logging.Filter):
30
+ """Filter that only allows logs from a specific execution context."""
31
+
32
+ def __init__(self, execution_id: str):
33
+ super().__init__()
34
+ self.execution_id = execution_id
35
+
36
+ def filter(self, record: logging.LogRecord) -> bool:
37
+ """Allow logs that have matching execution_id attribute or context."""
38
+ # First check if record has execution_id attribute
39
+ record_execution_id = getattr(record, "execution_id", None)
40
+ if record_execution_id == self.execution_id:
41
+ return True
42
+
43
+ # Fall back to context variable
44
+ ctx_execution_id = current_execution_id.get()
45
+ if ctx_execution_id == self.execution_id:
46
+ # Inject execution_id into record for downstream handlers
47
+ record.execution_id = self.execution_id
48
+ return True
49
+
50
+ return False
51
+
52
+
23
53
  class LogsInterceptor:
24
54
  """Intercepts all logging and stdout/stderr, routing to either persistent log files or stdout based on whether it's running as a job or not."""
25
55
 
@@ -31,6 +61,7 @@ class LogsInterceptor:
31
61
  job_id: Optional[str] = None,
32
62
  is_debug_run: bool = False,
33
63
  log_handler: Optional[logging.Handler] = None,
64
+ execution_id: Optional[str] = None,
34
65
  ):
35
66
  """Initialize the log interceptor.
36
67
 
@@ -41,9 +72,11 @@ class LogsInterceptor:
41
72
  job_id (str, optional): If provided, logs go to file; otherwise, to stdout.
42
73
  is_debug_run (bool, optional): If True, log the output to stdout/stderr.
43
74
  log_handler (logging.Handler, optional): Custom log handler to use.
75
+ execution_id (str, optional): Unique identifier for this execution context.
44
76
  """
45
77
  min_level = min_level or "INFO"
46
78
  self.job_id = job_id
79
+ self.execution_id = execution_id
47
80
 
48
81
  # Convert to numeric level for consistent comparison
49
82
  self.numeric_min_level = getattr(logging, min_level.upper(), logging.INFO)
@@ -81,6 +114,12 @@ class LogsInterceptor:
81
114
  self.log_handler = PersistentLogsHandler(file=log_file)
82
115
 
83
116
  self.log_handler.setLevel(self.numeric_min_level)
117
+
118
+ # Add execution context filter if execution_id provided
119
+ if execution_id:
120
+ self.execution_filter = ExecutionContextFilter(execution_id)
121
+ self.log_handler.addFilter(self.execution_filter)
122
+
84
123
  self.logger = logging.getLogger("runtime")
85
124
  self.patched_loggers: set[str] = set()
86
125
 
@@ -95,22 +134,37 @@ class LogsInterceptor:
95
134
 
96
135
  def setup(self) -> None:
97
136
  """Configure logging to use our persistent handler."""
98
- # Use global disable to prevent all logging below our minimum level
99
- if self.numeric_min_level > logging.NOTSET:
137
+ # Set the context variable for this execution
138
+ if self.execution_id:
139
+ current_execution_id.set(self.execution_id)
140
+
141
+ # Only use global disable if we're not in a parallel execution context
142
+ if not self.execution_id and self.numeric_min_level > logging.NOTSET:
100
143
  logging.disable(self.numeric_min_level - 1)
101
144
 
102
145
  # Set root logger level
103
146
  self.root_logger.setLevel(self.numeric_min_level)
104
147
 
105
- # Remove ALL handlers from root logger and add only ours
106
- self._clean_all_handlers(self.root_logger)
148
+ if self.execution_id:
149
+ # Parallel execution mode: add our handler without removing others
150
+ if self.log_handler not in self.root_logger.handlers:
151
+ self.root_logger.addHandler(self.log_handler)
152
+
153
+ # Set up propagation for all existing loggers
154
+ for logger_name in logging.root.manager.loggerDict:
155
+ logger = logging.getLogger(logger_name)
156
+ # Keep propagation enabled so logs flow to all handlers
157
+ self.patched_loggers.add(logger_name)
158
+ else:
159
+ # Single execution mode: remove all handlers and add only ours
160
+ self._clean_all_handlers(self.root_logger)
107
161
 
108
- # Set up propagation for all existing loggers
109
- for logger_name in logging.root.manager.loggerDict:
110
- logger = logging.getLogger(logger_name)
111
- logger.propagate = False # Prevent double-logging
112
- self._clean_all_handlers(logger)
113
- self.patched_loggers.add(logger_name)
162
+ # Set up propagation for all existing loggers
163
+ for logger_name in logging.root.manager.loggerDict:
164
+ logger = logging.getLogger(logger_name)
165
+ logger.propagate = False # Prevent double-logging
166
+ self._clean_all_handlers(logger)
167
+ self.patched_loggers.add(logger_name)
114
168
 
115
169
  # Set up stdout/stderr redirection
116
170
  self._redirect_stdout_stderr()
@@ -130,7 +184,7 @@ class LogsInterceptor:
130
184
  self.level = level
131
185
  self.min_level = min_level
132
186
  self.buffer = ""
133
- self.sys_file = sys_file # Store reference to system stdout/stderr
187
+ self.sys_file = sys_file
134
188
 
135
189
  def write(self, message: str) -> None:
136
190
  self.buffer += message
@@ -138,7 +192,7 @@ class LogsInterceptor:
138
192
  line, self.buffer = self.buffer.split("\n", 1)
139
193
  # Only log if the message is not empty and the level is sufficient
140
194
  if line and self.level >= self.min_level:
141
- # Use _log to avoid potential recursive logging if logging methods are overridden
195
+ # The context variable is automatically available here
142
196
  self.logger._log(self.level, line, ())
143
197
 
144
198
  def flush(self) -> None:
@@ -160,14 +214,21 @@ class LogsInterceptor:
160
214
  def writable(self) -> bool:
161
215
  return True
162
216
 
163
- # Set up stdout and stderr loggers with propagate=False
217
+ # Set up stdout and stderr loggers
164
218
  stdout_logger = logging.getLogger("stdout")
165
- stdout_logger.propagate = False
166
- self._clean_all_handlers(stdout_logger)
167
-
168
219
  stderr_logger = logging.getLogger("stderr")
220
+
221
+ stdout_logger.propagate = False
169
222
  stderr_logger.propagate = False
170
- self._clean_all_handlers(stderr_logger)
223
+
224
+ if self.execution_id:
225
+ if self.log_handler not in stdout_logger.handlers:
226
+ stdout_logger.addHandler(self.log_handler)
227
+ if self.log_handler not in stderr_logger.handlers:
228
+ stderr_logger.addHandler(self.log_handler)
229
+ else:
230
+ self._clean_all_handlers(stdout_logger)
231
+ self._clean_all_handlers(stderr_logger)
171
232
 
172
233
  # Use the min_level in the LoggerWriter to filter messages
173
234
  sys.stdout = LoggerWriter(
@@ -179,21 +240,41 @@ class LogsInterceptor:
179
240
 
180
241
  def teardown(self) -> None:
181
242
  """Restore original logging configuration."""
182
- # Restore the original disable level
183
- logging.disable(self.original_disable_level)
184
-
185
- if self.log_handler in self.root_logger.handlers:
186
- self.root_logger.removeHandler(self.log_handler)
243
+ # Clear the context variable
244
+ if self.execution_id:
245
+ current_execution_id.set(None)
187
246
 
188
- for logger_name in self.patched_loggers:
189
- logger = logging.getLogger(logger_name)
190
- if self.log_handler in logger.handlers:
191
- logger.removeHandler(self.log_handler)
192
-
193
- self.root_logger.setLevel(self.original_level)
194
- for handler in self.original_handlers:
195
- if handler not in self.root_logger.handlers:
196
- self.root_logger.addHandler(handler)
247
+ # Restore the original disable level
248
+ if not self.execution_id:
249
+ logging.disable(self.original_disable_level)
250
+
251
+ # Remove our handler and filter
252
+ if self.execution_id:
253
+ if hasattr(self, "execution_filter"):
254
+ self.log_handler.removeFilter(self.execution_filter)
255
+ if self.log_handler in self.root_logger.handlers:
256
+ self.root_logger.removeHandler(self.log_handler)
257
+
258
+ # Remove from stdout/stderr loggers too
259
+ stdout_logger = logging.getLogger("stdout")
260
+ stderr_logger = logging.getLogger("stderr")
261
+ if self.log_handler in stdout_logger.handlers:
262
+ stdout_logger.removeHandler(self.log_handler)
263
+ if self.log_handler in stderr_logger.handlers:
264
+ stderr_logger.removeHandler(self.log_handler)
265
+ else:
266
+ if self.log_handler in self.root_logger.handlers:
267
+ self.root_logger.removeHandler(self.log_handler)
268
+
269
+ for logger_name in self.patched_loggers:
270
+ logger = logging.getLogger(logger_name)
271
+ if self.log_handler in logger.handlers:
272
+ logger.removeHandler(self.log_handler)
273
+
274
+ self.root_logger.setLevel(self.original_level)
275
+ for handler in self.original_handlers:
276
+ if handler not in self.root_logger.handlers:
277
+ self.root_logger.addHandler(handler)
197
278
 
198
279
  self.log_handler.close()
199
280
 
uipath/_cli/cli_pull.py CHANGED
@@ -112,7 +112,7 @@ async def download_folder_files(
112
112
  if local_hash != remote_hash:
113
113
  styled_path = click.style(str(file_path), fg="cyan")
114
114
  console.warning(f"File {styled_path}" + " differs from remote version.")
115
- response = click.prompt("Do you want to override it? (y/n)", type=str)
115
+ response = click.prompt("Do you want to overwrite it? (y/n)", type=str)
116
116
  if response.lower() == "y":
117
117
  with open(local_path, "w", encoding="utf-8", newline="\n") as f:
118
118
  f.write(remote_content)
@@ -106,35 +106,32 @@ def _cleanup_schema(model_class: type[BaseModel]) -> Dict[str, Any]:
106
106
  """
107
107
  schema = model_class.model_json_schema()
108
108
 
109
- def clean_properties(properties):
110
- """Clean property definitions by removing titles and cleaning nested items."""
111
- cleaned_props = {}
112
- for prop_name, prop_def in properties.items():
113
- if isinstance(prop_def, dict):
114
- cleaned_prop = {}
115
- for key, value in prop_def.items():
116
- if key == "title": # Skip title
117
- continue
118
- elif key == "items" and isinstance(value, dict):
119
- # Clean nested items
120
- cleaned_items = {}
121
- for item_key, item_value in value.items():
122
- if item_key != "title":
123
- cleaned_items[item_key] = item_value
124
- cleaned_prop[key] = cleaned_items
125
- else:
126
- cleaned_prop[key] = value
127
- cleaned_props[prop_name] = cleaned_prop
128
- return cleaned_props
109
+ def clean_type(type_def):
110
+ """Clean property definitions by removing titles and cleaning nested items. Additionally, `additionalProperties` is ensured on all objects."""
111
+ cleaned_type = {}
112
+ for key, value in type_def.items():
113
+ if key == "title" or key == "properties":
114
+ continue
115
+ else:
116
+ cleaned_type[key] = value
117
+ if type_def.get("type") == "object" and "additionalProperties" not in type_def:
118
+ cleaned_type["additionalProperties"] = False
119
+
120
+ if "properties" in type_def:
121
+ properties = type_def.get("properties", {})
122
+ for key, value in properties.items():
123
+ properties[key] = clean_type(value)
124
+ cleaned_type["properties"] = properties
125
+
126
+ if "$defs" in type_def:
127
+ cleaned_defs = {}
128
+ for key, value in type_def["$defs"].items():
129
+ cleaned_defs[key] = clean_type(value)
130
+ cleaned_type["$defs"] = cleaned_defs
131
+ return cleaned_type
129
132
 
130
133
  # Create clean schema
131
- clean_schema = {
132
- "type": "object",
133
- "properties": clean_properties(schema.get("properties", {})),
134
- "required": schema.get("required", []),
135
- "additionalProperties": False,
136
- }
137
-
134
+ clean_schema = clean_type(schema)
138
135
  return clean_schema
139
136
 
140
137
 
uipath/agent/_utils.py CHANGED
@@ -4,13 +4,17 @@ from pathlib import PurePath
4
4
  from httpx import Response
5
5
  from pydantic import TypeAdapter
6
6
 
7
+ from uipath._cli._evals._models._evaluation_set import LLMMockingStrategy
7
8
  from uipath._cli._utils._studio_project import (
8
9
  ProjectFile,
9
10
  ProjectFolder,
10
11
  StudioClient,
11
12
  resolve_path,
12
13
  )
13
- from uipath.agent.models.agent import AgentDefinition
14
+ from uipath.agent.models.agent import (
15
+ AgentDefinition,
16
+ UnknownAgentDefinition,
17
+ )
14
18
 
15
19
  logger = logging.getLogger(__name__)
16
20
 
@@ -23,7 +27,7 @@ async def get_file(
23
27
  return await studio_client.download_file_async(resolved.id)
24
28
 
25
29
 
26
- async def load_agent_definition(project_id: str):
30
+ async def load_agent_definition(project_id: str) -> AgentDefinition:
27
31
  studio_client = StudioClient(project_id=project_id)
28
32
  project_structure = await studio_client.get_project_structure_async()
29
33
 
@@ -31,6 +35,52 @@ async def load_agent_definition(project_id: str):
31
35
  await get_file(project_structure, PurePath("agent.json"), studio_client)
32
36
  ).json()
33
37
 
38
+ evaluators = []
39
+ try:
40
+ evaluators_path = resolve_path(
41
+ project_structure, PurePath("evals", "evaluators")
42
+ )
43
+ if isinstance(evaluators_path, ProjectFolder):
44
+ for file in evaluators_path.files:
45
+ evaluators.append(
46
+ (
47
+ await get_file(
48
+ evaluators_path, PurePath(file.name), studio_client
49
+ )
50
+ ).json()
51
+ )
52
+ else:
53
+ logger.warning(
54
+ "Unable to read evaluators from project. Defaulting to empty evaluators."
55
+ )
56
+ except Exception:
57
+ logger.warning(
58
+ "Unable to read evaluators from project. Defaulting to empty evaluators."
59
+ )
60
+
61
+ evaluation_sets = []
62
+ try:
63
+ evaluation_sets_path = resolve_path(
64
+ project_structure, PurePath("evals", "eval-sets")
65
+ )
66
+ if isinstance(evaluation_sets_path, ProjectFolder):
67
+ for file in evaluation_sets_path.files:
68
+ evaluation_sets.append(
69
+ (
70
+ await get_file(
71
+ evaluation_sets_path, PurePath(file.name), studio_client
72
+ )
73
+ ).json()
74
+ )
75
+ else:
76
+ logger.warning(
77
+ "Unable to read eval-sets from project. Defaulting to empty eval-sets."
78
+ )
79
+ except Exception:
80
+ logger.warning(
81
+ "Unable to read eval-sets from project. Defaulting to empty eval-sets."
82
+ )
83
+
34
84
  resolved_path = resolve_path(project_structure, PurePath("resources"))
35
85
  if isinstance(resolved_path, ProjectFolder):
36
86
  resource_folders = resolved_path.folders
@@ -50,6 +100,25 @@ async def load_agent_definition(project_id: str):
50
100
  "id": project_id,
51
101
  "name": project_structure.name,
52
102
  "resources": resources,
103
+ "evaluators": evaluators,
104
+ "evaluationSets": evaluation_sets,
53
105
  **agent,
54
106
  }
55
- return TypeAdapter(AgentDefinition).validate_python(agent_definition)
107
+ agent_definition = TypeAdapter(AgentDefinition).validate_python(agent_definition)
108
+ if agent_definition and isinstance(agent_definition, UnknownAgentDefinition):
109
+ if agent_definition.evaluation_sets:
110
+ for evaluation_set in agent_definition.evaluation_sets:
111
+ for evaluation in evaluation_set.evaluations:
112
+ if not evaluation.mocking_strategy:
113
+ # Migrate lowCode evaluation definitions
114
+ if evaluation.model_extra.get("simulateTools", False):
115
+ tools_to_simulate = evaluation.model_extra.get(
116
+ "toolsToSimulate", []
117
+ )
118
+ prompt = evaluation.model_extra.get(
119
+ "simulationInstructions", ""
120
+ )
121
+ evaluation.mocking_strategy = LLMMockingStrategy(
122
+ prompt=prompt, tools_to_simulate=tools_to_simulate
123
+ )
124
+ return agent_definition
@@ -5,6 +5,8 @@ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
5
5
 
6
6
  from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag
7
7
 
8
+ from uipath._cli._evals._models._evaluation_set import EvaluationSet
9
+ from uipath._cli._evals._models._evaluator import Evaluator
8
10
  from uipath.models import Connection
9
11
 
10
12
 
@@ -53,6 +55,7 @@ class AgentToolType(str, Enum):
53
55
 
54
56
  AGENT = "agent"
55
57
  INTEGRATION = "integration"
58
+ PROCESS = "process"
56
59
 
57
60
 
58
61
  class AgentToolSettings(BaseModel):
@@ -307,6 +310,14 @@ class BaseAgentDefinition(BaseModel):
307
310
  resources: List[AgentResourceConfig] = Field(
308
311
  ..., description="List of tools, context, and escalation resources"
309
312
  )
313
+ evaluation_sets: Optional[List[EvaluationSet]] = Field(
314
+ None,
315
+ alias="evaluationSets",
316
+ description="List of agent evaluation sets",
317
+ )
318
+ evaluators: Optional[List[Evaluator]] = Field(
319
+ None, description="List of agent evaluators"
320
+ )
310
321
 
311
322
  model_config = ConfigDict(
312
323
  validate_by_name=True, validate_by_alias=True, extra="allow"