Skip to content

Commit adf1fca

Browse files
rakesh-uipathclaude
andcommitted
chore: merge main into feat/uipath-eval-package and resolve version conflict
Accept uipath-runtime>=0.10.1 bump from main while keeping uipath-eval dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents b8fbd1b + 28ad47c commit adf1fca

23 files changed

Lines changed: 1225 additions & 242 deletions

packages/uipath-platform/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-platform"
3-
version = "0.1.34"
3+
version = "0.1.35"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/resource_catalog/_resource_catalog_service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
1+
from typing import Any, AsyncGenerator, Dict, Iterator, List, Optional
22

33
from uipath.core.tracing import traced
44

@@ -110,7 +110,7 @@ async def search_async(
110110
resource_types: Optional[List[ResourceType]] = None,
111111
resource_sub_types: Optional[List[str]] = None,
112112
page_size: int = _DEFAULT_PAGE_SIZE,
113-
) -> AsyncIterator[Resource]:
113+
) -> AsyncGenerator[Resource, None]:
114114
"""Asynchronously search for tenant scoped resources and folder scoped resources (accessible to the user).
115115
116116
This method automatically handles pagination and yields resources one by one.
@@ -258,7 +258,7 @@ async def list_async(
258258
folder_path: Optional[str] = None,
259259
folder_key: Optional[str] = None,
260260
page_size: int = _DEFAULT_PAGE_SIZE,
261-
) -> AsyncIterator[Resource]:
261+
) -> AsyncGenerator[Resource, None]:
262262
"""Asynchronously get tenant scoped resources and folder scoped resources (accessible to the user).
263263
264264
If no folder identifier is provided (path or key) only tenant resources will be retrieved.
@@ -428,7 +428,7 @@ async def list_by_type_async(
428428
folder_path: Optional[str] = None,
429429
folder_key: Optional[str] = None,
430430
page_size: int = _DEFAULT_PAGE_SIZE,
431-
) -> AsyncIterator[Resource]:
431+
) -> AsyncGenerator[Resource, None]:
432432
"""Asynchronously get resources of a specific type (tenant scoped or folder scoped).
433433
434434
If no folder identifier is provided (path or key) only tenant resources will be retrieved.

packages/uipath-platform/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath/docs/cli/index.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,92 @@ Processing: uipath.json
308308
File 'uipath.json' is up to date
309309
✓ Project pulled successfully
310310
```
311+
---
312+
313+
::: mkdocs-click
314+
:module: uipath._cli
315+
:command: debug
316+
:depth: 1
317+
:style: table
318+
319+
Runs your agent under the debug runtime, with a debug bridge attached. Locally, the bridge is the interactive **console** (read commands from stdin, stop at breakpoints). In the cloud, the bridge is **SignalR** (driven by Studio Web / Orchestrator). The `--attach` flag lets you override that default, including `none` for executors that need the debug command's surrounding behavior (bindings fetch, state streaming) but cannot speak the interactive debug protocol.
320+
321+
### Attach modes
322+
323+
| Mode | When to use |
324+
|------|-------------|
325+
| `signalr` | Remote runs driven by Studio Web / Orchestrator. Default when `job_id` is set. |
326+
| `console` | Local interactive debugging from the terminal. Default when no `job_id`. |
327+
| `none` | Run under the debug command without attaching a debugger. No wait-for-start gate, no breakpoints, no step mode. |
328+
329+
/// info
330+
`--attach` selects the **debug bridge**. It's unrelated to `--debug`, which starts a `debugpy` server for Python-level breakpoints in your IDE. The two can be combined.
331+
///
332+
333+
<!-- termynal -->
334+
335+
```shell
336+
> uipath debug main '{"message": "test"}'
337+
Debug Mode Commands
338+
c, continue Continue until next breakpoint
339+
s, step Step to next node
340+
b <node> Set breakpoint at <node>
341+
l, list List all breakpoints
342+
r <node> Remove breakpoint at <node>
343+
h, help Show help
344+
q, quit Exit debugger
345+
▶ START
346+
> b analyze_sentiment
347+
✓ Breakpoint set at: analyze_sentiment
348+
> c
349+
────────────────────────────────────────
350+
■ BREAKPOINT analyze_sentiment (before)
351+
Next: analyze_sentiment
352+
────────────────────────────────────────
353+
> s
354+
● analyze_sentiment
355+
> c
356+
✓ Execution completed
357+
```
358+
---
359+
360+
::: mkdocs-click
361+
:module: uipath._cli
362+
:command: eval
363+
:depth: 1
364+
:style: table
365+
366+
Runs an evaluation set against your agent. Entry point and eval set are auto-discovered from the project if not passed explicitly. Evaluations run in parallel (see `--workers`) and, unless `--no-report` is passed, results are reported back to Studio Web when `UIPATH_PROJECT_ID` is set.
367+
368+
### Common flags
369+
370+
| Flag | Purpose |
371+
|------|---------|
372+
| `--eval-ids` | Run only a subset of evaluations by id. |
373+
| `--workers` | Parallel workers for running evaluations (default 1). |
374+
| `--no-report` | Skip reporting results back to UiPath. |
375+
| `--enable-mocker-cache` | Cache LLM mocker responses across runs. |
376+
| `--input-overrides` | Per-eval input overrides, merged into the eval's input. |
377+
| `--trace-file` | Write OpenTelemetry traces to a JSONL file for offline inspection. |
378+
| `--resume` | Resume evaluation from a previous suspended state. |
379+
380+
<!-- termynal -->
381+
382+
```shell
383+
> uipath eval
384+
⠋ Running evaluations ...
385+
Weather in Paris
386+
LLM Judge Output 0.7
387+
Tool Call Arguments 1.0
388+
Tool Call Count 1.0
389+
Tool Call Order 1.0
390+
391+
Evaluation Results
392+
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
393+
┃ Evaluation ┃ LLM Judge Output ┃ Tool Call Args ┃ Tool Call Count ┃ Tool Call Order ┃
394+
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
395+
│ Weather in Paris │ 0.7 │ 1.0 │ 1.0 │ 1.0 │
396+
├────────────────────┼────────────────────┼────────────────────┼────────────────────┼────────────────────┤
397+
│ Average │ 0.7 │ 1.0 │ 1.0 │ 1.0 │
398+
└────────────────────┴────────────────────┴────────────────────┴────────────────────┴────────────────────┘
399+
```

packages/uipath/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "uipath"
3-
version = "2.10.51"
3+
version = "2.10.54"
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"
77
dependencies = [
88
"uipath-core>=0.5.8, <0.6.0",
99
"uipath-eval>=0.1.0, <0.2.0",
10-
"uipath-runtime>=0.10.0, <0.11.0",
10+
"uipath-runtime>=0.10.1, <0.11.0",
1111
"uipath-platform>=0.1.13, <0.2.0",
1212
"click>=8.3.1",
1313
"httpx>=0.28.1",

packages/uipath/src/uipath/_cli/_debug/_bridge.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
UiPathRuntimeResult,
2020
UiPathRuntimeStatus,
2121
)
22-
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugQuitError
22+
from uipath.runtime.debug import (
23+
DetachedDebugBridge,
24+
UiPathDebugProtocol,
25+
UiPathDebugQuitError,
26+
)
2327
from uipath.runtime.events import UiPathRuntimeStateEvent, UiPathRuntimeStatePhase
2428

29+
DebugAttachMode = Literal["signalr", "console", "none"]
30+
2531
logger = logging.getLogger(__name__)
2632

2733

@@ -871,18 +877,27 @@ def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugProtoco
871877

872878

873879
def get_debug_bridge(
874-
context: UiPathRuntimeContext, verbose: bool = True
880+
context: UiPathRuntimeContext,
881+
verbose: bool = True,
882+
attach: DebugAttachMode | None = None,
875883
) -> UiPathDebugProtocol:
876884
"""Factory to get appropriate debug bridge based on context.
877885
878886
Args:
879887
context: The runtime context containing debug configuration.
880888
verbose: If True, console bridge shows all state updates. If False, only breakpoints.
889+
attach: Explicit attach mode. When None, falls back to
890+
``context.job_id``-based selection.
881891
882892
Returns:
883893
An instance of UiPathDebugBridge suitable for the context.
884894
"""
885-
if context.job_id:
895+
if attach == "none":
896+
return DetachedDebugBridge()
897+
if attach == "signalr":
886898
return get_remote_debug_bridge(context)
887-
else:
899+
if attach == "console":
888900
return ConsoleDebugBridge(verbose=verbose)
901+
if context.job_id:
902+
return get_remote_debug_bridge(context)
903+
return ConsoleDebugBridge(verbose=verbose)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from typing import AsyncIterator
2+
3+
from uipath.platform.connections import ConnectionsService
4+
from uipath.platform.errors import EnrichedException, FolderNotFoundException
5+
from uipath.platform.resource_catalog import (
6+
Resource,
7+
ResourceCatalogService,
8+
ResourceType,
9+
)
10+
11+
from .._utils._studio_project import (
12+
ReferencedResourceFolder,
13+
ReferencedResourceRequest,
14+
VirtualResourceRequest,
15+
)
16+
from ..models.runtime_schema import BindingResource, Bindings
17+
from ._resource_actions import CreateReference, CreateVirtual, ResourceAction, Skip
18+
19+
_NOT_FOUND_SUFFIX = "was not found and will not be added to the solution."
20+
21+
22+
async def resolve_bindings(
23+
bindings: Bindings,
24+
resource_catalog: ResourceCatalogService,
25+
connections: ConnectionsService,
26+
supported_virtual_kinds: set[str],
27+
) -> AsyncIterator[ResourceAction]:
28+
"""Yield one ResourceAction per importable binding.
29+
30+
Bindings that should be silently ignored (e.g. guardrail bindings without a
31+
folderPath) are filtered out here.
32+
"""
33+
for binding in bindings.resources:
34+
action = await _resolve_binding(
35+
binding, resource_catalog, connections, supported_virtual_kinds
36+
)
37+
if action is not None:
38+
yield action
39+
40+
41+
async def _resolve_binding(
42+
binding: BindingResource,
43+
resource_catalog: ResourceCatalogService,
44+
connections: ConnectionsService,
45+
supported_virtual_kinds: set[str],
46+
) -> ResourceAction | None:
47+
if binding.resource == "connection":
48+
return await _resolve_connection(binding, resource_catalog, connections)
49+
return await _resolve_regular(binding, resource_catalog, supported_virtual_kinds)
50+
51+
52+
async def _resolve_connection(
53+
binding: BindingResource,
54+
resource_catalog: ResourceCatalogService,
55+
connections: ConnectionsService,
56+
) -> ResourceAction | None:
57+
connection_id_value = binding.value.get("ConnectionId")
58+
if connection_id_value is None:
59+
raise ValueError(
60+
f"Connection binding {binding.key!r} is missing required field 'ConnectionId'"
61+
)
62+
connection_key = connection_id_value.default_value
63+
64+
try:
65+
connection = await connections.retrieve_async(connection_key)
66+
except EnrichedException:
67+
connector_name = (binding.metadata or {}).get("Connector")
68+
return Skip(
69+
message=(
70+
f"Connection with key '{connection_key}' of type "
71+
f"'{connector_name}' {_NOT_FOUND_SUFFIX}"
72+
)
73+
)
74+
75+
resource_name: str = connection.name
76+
folder_path: str = connection.folder.get("path")
77+
78+
found = await _find_in_resource_catalog(
79+
resource_catalog, "connection", resource_name, folder_path
80+
)
81+
if found is None:
82+
return Skip(
83+
message=(
84+
f"Resource '{resource_name}' of type 'connection' at folder path "
85+
f"'{folder_path}' {_NOT_FOUND_SUFFIX}"
86+
)
87+
)
88+
return _build_create_reference(found, resource_name)
89+
90+
91+
async def _resolve_regular(
92+
binding: BindingResource,
93+
resource_catalog: ResourceCatalogService,
94+
supported_virtual_kinds: set[str],
95+
) -> ResourceAction | None:
96+
name_value = binding.value.get("name")
97+
folder_path_value = binding.value.get("folderPath")
98+
if not folder_path_value:
99+
# guardrail resource, nothing to import
100+
return None
101+
if name_value is None:
102+
raise ValueError(f"Binding {binding.key!r} is missing required field 'name'")
103+
resource_name: str = name_value.default_value
104+
folder_path: str = folder_path_value.default_value
105+
resource_type: str = binding.resource
106+
107+
found = await _find_in_resource_catalog(
108+
resource_catalog, resource_type, resource_name, folder_path
109+
)
110+
if found is not None:
111+
return _build_create_reference(found, resource_name)
112+
113+
if resource_type not in supported_virtual_kinds:
114+
return Skip(
115+
message=(
116+
f"Cannot create virtual resource '{resource_name}' — "
117+
f"kind '{resource_type}' is not supported."
118+
)
119+
)
120+
121+
sub_type: str | None = (binding.metadata or {}).get("SubType")
122+
return CreateVirtual(
123+
request=VirtualResourceRequest(
124+
kind=resource_type,
125+
name=resource_name,
126+
type=sub_type,
127+
)
128+
)
129+
130+
131+
async def _find_in_resource_catalog(
132+
resource_catalog: ResourceCatalogService,
133+
resource_type: str,
134+
name: str,
135+
folder_path: str,
136+
) -> Resource | None:
137+
"""Look up a single resource in the Resource Catalog.
138+
139+
Returns the first match or None if the catalog can't search this kind, the
140+
folder is unknown, or no resource matches.
141+
"""
142+
catalog_type = next(
143+
(m for m in ResourceType if m.value == resource_type.lower()), None
144+
)
145+
if catalog_type is None:
146+
return None
147+
148+
resources = resource_catalog.list_by_type_async(
149+
resource_type=catalog_type, name=name, folder_path=folder_path
150+
)
151+
try:
152+
return await anext(resources, None)
153+
except FolderNotFoundException:
154+
return None
155+
finally:
156+
await resources.aclose()
157+
158+
159+
def _build_create_reference(
160+
found_resource: Resource, resource_name: str
161+
) -> CreateReference:
162+
folder = next(iter(found_resource.folders))
163+
return CreateReference(
164+
request=ReferencedResourceRequest(
165+
key=found_resource.resource_key,
166+
kind=found_resource.resource_type,
167+
type=found_resource.resource_sub_type,
168+
folder=ReferencedResourceFolder(
169+
folder_key=folder.key,
170+
fully_qualified_name=folder.fully_qualified_name,
171+
path=folder.path,
172+
),
173+
),
174+
resource_name=resource_name,
175+
kind=found_resource.resource_type,
176+
sub_type=found_resource.resource_sub_type,
177+
)

0 commit comments

Comments
 (0)