uipath 2.1.43__py3-none-any.whl → 2.1.45__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/_evals/_runtime.py +18 -53
- uipath/_cli/_runtime/_contracts.py +89 -36
- uipath/_cli/_runtime/_runtime.py +26 -259
- uipath/_cli/_runtime/_script_executor.py +254 -0
- uipath/_cli/cli_dev.py +4 -2
- uipath/_cli/cli_eval.py +27 -33
- uipath/_cli/cli_run.py +43 -140
- uipath/_services/processes_service.py +2 -2
- uipath/models/errors.py +2 -2
- {uipath-2.1.43.dist-info → uipath-2.1.45.dist-info}/METADATA +1 -1
- {uipath-2.1.43.dist-info → uipath-2.1.45.dist-info}/RECORD +14 -13
- {uipath-2.1.43.dist-info → uipath-2.1.45.dist-info}/WHEEL +0 -0
- {uipath-2.1.43.dist-info → uipath-2.1.45.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.43.dist-info → uipath-2.1.45.dist-info}/licenses/LICENSE +0 -0
uipath/_cli/_evals/_runtime.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import copy
|
2
1
|
from collections import defaultdict
|
3
2
|
from time import time
|
4
3
|
from typing import Dict, Generic, List, Optional, Sequence, TypeVar
|
@@ -6,8 +5,6 @@ from typing import Dict, Generic, List, Optional, Sequence, TypeVar
|
|
6
5
|
from opentelemetry.sdk.trace import ReadableSpan
|
7
6
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
8
7
|
|
9
|
-
from uipath.eval._helpers import auto_discover_entrypoint
|
10
|
-
|
11
8
|
from .._runtime._contracts import (
|
12
9
|
UiPathBaseRuntime,
|
13
10
|
UiPathRuntimeContext,
|
@@ -54,58 +51,30 @@ class ExecutionSpanExporter(SpanExporter):
|
|
54
51
|
self.clear()
|
55
52
|
|
56
53
|
|
57
|
-
class UiPathEvalContext(UiPathRuntimeContext
|
54
|
+
class UiPathEvalContext(UiPathRuntimeContext):
|
58
55
|
"""Context used for evaluation runs."""
|
59
56
|
|
60
|
-
|
61
|
-
|
62
|
-
workers: int
|
57
|
+
no_report: Optional[bool] = False
|
58
|
+
workers: Optional[int] = 1
|
63
59
|
eval_set: Optional[str] = None
|
64
60
|
eval_ids: Optional[List[str]] = None
|
65
61
|
|
66
|
-
def __init__(
|
67
|
-
self,
|
68
|
-
runtime_context: C,
|
69
|
-
no_report: bool,
|
70
|
-
workers: int,
|
71
|
-
eval_set: Optional[str] = None,
|
72
|
-
eval_ids: Optional[List[str]] = None,
|
73
|
-
**kwargs,
|
74
|
-
):
|
75
|
-
super().__init__(
|
76
|
-
runtime_context=runtime_context, # type: ignore
|
77
|
-
no_report=no_report,
|
78
|
-
workers=workers,
|
79
|
-
eval_set=eval_set,
|
80
|
-
eval_ids=eval_ids,
|
81
|
-
**kwargs,
|
82
|
-
)
|
83
|
-
self._auto_discover()
|
84
|
-
|
85
|
-
def _auto_discover(self):
|
86
|
-
self.runtime_context.entrypoint = (
|
87
|
-
self.runtime_context.entrypoint or auto_discover_entrypoint()
|
88
|
-
)
|
89
|
-
self.eval_set = self.eval_set or EvalHelpers.auto_discover_eval_set()
|
90
|
-
|
91
62
|
|
92
63
|
class UiPathEvalRuntime(UiPathBaseRuntime, Generic[T, C]):
|
93
64
|
"""Specialized runtime for evaluation runs, with access to the factory."""
|
94
65
|
|
95
|
-
def __init__(
|
96
|
-
self, context: "UiPathEvalContext[C]", factory: "UiPathRuntimeFactory[T, C]"
|
97
|
-
):
|
66
|
+
def __init__(self, context: UiPathEvalContext, factory: UiPathRuntimeFactory[T, C]):
|
98
67
|
super().__init__(context)
|
99
|
-
self.context:
|
68
|
+
self.context: UiPathEvalContext = context
|
100
69
|
self.factory: UiPathRuntimeFactory[T, C] = factory
|
101
70
|
self.span_exporter: ExecutionSpanExporter = ExecutionSpanExporter()
|
102
71
|
self.factory.add_span_exporter(self.span_exporter)
|
103
72
|
|
104
73
|
@classmethod
|
105
|
-
def
|
74
|
+
def from_eval_context(
|
106
75
|
cls,
|
107
|
-
context:
|
108
|
-
factory:
|
76
|
+
context: UiPathEvalContext,
|
77
|
+
factory: UiPathRuntimeFactory[T, C],
|
109
78
|
) -> "UiPathEvalRuntime[T, C]":
|
110
79
|
return cls(context, factory)
|
111
80
|
|
@@ -119,30 +88,26 @@ class UiPathEvalRuntime(UiPathBaseRuntime, Generic[T, C]):
|
|
119
88
|
)
|
120
89
|
execution_output_list: list[UiPathEvalRunExecutionOutput] = []
|
121
90
|
for eval_item in evaluation_set.evaluations:
|
122
|
-
|
91
|
+
execution_output = await self.execute_runtime(eval_item)
|
92
|
+
execution_output_list.append(execution_output)
|
123
93
|
|
124
94
|
self.context.result = UiPathRuntimeResult(
|
125
95
|
output={
|
126
96
|
"results": execution_output_list,
|
127
97
|
},
|
128
98
|
status=UiPathRuntimeStatus.SUCCESSFUL,
|
129
|
-
resume=None,
|
130
99
|
)
|
131
100
|
|
132
|
-
return self.context.
|
133
|
-
|
134
|
-
def _prepare_new_runtime_context(self, eval_item: EvaluationItem) -> C:
|
135
|
-
runtime_context = copy.deepcopy(self.context.runtime_context)
|
136
|
-
runtime_context.execution_id = eval_item.id
|
137
|
-
runtime_context.input_json = eval_item.inputs
|
138
|
-
# here we can pass other values from eval_item: expectedAgentBehavior, simulationInstructions etc.
|
139
|
-
return runtime_context
|
101
|
+
return self.context.result
|
140
102
|
|
141
|
-
|
142
|
-
async def execute_agent(
|
103
|
+
async def execute_runtime(
|
143
104
|
self, eval_item: EvaluationItem
|
144
|
-
) ->
|
145
|
-
runtime_context = self.
|
105
|
+
) -> UiPathEvalRunExecutionOutput:
|
106
|
+
runtime_context: C = self.factory.new_context(
|
107
|
+
execution_id=eval_item.id,
|
108
|
+
input_json=eval_item.inputs,
|
109
|
+
is_eval_run=True,
|
110
|
+
)
|
146
111
|
start_time = time()
|
147
112
|
result = await self.factory.execute_in_root_span(
|
148
113
|
runtime_context, root_span=eval_item.name
|
@@ -30,6 +30,9 @@ from ._logging import LogsInterceptor
|
|
30
30
|
|
31
31
|
logger = logging.getLogger(__name__)
|
32
32
|
|
33
|
+
T = TypeVar("T", bound="UiPathBaseRuntime")
|
34
|
+
C = TypeVar("C", bound="UiPathRuntimeContext")
|
35
|
+
|
33
36
|
|
34
37
|
class UiPathResumeTriggerType(str, Enum):
|
35
38
|
"""Constants representing different types of resume job triggers in the system."""
|
@@ -315,21 +318,43 @@ class UiPathRuntimeContext(BaseModel):
|
|
315
318
|
model_config = {"arbitrary_types_allowed": True}
|
316
319
|
|
317
320
|
@classmethod
|
318
|
-
def
|
319
|
-
"""
|
321
|
+
def with_defaults(cls: type[C], config_path: Optional[str] = None, **kwargs) -> C:
|
322
|
+
"""Construct a context with defaults, reading env vars and config file."""
|
323
|
+
resolved_config_path = config_path or os.environ.get(
|
324
|
+
"UIPATH_CONFIG_PATH", "uipath.json"
|
325
|
+
)
|
320
326
|
|
321
|
-
|
322
|
-
|
323
|
-
|
327
|
+
base = cls.from_config(resolved_config_path)
|
328
|
+
|
329
|
+
# Apply defaults from env
|
330
|
+
base.job_id = os.environ.get("UIPATH_JOB_KEY")
|
331
|
+
base.trace_id = os.environ.get("UIPATH_TRACE_ID")
|
332
|
+
base.tracing_enabled = os.environ.get("UIPATH_TRACING_ENABLED", True)
|
333
|
+
base.logs_min_level = os.environ.get("LOG_LEVEL", "INFO")
|
334
|
+
|
335
|
+
base.trace_context = UiPathTraceContext(
|
336
|
+
trace_id=os.environ.get("UIPATH_TRACE_ID"),
|
337
|
+
parent_span_id=os.environ.get("UIPATH_PARENT_SPAN_ID"),
|
338
|
+
root_span_id=os.environ.get("UIPATH_ROOT_SPAN_ID"),
|
339
|
+
enabled=os.environ.get("UIPATH_TRACING_ENABLED", True),
|
340
|
+
job_id=os.environ.get("UIPATH_JOB_KEY"),
|
341
|
+
org_id=os.environ.get("UIPATH_ORGANIZATION_ID"),
|
342
|
+
tenant_id=os.environ.get("UIPATH_TENANT_ID"),
|
343
|
+
process_key=os.environ.get("UIPATH_PROCESS_UUID"),
|
344
|
+
folder_key=os.environ.get("UIPATH_FOLDER_KEY"),
|
345
|
+
reference_id=os.environ.get("UIPATH_JOB_KEY") or str(uuid4()),
|
346
|
+
)
|
324
347
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
import json
|
329
|
-
import os
|
348
|
+
# Override with kwargs
|
349
|
+
for k, v in kwargs.items():
|
350
|
+
setattr(base, k, v)
|
330
351
|
|
331
|
-
|
352
|
+
return base
|
332
353
|
|
354
|
+
@classmethod
|
355
|
+
def from_config(cls: type[C], config_path: Optional[str] = None, **kwargs) -> C:
|
356
|
+
"""Load configuration from uipath.json file."""
|
357
|
+
path = config_path or "uipath.json"
|
333
358
|
config = {}
|
334
359
|
|
335
360
|
if os.path.exists(path):
|
@@ -346,7 +371,6 @@ class UiPathRuntimeContext(BaseModel):
|
|
346
371
|
}
|
347
372
|
|
348
373
|
attributes_set = set()
|
349
|
-
# set values from config file if available
|
350
374
|
if "runtime" in config:
|
351
375
|
runtime_config = config["runtime"]
|
352
376
|
for config_key, attr_name in mapping.items():
|
@@ -354,10 +378,8 @@ class UiPathRuntimeContext(BaseModel):
|
|
354
378
|
attributes_set.add(attr_name)
|
355
379
|
setattr(instance, attr_name, runtime_config[config_key])
|
356
380
|
|
357
|
-
# fallback to kwargs for any values not set from config file
|
358
381
|
for _, attr_name in mapping.items():
|
359
382
|
if attr_name in kwargs and hasattr(instance, attr_name):
|
360
|
-
# Only set from kwargs if not already set from config file
|
361
383
|
if attr_name not in attributes_set:
|
362
384
|
setattr(instance, attr_name, kwargs[attr_name])
|
363
385
|
|
@@ -383,7 +405,7 @@ class UiPathRuntimeError(Exception):
|
|
383
405
|
if (
|
384
406
|
tb and tb.strip() != "NoneType: None"
|
385
407
|
): # Ensure there's an actual traceback
|
386
|
-
detail = f"{detail}\n\
|
408
|
+
detail = f"{detail}\n\n{tb}"
|
387
409
|
|
388
410
|
if status is None:
|
389
411
|
status = self._extract_http_status()
|
@@ -473,6 +495,21 @@ class UiPathBaseRuntime(ABC):
|
|
473
495
|
with open(self.context.input_file) as f:
|
474
496
|
self.context.input = f.read()
|
475
497
|
|
498
|
+
try:
|
499
|
+
if self.context.input:
|
500
|
+
self.context.input_json = json.loads(self.context.input)
|
501
|
+
if self.context.input_json is None:
|
502
|
+
self.context.input_json = {}
|
503
|
+
except json.JSONDecodeError as e:
|
504
|
+
raise UiPathRuntimeError(
|
505
|
+
"INPUT_INVALID_JSON",
|
506
|
+
"Invalid JSON input",
|
507
|
+
f"The input data is not valid JSON: {str(e)}",
|
508
|
+
UiPathErrorCategory.USER,
|
509
|
+
) from e
|
510
|
+
|
511
|
+
await self.validate()
|
512
|
+
|
476
513
|
# Intercept all stdout/stderr/logs and write them to a file (runtime/evals), stdout (debug)
|
477
514
|
self.logs_interceptor = LogsInterceptor(
|
478
515
|
min_level=self.context.logs_min_level,
|
@@ -620,14 +657,16 @@ class UiPathBaseRuntime(ABC):
|
|
620
657
|
return os.path.join("__uipath", "state.db")
|
621
658
|
|
622
659
|
|
623
|
-
T = TypeVar("T", bound=UiPathBaseRuntime)
|
624
|
-
C = TypeVar("C", bound=UiPathRuntimeContext)
|
625
|
-
|
626
|
-
|
627
660
|
class UiPathRuntimeFactory(Generic[T, C]):
|
628
661
|
"""Generic factory for UiPath runtime classes."""
|
629
662
|
|
630
|
-
def __init__(
|
663
|
+
def __init__(
|
664
|
+
self,
|
665
|
+
runtime_class: Type[T],
|
666
|
+
context_class: Type[C],
|
667
|
+
runtime_generator: Optional[Callable[[C], T]] = None,
|
668
|
+
context_generator: Optional[Callable[..., C]] = None,
|
669
|
+
):
|
631
670
|
if not issubclass(runtime_class, UiPathBaseRuntime):
|
632
671
|
raise TypeError(
|
633
672
|
f"runtime_class {runtime_class.__name__} must inherit from UiPathBaseRuntime"
|
@@ -640,10 +679,11 @@ class UiPathRuntimeFactory(Generic[T, C]):
|
|
640
679
|
|
641
680
|
self.runtime_class = runtime_class
|
642
681
|
self.context_class = context_class
|
682
|
+
self.runtime_generator = runtime_generator
|
683
|
+
self.context_generator = context_generator
|
643
684
|
self.tracer_provider: TracerProvider = TracerProvider()
|
644
685
|
self.tracer_span_processors: List[SpanProcessor] = []
|
645
686
|
trace.set_tracer_provider(self.tracer_provider)
|
646
|
-
|
647
687
|
if os.getenv("UIPATH_JOB_KEY"):
|
648
688
|
self.add_span_exporter(LlmOpsHttpExporter())
|
649
689
|
|
@@ -674,36 +714,49 @@ class UiPathRuntimeFactory(Generic[T, C]):
|
|
674
714
|
|
675
715
|
def new_context(self, **kwargs) -> C:
|
676
716
|
"""Create a new context instance."""
|
717
|
+
if self.context_generator:
|
718
|
+
return self.context_generator(**kwargs)
|
677
719
|
return self.context_class(**kwargs)
|
678
720
|
|
721
|
+
def new_runtime(self) -> T:
|
722
|
+
"""Create a new runtime instance."""
|
723
|
+
context = self.new_context()
|
724
|
+
if self.runtime_generator:
|
725
|
+
return self.runtime_generator(context)
|
726
|
+
return self.runtime_class.from_context(context)
|
727
|
+
|
679
728
|
def from_context(self, context: C) -> T:
|
680
729
|
"""Create runtime instance from context."""
|
730
|
+
if self.runtime_generator:
|
731
|
+
return self.runtime_generator(context)
|
681
732
|
return self.runtime_class.from_context(context)
|
682
733
|
|
683
734
|
async def execute(self, context: C) -> Optional[UiPathRuntimeResult]:
|
684
735
|
"""Execute runtime with context."""
|
685
736
|
async with self.from_context(context) as runtime:
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
737
|
+
try:
|
738
|
+
return await runtime.execute()
|
739
|
+
finally:
|
740
|
+
for span_processor in self.tracer_span_processors:
|
741
|
+
span_processor.force_flush()
|
690
742
|
|
691
743
|
async def execute_in_root_span(
|
692
744
|
self, context: C, root_span: str = "root"
|
693
745
|
) -> Optional[UiPathRuntimeResult]:
|
694
746
|
"""Execute runtime with context."""
|
695
747
|
async with self.from_context(context) as runtime:
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
748
|
+
try:
|
749
|
+
tracer: Tracer = trace.get_tracer("uipath-runtime")
|
750
|
+
with tracer.start_as_current_span(
|
751
|
+
root_span,
|
752
|
+
attributes={"execution.id": context.execution_id}
|
753
|
+
if context.execution_id
|
754
|
+
else {},
|
755
|
+
):
|
756
|
+
return await runtime.execute()
|
757
|
+
finally:
|
758
|
+
for span_processor in self.tracer_span_processors:
|
759
|
+
span_processor.force_flush()
|
707
760
|
|
708
761
|
|
709
762
|
class UiPathExecutionTraceProcessorMixin:
|
uipath/_cli/_runtime/_runtime.py
CHANGED
@@ -1,30 +1,37 @@
|
|
1
1
|
"""Python script runtime implementation for executing and managing python scripts."""
|
2
2
|
|
3
|
-
import importlib.util
|
4
|
-
import inspect
|
5
|
-
import json
|
6
3
|
import logging
|
7
|
-
import
|
8
|
-
from dataclasses import asdict, is_dataclass
|
9
|
-
from typing import Any, Dict, Optional, Type, TypeVar, cast, get_type_hints
|
10
|
-
|
11
|
-
from pydantic import BaseModel
|
4
|
+
from typing import Any, Awaitable, Callable, Optional, TypeVar
|
12
5
|
|
13
6
|
from ._contracts import (
|
14
7
|
UiPathBaseRuntime,
|
15
8
|
UiPathErrorCategory,
|
9
|
+
UiPathRuntimeContext,
|
16
10
|
UiPathRuntimeError,
|
17
11
|
UiPathRuntimeResult,
|
18
12
|
UiPathRuntimeStatus,
|
19
13
|
)
|
14
|
+
from ._script_executor import ScriptExecutor
|
20
15
|
|
21
16
|
logger = logging.getLogger(__name__)
|
22
17
|
|
23
18
|
T = TypeVar("T")
|
19
|
+
R = TypeVar("R")
|
20
|
+
AsyncFunc = Callable[[T], Awaitable[R]]
|
24
21
|
|
25
22
|
|
26
23
|
class UiPathRuntime(UiPathBaseRuntime):
|
27
|
-
|
24
|
+
def __init__(self, context: UiPathRuntimeContext, executor: AsyncFunc[Any, Any]):
|
25
|
+
self.context = context
|
26
|
+
self.executor = executor
|
27
|
+
|
28
|
+
async def cleanup(self) -> None:
|
29
|
+
"""Cleanup runtime resources."""
|
30
|
+
pass
|
31
|
+
|
32
|
+
async def validate(self):
|
33
|
+
"""Validate runtime context."""
|
34
|
+
pass
|
28
35
|
|
29
36
|
async def execute(self) -> Optional[UiPathRuntimeResult]:
|
30
37
|
"""Execute the Python script with the provided input and configuration.
|
@@ -35,15 +42,8 @@ class UiPathRuntime(UiPathBaseRuntime):
|
|
35
42
|
Raises:
|
36
43
|
UiPathRuntimeError: If execution fails
|
37
44
|
"""
|
38
|
-
await self.validate()
|
39
|
-
|
40
45
|
try:
|
41
|
-
|
42
|
-
return None
|
43
|
-
|
44
|
-
script_result = await self._execute_python_script(
|
45
|
-
self.context.entrypoint, self.context.input_json
|
46
|
-
)
|
46
|
+
script_result = await self.executor(self.context.input_json)
|
47
47
|
|
48
48
|
if self.context.job_id is None:
|
49
49
|
logger.info(script_result)
|
@@ -65,248 +65,15 @@ class UiPathRuntime(UiPathBaseRuntime):
|
|
65
65
|
UiPathErrorCategory.SYSTEM,
|
66
66
|
) from e
|
67
67
|
|
68
|
-
async def validate(self) -> None:
|
69
|
-
"""Validate runtime inputs."""
|
70
|
-
if not self.context.entrypoint:
|
71
|
-
raise UiPathRuntimeError(
|
72
|
-
"ENTRYPOINT_MISSING",
|
73
|
-
"No entrypoint specified",
|
74
|
-
"Please provide a path to a Python script.",
|
75
|
-
UiPathErrorCategory.USER,
|
76
|
-
)
|
77
|
-
|
78
|
-
if not os.path.exists(self.context.entrypoint):
|
79
|
-
raise UiPathRuntimeError(
|
80
|
-
"ENTRYPOINT_NOT_FOUND",
|
81
|
-
"Script not found",
|
82
|
-
f"Script not found at path {self.context.entrypoint}.",
|
83
|
-
UiPathErrorCategory.USER,
|
84
|
-
)
|
85
|
-
|
86
|
-
try:
|
87
|
-
if self.context.input:
|
88
|
-
self.context.input_json = json.loads(self.context.input)
|
89
|
-
if self.context.input_json is None:
|
90
|
-
self.context.input_json = {}
|
91
|
-
except json.JSONDecodeError as e:
|
92
|
-
raise UiPathRuntimeError(
|
93
|
-
"INPUT_INVALID_JSON",
|
94
|
-
"Invalid JSON input",
|
95
|
-
f"The input data is not valid JSON: {str(e)}",
|
96
|
-
UiPathErrorCategory.USER,
|
97
|
-
) from e
|
98
|
-
|
99
|
-
async def cleanup(self) -> None:
|
100
|
-
"""Cleanup runtime resources."""
|
101
|
-
pass
|
102
|
-
|
103
|
-
async def _execute_python_script(self, script_path: str, input_data: Any) -> Any:
|
104
|
-
"""Execute the Python script with the given input."""
|
105
|
-
spec = importlib.util.spec_from_file_location("dynamic_module", script_path)
|
106
|
-
if not spec or not spec.loader:
|
107
|
-
raise UiPathRuntimeError(
|
108
|
-
"IMPORT_ERROR",
|
109
|
-
"Module import failed",
|
110
|
-
f"Could not load spec for {script_path}",
|
111
|
-
UiPathErrorCategory.USER,
|
112
|
-
)
|
113
|
-
|
114
|
-
module = importlib.util.module_from_spec(spec)
|
115
|
-
try:
|
116
|
-
spec.loader.exec_module(module)
|
117
|
-
except Exception as e:
|
118
|
-
raise UiPathRuntimeError(
|
119
|
-
"MODULE_EXECUTION_ERROR",
|
120
|
-
"Module execution failed",
|
121
|
-
f"Error executing module: {str(e)}",
|
122
|
-
UiPathErrorCategory.USER,
|
123
|
-
) from e
|
124
|
-
|
125
|
-
for func_name in ["main", "run", "execute"]:
|
126
|
-
if hasattr(module, func_name):
|
127
|
-
main_func = getattr(module, func_name)
|
128
|
-
sig = inspect.signature(main_func)
|
129
|
-
params = list(sig.parameters.values())
|
130
|
-
|
131
|
-
# Check if the function is asynchronous
|
132
|
-
is_async = inspect.iscoroutinefunction(main_func)
|
133
|
-
|
134
|
-
# Case 1: No parameters
|
135
|
-
if not params:
|
136
|
-
try:
|
137
|
-
result = await main_func() if is_async else main_func()
|
138
|
-
return (
|
139
|
-
self._convert_from_class(result)
|
140
|
-
if result is not None
|
141
|
-
else {}
|
142
|
-
)
|
143
|
-
except Exception as e:
|
144
|
-
raise UiPathRuntimeError(
|
145
|
-
"FUNCTION_EXECUTION_ERROR",
|
146
|
-
f"Error executing {func_name} function",
|
147
|
-
f"Error: {str(e)}",
|
148
|
-
UiPathErrorCategory.USER,
|
149
|
-
) from e
|
150
|
-
|
151
|
-
input_param = params[0]
|
152
|
-
input_type = input_param.annotation
|
153
|
-
|
154
|
-
# Case 2: Class, dataclass, or Pydantic model parameter
|
155
|
-
if input_type != inspect.Parameter.empty and (
|
156
|
-
is_dataclass(input_type)
|
157
|
-
or self._is_pydantic_model(input_type)
|
158
|
-
or hasattr(input_type, "__annotations__")
|
159
|
-
):
|
160
|
-
try:
|
161
|
-
valid_type = cast(Type[Any], input_type)
|
162
|
-
typed_input = self._convert_to_class(input_data, valid_type)
|
163
|
-
result = (
|
164
|
-
await main_func(typed_input)
|
165
|
-
if is_async
|
166
|
-
else main_func(typed_input)
|
167
|
-
)
|
168
|
-
return (
|
169
|
-
self._convert_from_class(result)
|
170
|
-
if result is not None
|
171
|
-
else {}
|
172
|
-
)
|
173
|
-
except Exception as e:
|
174
|
-
raise UiPathRuntimeError(
|
175
|
-
"FUNCTION_EXECUTION_ERROR",
|
176
|
-
f"Error executing {func_name} function with typed input",
|
177
|
-
f"Error: {str(e)}",
|
178
|
-
UiPathErrorCategory.USER,
|
179
|
-
) from e
|
180
|
-
|
181
|
-
# Case 3: Dict parameter
|
182
|
-
else:
|
183
|
-
try:
|
184
|
-
result = (
|
185
|
-
await main_func(input_data)
|
186
|
-
if is_async
|
187
|
-
else main_func(input_data)
|
188
|
-
)
|
189
|
-
return (
|
190
|
-
self._convert_from_class(result)
|
191
|
-
if result is not None
|
192
|
-
else {}
|
193
|
-
)
|
194
|
-
except Exception as e:
|
195
|
-
raise UiPathRuntimeError(
|
196
|
-
"FUNCTION_EXECUTION_ERROR",
|
197
|
-
f"Error executing {func_name} function with dictionary input",
|
198
|
-
f"Error: {str(e)}",
|
199
|
-
UiPathErrorCategory.USER,
|
200
|
-
) from e
|
201
|
-
|
202
|
-
raise UiPathRuntimeError(
|
203
|
-
"ENTRYPOINT_FUNCTION_MISSING",
|
204
|
-
"No entry function found",
|
205
|
-
f"No main function (main, run, or execute) found in {script_path}",
|
206
|
-
UiPathErrorCategory.USER,
|
207
|
-
)
|
208
|
-
|
209
|
-
def _convert_to_class(self, data: Dict[str, Any], cls: Type[T]) -> T:
|
210
|
-
"""Convert a dictionary to either a dataclass, Pydantic model, or regular class instance."""
|
211
|
-
# Handle Pydantic models
|
212
|
-
try:
|
213
|
-
if inspect.isclass(cls) and issubclass(cls, BaseModel):
|
214
|
-
return cls.model_validate(data)
|
215
|
-
except TypeError:
|
216
|
-
# issubclass can raise TypeError if cls is not a class
|
217
|
-
pass
|
218
|
-
|
219
|
-
# Handle dataclasses
|
220
|
-
if is_dataclass(cls):
|
221
|
-
field_types = get_type_hints(cls)
|
222
|
-
converted_data = {}
|
223
68
|
|
224
|
-
|
225
|
-
|
226
|
-
continue
|
227
|
-
|
228
|
-
value = data[field_name]
|
229
|
-
if (
|
230
|
-
is_dataclass(field_type)
|
231
|
-
or self._is_pydantic_model(field_type)
|
232
|
-
or hasattr(field_type, "__annotations__")
|
233
|
-
) and isinstance(value, dict):
|
234
|
-
typed_field = cast(Type[Any], field_type)
|
235
|
-
value = self._convert_to_class(value, typed_field)
|
236
|
-
converted_data[field_name] = value
|
237
|
-
|
238
|
-
return cls(**converted_data)
|
239
|
-
|
240
|
-
# Handle regular classes
|
241
|
-
else:
|
242
|
-
sig = inspect.signature(cls.__init__)
|
243
|
-
params = sig.parameters
|
244
|
-
|
245
|
-
init_args = {}
|
246
|
-
|
247
|
-
for param_name, param in params.items():
|
248
|
-
if param_name == "self":
|
249
|
-
continue
|
250
|
-
|
251
|
-
if param_name in data:
|
252
|
-
value = data[param_name]
|
253
|
-
param_type = (
|
254
|
-
param.annotation
|
255
|
-
if param.annotation != inspect.Parameter.empty
|
256
|
-
else Any
|
257
|
-
)
|
258
|
-
|
259
|
-
if (
|
260
|
-
is_dataclass(param_type)
|
261
|
-
or self._is_pydantic_model(param_type)
|
262
|
-
or hasattr(param_type, "__annotations__")
|
263
|
-
) and isinstance(value, dict):
|
264
|
-
typed_param = cast(Type[Any], param_type)
|
265
|
-
value = self._convert_to_class(value, typed_param)
|
266
|
-
|
267
|
-
init_args[param_name] = value
|
268
|
-
elif param.default != inspect.Parameter.empty:
|
269
|
-
init_args[param_name] = param.default
|
270
|
-
|
271
|
-
return cls(**init_args)
|
272
|
-
|
273
|
-
def _is_pydantic_model(self, cls: Type[Any]) -> bool:
|
274
|
-
"""Safely check if a class is a Pydantic model."""
|
275
|
-
try:
|
276
|
-
return inspect.isclass(cls) and issubclass(cls, BaseModel)
|
277
|
-
except TypeError:
|
278
|
-
# issubclass can raise TypeError if cls is not a class
|
279
|
-
return False
|
280
|
-
|
281
|
-
def _convert_from_class(self, obj: Any) -> Dict[str, Any]:
|
282
|
-
"""Convert a class instance (dataclass, Pydantic model, or regular) to a dictionary."""
|
283
|
-
if obj is None:
|
284
|
-
return {}
|
285
|
-
|
286
|
-
# Handle Pydantic models
|
287
|
-
if isinstance(obj, BaseModel):
|
288
|
-
return obj.model_dump()
|
69
|
+
class UiPathScriptRuntime(UiPathRuntime):
|
70
|
+
"""Runtime for executing Python scripts."""
|
289
71
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
if isinstance(obj, type):
|
294
|
-
return {}
|
295
|
-
return asdict(obj)
|
72
|
+
def __init__(self, context: UiPathRuntimeContext, entrypoint: str):
|
73
|
+
executor = ScriptExecutor(entrypoint)
|
74
|
+
super().__init__(context, executor)
|
296
75
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
# Skip private attributes
|
302
|
-
if not key.startswith("_"):
|
303
|
-
if (
|
304
|
-
isinstance(value, BaseModel)
|
305
|
-
or hasattr(value, "__dict__")
|
306
|
-
or is_dataclass(value)
|
307
|
-
):
|
308
|
-
result[key] = self._convert_from_class(value)
|
309
|
-
else:
|
310
|
-
result[key] = value
|
311
|
-
return result
|
312
|
-
return {} if obj is None else {str(type(obj).__name__): str(obj)} # Fallback
|
76
|
+
@classmethod
|
77
|
+
def from_context(cls, context: UiPathRuntimeContext):
|
78
|
+
"""Create runtime instance from context."""
|
79
|
+
return UiPathScriptRuntime(context, context.entrypoint or "")
|