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.
- uipath/_cli/_dev/_terminal/_components/_history.py +25 -5
- uipath/_cli/_evals/_evaluator_factory.py +35 -73
- uipath/_cli/_evals/_models/_evaluation_set.py +127 -18
- uipath/_cli/_evals/_models/_evaluator.py +106 -0
- uipath/_cli/_evals/_runtime.py +2 -0
- uipath/_cli/_evals/mocks/__init__.py +1 -0
- uipath/_cli/_evals/mocks/llm_mocker.py +153 -0
- uipath/_cli/_evals/mocks/mocker.py +29 -0
- uipath/_cli/_evals/mocks/mocker_factory.py +25 -0
- uipath/_cli/_evals/mocks/mockito_mocker.py +62 -0
- uipath/_cli/_evals/mocks/mocks.py +136 -0
- uipath/_cli/_runtime/_contracts.py +1 -0
- uipath/_cli/_runtime/_logging.py +112 -31
- uipath/_cli/cli_pull.py +1 -1
- uipath/_services/llm_gateway_service.py +24 -27
- uipath/agent/_utils.py +72 -3
- uipath/agent/models/agent.py +11 -0
- {uipath-2.1.71.dist-info → uipath-2.1.73.dist-info}/METADATA +4 -1
- {uipath-2.1.71.dist-info → uipath-2.1.73.dist-info}/RECORD +22 -15
- {uipath-2.1.71.dist-info → uipath-2.1.73.dist-info}/WHEEL +0 -0
- {uipath-2.1.71.dist-info → uipath-2.1.73.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.71.dist-info → uipath-2.1.73.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
)
|
uipath/_cli/_runtime/_logging.py
CHANGED
@@ -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
|
-
#
|
99
|
-
if self.
|
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
|
-
|
106
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
#
|
183
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
self.
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
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
|
110
|
-
"""Clean property definitions by removing titles and cleaning nested items."""
|
111
|
-
|
112
|
-
for
|
113
|
-
if
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
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
|
-
|
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
|
uipath/agent/models/agent.py
CHANGED
@@ -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"
|