Skip to content

Commit 85056ab

Browse files
authored
Merge pull request #738 from UiPath/akshaya/update_contracts
feat(UpdateContract): add a runtime generator
2 parents da02d07 + 3bc115a commit 85056ab

File tree

9 files changed

+168
-111
lines changed

9 files changed

+168
-111
lines changed

src/uipath/_cli/_evals/_runtime.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,12 @@ def _get_and_clear_execution_data(
451451
async def execute_runtime(
452452
self, eval_item: EvaluationItem, execution_id: str
453453
) -> UiPathEvalRunExecutionOutput:
454-
runtime_context: C = self.factory.new_context(
455-
execution_id=execution_id,
456-
input_json=eval_item.inputs,
457-
is_eval_run=True,
458-
log_handler=self._setup_execution_logging(execution_id),
459-
)
454+
context_args = self.context.model_dump()
455+
context_args["execution_id"] = execution_id
456+
context_args["input_json"] = eval_item.inputs
457+
context_args["is_eval_run"] = True
458+
context_args["log_handler"] = self._setup_execution_logging(execution_id)
459+
runtime_context: C = self.factory.new_context(**context_args)
460460
if runtime_context.execution_id is None:
461461
raise ValueError("execution_id must be set for eval runs")
462462

src/uipath/_cli/_runtime/_contracts.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from uipath.agent.conversation import UiPathConversationEvent, UiPathConversationMessage
4040
from uipath.tracing import TracingManager
4141

42+
from ..models.runtime_schema import BindingResource, Entrypoint
4243
from ._logging import LogsInterceptor
4344

4445
logger = logging.getLogger(__name__)
@@ -516,6 +517,22 @@ def from_context(cls, context: UiPathRuntimeContext):
516517
runtime = cls(context)
517518
return runtime
518519

520+
@property
521+
def get_binding_resources(self) -> List[BindingResource]:
522+
"""Get binding resources for this runtime.
523+
524+
Returns: A list of binding resources.
525+
"""
526+
raise NotImplementedError()
527+
528+
@property
529+
def get_entrypoint(self) -> Entrypoint:
530+
"""Get entrypoint for this runtime.
531+
532+
Returns: A entrypoint for this runtime.
533+
"""
534+
raise NotImplementedError()
535+
519536
async def __aenter__(self):
520537
"""Async enter method called when entering the 'async with' block.
521538
@@ -806,9 +823,9 @@ def new_context(self, **kwargs) -> C:
806823
return self.context_generator(**kwargs)
807824
return self.context_class(**kwargs)
808825

809-
def new_runtime(self) -> T:
826+
def new_runtime(self, **kwargs) -> T:
810827
"""Create a new runtime instance."""
811-
context = self.new_context()
828+
context = self.new_context(**kwargs)
812829
if self.runtime_generator:
813830
return self.runtime_generator(context)
814831
return self.runtime_class.from_context(context)

src/uipath/_cli/_runtime/_runtime.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
"""Python script runtime implementation for executing and managing python scripts."""
22

33
import logging
4-
from typing import Any, Awaitable, Callable, Optional, TypeVar
5-
4+
import os
5+
import uuid
6+
from functools import cached_property
7+
from pathlib import Path
8+
from typing import Any, Awaitable, Callable, List, Optional, TypeVar
9+
10+
from typing_extensions import override
11+
12+
from .._utils._console import ConsoleLogger
13+
from .._utils._input_args import generate_args
14+
from .._utils._parse_ast import generate_bindings # type: ignore[attr-defined]
15+
from ..models.runtime_schema import BindingResource, Entrypoint
616
from ._contracts import (
717
UiPathBaseRuntime,
818
UiPathErrorCategory,
@@ -63,6 +73,36 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
6373
) from e
6474

6575

76+
console = ConsoleLogger()
77+
78+
79+
def get_user_script(directory: str, entrypoint: Optional[str] = None) -> Optional[str]:
80+
"""Find the Python script to process."""
81+
if entrypoint:
82+
script_path = os.path.join(directory, entrypoint)
83+
if not os.path.isfile(script_path):
84+
console.error(
85+
f"The {entrypoint} file does not exist in the current directory."
86+
)
87+
return None
88+
return script_path
89+
90+
python_files = [f for f in os.listdir(directory) if f.endswith(".py")]
91+
92+
if not python_files:
93+
console.error(
94+
"No python files found in the current directory.\nPlease specify the entrypoint: `uipath init <entrypoint_path>`"
95+
)
96+
return None
97+
elif len(python_files) == 1:
98+
return os.path.join(directory, python_files[0])
99+
else:
100+
console.error(
101+
"Multiple python files found in the current directory.\nPlease specify the entrypoint: `uipath init <entrypoint_path>`"
102+
)
103+
return None
104+
105+
66106
class UiPathScriptRuntime(UiPathRuntime):
67107
"""Runtime for executing Python scripts."""
68108

@@ -74,3 +114,32 @@ def __init__(self, context: UiPathRuntimeContext, entrypoint: str):
74114
def from_context(cls, context: UiPathRuntimeContext):
75115
"""Create runtime instance from context."""
76116
return UiPathScriptRuntime(context, context.entrypoint or "")
117+
118+
@cached_property
119+
@override
120+
def get_binding_resources(self) -> List[BindingResource]:
121+
"""Get binding resources for script runtime.
122+
123+
Returns: A list of binding resources.
124+
"""
125+
working_dir = self.context.runtime_dir or os.getcwd()
126+
script_path = get_user_script(working_dir, entrypoint=self.context.entrypoint)
127+
bindings = generate_bindings(script_path)
128+
return bindings.resources
129+
130+
@cached_property
131+
@override
132+
def get_entrypoint(self) -> Entrypoint:
133+
working_dir = self.context.runtime_dir or os.getcwd()
134+
script_path = get_user_script(working_dir, entrypoint=self.context.entrypoint)
135+
if not script_path:
136+
raise ValueError("Entrypoint not found.")
137+
relative_path = Path(script_path).relative_to(working_dir).as_posix()
138+
args = generate_args(script_path)
139+
return Entrypoint(
140+
file_path=relative_path, # type: ignore[call-arg] # This exists
141+
unique_id=str(uuid.uuid4()),
142+
type="agent",
143+
input=args["input"],
144+
output=args["output"],
145+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from uipath._cli._runtime._contracts import (
2+
UiPathBaseRuntime,
3+
UiPathRuntimeContext,
4+
UiPathRuntimeFactory,
5+
)
6+
from uipath._cli._runtime._runtime import UiPathScriptRuntime
7+
8+
9+
def generate_runtime_factory() -> UiPathRuntimeFactory[
10+
UiPathBaseRuntime, UiPathRuntimeContext
11+
]:
12+
runtime_factory: UiPathRuntimeFactory[UiPathBaseRuntime, UiPathRuntimeContext] = (
13+
UiPathRuntimeFactory(
14+
UiPathScriptRuntime,
15+
UiPathRuntimeContext,
16+
context_generator=lambda **kwargs: UiPathRuntimeContext.with_defaults(
17+
**kwargs
18+
),
19+
)
20+
)
21+
return runtime_factory

src/uipath/_cli/cli_eval.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@
1212
UiPathEvalContext,
1313
UiPathEvalRuntime,
1414
)
15-
from uipath._cli._runtime._contracts import (
16-
UiPathRuntimeContext,
17-
UiPathRuntimeFactory,
18-
)
19-
from uipath._cli._runtime._runtime import UiPathScriptRuntime
15+
from uipath._cli._runtime._runtime_factory import generate_runtime_factory
2016
from uipath._cli._utils._constants import UIPATH_PROJECT_ID
2117
from uipath._cli._utils._folders import get_personal_workspace_key_async
2218
from uipath._cli.middlewares import Middlewares
@@ -96,6 +92,15 @@ def eval(
9692
workers: Number of parallel workers for running evaluations
9793
no_report: Do not report the evaluation results
9894
"""
95+
context_args = {
96+
"entrypoint": entrypoint or auto_discover_entrypoint(),
97+
"eval_set": eval_set,
98+
"eval_ids": eval_ids,
99+
"workers": workers,
100+
"no_report": no_report,
101+
"output_file": output_file,
102+
}
103+
99104
should_register_progress_reporter = setup_reporting_prereq(no_report)
100105

101106
result = Middlewares.next(
@@ -119,16 +124,9 @@ def eval(
119124
progress_reporter = StudioWebProgressReporter(LlmOpsHttpExporter())
120125
asyncio.run(progress_reporter.subscribe_to_eval_runtime_events(event_bus))
121126

122-
def generate_runtime_context(**context_kwargs) -> UiPathRuntimeContext:
123-
runtime_context = UiPathRuntimeContext.with_defaults(**context_kwargs)
124-
runtime_context.entrypoint = runtime_entrypoint
125-
return runtime_context
126-
127-
runtime_entrypoint = entrypoint or auto_discover_entrypoint()
128-
129127
eval_context = UiPathEvalContext.with_defaults(
130128
execution_output_file=output_file,
131-
entrypoint=runtime_entrypoint,
129+
entrypoint=context_args["entrypoint"],
132130
)
133131

134132
eval_context.no_report = no_report
@@ -140,11 +138,7 @@ def generate_runtime_context(**context_kwargs) -> UiPathRuntimeContext:
140138
asyncio.run(console_reporter.subscribe_to_eval_runtime_events(event_bus))
141139

142140
try:
143-
runtime_factory = UiPathRuntimeFactory(
144-
UiPathScriptRuntime,
145-
UiPathRuntimeContext,
146-
context_generator=generate_runtime_context,
147-
)
141+
runtime_factory = generate_runtime_factory()
148142
if eval_context.job_id:
149143
runtime_factory.add_span_exporter(LlmOpsHttpExporter())
150144

src/uipath/_cli/cli_init.py

Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@
55
import os
66
import shutil
77
import uuid
8-
from pathlib import Path
98
from typing import Any, Dict, Optional
109

1110
import click
1211

1312
from .._utils.constants import ENV_TELEMETRY_ENABLED
1413
from ..telemetry import track
1514
from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
15+
from ._runtime._runtime import get_user_script
16+
from ._runtime._runtime_factory import generate_runtime_factory
1617
from ._utils._console import ConsoleLogger
17-
from ._utils._input_args import generate_args
18-
from ._utils._parse_ast import generate_bindings
1918
from .middlewares import Middlewares
20-
from .models.runtime_schema import Bindings, Entrypoint, RuntimeSchema
19+
from .models.runtime_schema import Bindings, RuntimeSchema
2120

2221
console = ConsoleLogger()
2322
logger = logging.getLogger(__name__)
@@ -126,33 +125,6 @@ def get_existing_settings(config_path: str) -> Optional[Dict[str, Any]]:
126125
return None
127126

128127

129-
def get_user_script(directory: str, entrypoint: Optional[str] = None) -> Optional[str]:
130-
"""Find the Python script to process."""
131-
if entrypoint:
132-
script_path = os.path.join(directory, entrypoint)
133-
if not os.path.isfile(script_path):
134-
console.error(
135-
f"The {entrypoint} file does not exist in the current directory."
136-
)
137-
return None
138-
return script_path
139-
140-
python_files = [f for f in os.listdir(directory) if f.endswith(".py")]
141-
142-
if not python_files:
143-
console.error(
144-
"No python files found in the current directory.\nPlease specify the entrypoint: `uipath init <entrypoint_path>`"
145-
)
146-
return None
147-
elif len(python_files) == 1:
148-
return os.path.join(directory, python_files[0])
149-
else:
150-
console.error(
151-
"Multiple python files found in the current directory.\nPlease specify the entrypoint: `uipath init <entrypoint_path>`"
152-
)
153-
return None
154-
155-
156128
def write_config_file(config_data: Dict[str, Any] | RuntimeSchema) -> None:
157129
existing_settings = get_existing_settings(CONFIG_PATH)
158130
if existing_settings is not None:
@@ -205,39 +177,28 @@ def init(entrypoint: str, infer_bindings: bool) -> None:
205177

206178
generate_agent_md_files(current_directory)
207179
script_path = get_user_script(current_directory, entrypoint=entrypoint)
208-
209180
if not script_path:
210181
return
211182

212-
try:
213-
args = generate_args(script_path)
214-
215-
relative_path = Path(script_path).relative_to(current_directory).as_posix()
216-
bindings = None
217-
if infer_bindings:
218-
try:
219-
bindings = generate_bindings(script_path)
220-
except Exception as e:
221-
console.warning(f"Warning: Could not generate bindings: {str(e)}")
222-
if bindings is None:
183+
context_args = {
184+
"runtime_dir": os.getcwd(),
185+
"entrypoint": script_path,
186+
}
187+
188+
def initialize() -> None:
189+
try:
190+
runtime = generate_runtime_factory().new_runtime(**context_args)
223191
bindings = Bindings(
224192
version="2.0",
225-
resources=[],
193+
resources=runtime.get_binding_resources,
226194
)
227-
config_data = RuntimeSchema(
228-
entrypoints=[
229-
Entrypoint(
230-
file_path=relative_path,
231-
unique_id=str(uuid.uuid4()),
232-
type="agent",
233-
input=args["input"],
234-
output=args["output"],
235-
)
236-
],
237-
bindings=bindings,
238-
)
195+
config_data = RuntimeSchema(
196+
entryPoints=[runtime.get_entrypoint],
197+
bindings=bindings,
198+
)
199+
config_path = write_config_file(config_data)
200+
console.success(f"Created '{config_path}' file.")
201+
except Exception as e:
202+
console.error(f"Error creating configuration file:\n {str(e)}")
239203

240-
config_path = write_config_file(config_data)
241-
console.success(f"Created '{config_path}' file.")
242-
except Exception as e:
243-
console.error(f"Error creating configuration file:\n {str(e)}")
204+
initialize()

0 commit comments

Comments
 (0)