Skip to content

Commit fd61bc2

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/governance-audit
# Conflicts: # pyproject.toml # uv.lock
2 parents 54a3fce + b5d481d commit fd61bc2

12 files changed

Lines changed: 1195 additions & 8 deletions

File tree

SETUP.MD

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# SETUP.MD
2+
3+
This file documents how to provision a clean development environment for `uipath-runtime`, run the build, execute the tests, and validate a sample code change end-to-end. It is intended both as a quick reference for human contributors and as a structured guide for automated environment-setup tooling.
4+
5+
## Prerequisites
6+
7+
- Python 3.11+
8+
- [uv](https://docs.astral.sh/uv/) 0.5+
9+
10+
### Supported platforms
11+
12+
`uv` is shell- and OS-agnostic, so the commands below run unchanged on every supported platform:
13+
14+
- [x] Linux
15+
- [x] Windows
16+
- [x] macOS
17+
18+
## Environment Variables
19+
20+
None required for environment setup, build, or unit tests. The suite under the `Test` section runs fully offline and requires no external authentication.
21+
22+
> **All commands below must be run from the repository root.** The `uv` invocations resolve `pyproject.toml`, `src/`, and `tests/` relative to the current working directory. The first line of `## Setup` enforces this by `cd`-ing to the git root.
23+
24+
## Setup
25+
26+
```bash
27+
cd "$(git rev-parse --show-toplevel)"
28+
python3 -m pip install --upgrade uv
29+
uv sync --all-extras
30+
```
31+
32+
## Verify Setup
33+
34+
```bash
35+
uv --version
36+
uv run python --version
37+
uv run python -c "import uipath.runtime; print('uipath_runtime ok')"
38+
```
39+
40+
## Build
41+
42+
N/A
43+
44+
## Test
45+
46+
```bash
47+
uv run pytest
48+
```
49+
50+
## Sample Code Change
51+
52+
### The change
53+
54+
Add a new `count` classmethod to `UiPathRuntimeFactoryRegistry` in `src/uipath/runtime/registry.py`, immediately after the existing `get_all` method:
55+
56+
```python
57+
@classmethod
58+
def count(cls) -> int:
59+
"""Return the number of currently registered factories."""
60+
return len(cls._factories)
61+
```
62+
63+
Then create `tests/test_registry_count.py` with two pytest tests:
64+
65+
```python
66+
"""Tests for UiPathRuntimeFactoryRegistry.count."""
67+
68+
from unittest.mock import MagicMock
69+
70+
from uipath.runtime.registry import UiPathRuntimeFactoryRegistry
71+
72+
73+
def _make_factory():
74+
"""Return a callable that yields a protocol-shaped mock.
75+
76+
The registry stores the callable in `_factories` without invoking it, so
77+
`count()` only needs the entry to exist. Returning a `MagicMock()` keeps
78+
the callable's return type structurally compatible with
79+
`UiPathRuntimeFactoryProtocol` without dragging the real type in.
80+
"""
81+
return lambda _context: MagicMock()
82+
83+
84+
def test_count_empty(monkeypatch) -> None:
85+
monkeypatch.setattr(UiPathRuntimeFactoryRegistry, "_factories", {})
86+
monkeypatch.setattr(UiPathRuntimeFactoryRegistry, "_registration_order", [])
87+
assert UiPathRuntimeFactoryRegistry.count() == 0
88+
89+
90+
def test_count_after_registrations(monkeypatch) -> None:
91+
monkeypatch.setattr(UiPathRuntimeFactoryRegistry, "_factories", {})
92+
monkeypatch.setattr(UiPathRuntimeFactoryRegistry, "_registration_order", [])
93+
UiPathRuntimeFactoryRegistry.register("alpha", _make_factory(), "a.json")
94+
UiPathRuntimeFactoryRegistry.register("beta", _make_factory(), "b.json")
95+
assert UiPathRuntimeFactoryRegistry.count() == 2
96+
```
97+
98+
### Verification
99+
100+
```bash
101+
uv run pytest tests/test_registry_count.py -v
102+
```
103+
104+
## Test with a real UiPath Coded Agent
105+
106+
> This section is for human contributors who want to validate changes end-to-end against the real cloud platform. It is **not executed by the Agentic Inner Loop validation pipeline** — that pipeline only runs the sections above (Setup → Verify → Build → Test → Sample Code Change).
107+
108+
The unit tests above are necessary but not sufficient — they don't exercise the package end-to-end through a real agent. The flow below validates changes against a live runtime:
109+
110+
1. Apply the code changes locally.
111+
2. Run the unit tests (see the `Sample Code Change` section above).
112+
3. Scaffold a coded UiPath agent that exercises the changed code path.
113+
4. In the downstream project's `pyproject.toml`, add this local library as an editable dependency:
114+
115+
```toml
116+
[tool.uv.sources]
117+
uipath-runtime = { path = "../path/to/uipath-runtime-python", editable = true }
118+
```
119+
120+
5. Exercise the new behavior end-to-end:
121+
122+
```bash
123+
uv run uipath run <agent-name> --input '{...}'
124+
```
125+
126+
6. (Optional) Open a PR and apply the `build:dev` label — this publishes the development version to Test PyPI.
127+
7. The PR description is updated automatically with instructions for pointing the downstream agent at the Test PyPI dev version.
128+
8. Validate the new behavior against the real platform — use either or both of the deploy targets below (Studio Web and Orchestrator are not mutually exclusive):
129+
- **Studio Web**: export the `UIPATH_PROJECT_ID` environment variable pointing to an existing Coded Agent project in your solution, then run [`uipath push`](https://uipath.github.io/uipath-python/cli/#push) to push the dev version to that project. Open it in Studio Web and exercise the changed code path.
130+
- **Orchestrator**: run [`uipath deploy`](https://uipath.github.io/uipath-python/cli/#deploy) to deploy the dev version as a package, then start a job in Orchestrator and exercise the changed code path.
131+
9. Once validation is done, close the dev PR — these PRs are not meant to be merged; their only purpose was to publish a Test PyPI build for end-to-end validation.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "uipath-runtime"
3-
version = "0.11.0"
3+
version = "0.11.4"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
8-
"uipath-core>=0.5.21, <0.6.0",
8+
"uipath-core>=0.5.22,<0.6.0",
99
"pyyaml>=6.0, <7.0",
1010
"vaderSentiment>=3.3.2, <4.0",
1111
"chardet>=5.2.0, <8.0",

src/uipath/runtime/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@
4343
)
4444
from uipath.runtime.schema import UiPathRuntimeSchema
4545
from uipath.runtime.storage import UiPathRuntimeStorageProtocol
46+
from uipath.runtime.workspace import (
47+
AttachmentRegistryEntry,
48+
HydrationPolicy,
49+
HydrationRuntime,
50+
Workspace,
51+
WorkspaceHydrator,
52+
WorkspaceRegistryStore,
53+
)
4654

4755
__all__ = [
4856
"UiPathExecuteOptions",
@@ -73,4 +81,10 @@
7381
"UiPathResumeTriggerName",
7482
"UiPathChatProtocol",
7583
"UiPathChatRuntime",
84+
"AttachmentRegistryEntry",
85+
"HydrationPolicy",
86+
"HydrationRuntime",
87+
"Workspace",
88+
"WorkspaceHydrator",
89+
"WorkspaceRegistryStore",
7690
]

src/uipath/runtime/context.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pathlib import Path
88
from typing import Any, Literal
99

10-
from pydantic import BaseModel, ConfigDict, Field
10+
from pydantic import BaseModel, ConfigDict, Field, model_validator
1111
from uipath.core.errors import UiPathFaultedTriggerError
1212
from uipath.core.tracing import UiPathTraceManager
1313

@@ -23,6 +23,13 @@
2323

2424
logger = logging.getLogger(__name__)
2525

26+
_EXECUTION_SOURCE_BY_COMMAND: dict[str, str] = {
27+
"run": "runtime",
28+
"debug": "playground",
29+
"dev": "playground",
30+
"eval": "eval",
31+
}
32+
2633

2734
class UiPathRuntimeContext(BaseModel):
2835
"""Context information passed throughout the runtime execution."""
@@ -31,12 +38,23 @@ class UiPathRuntimeContext(BaseModel):
3138
input: str | None = None
3239
resume: bool = False
3340
command: str | None = None
41+
execution_source: str | None = Field(
42+
None, description="Execution source derived from the command."
43+
)
3444
job_id: str | None = None
3545
conversation_id: str | None = Field(
3646
None, description="Conversation identifier for CAS"
3747
)
3848
exchange_id: str | None = Field(None, description="Exchange identifier for CAS")
3949
message_id: str | None = Field(None, description="Message identifier for CAS")
50+
end_exchange: bool = Field(
51+
True,
52+
description="Whether to emit the exchange end event for CAS",
53+
)
54+
conversational_user_id: str | None = Field(
55+
None,
56+
description="Conversation owner id for CAS (a real cloud user id or a synthetic user id)",
57+
)
4058
voice_mode: Literal["session"] | None = Field(
4159
None, description="Voice job type for CAS"
4260
)
@@ -88,6 +106,28 @@ class UiPathRuntimeContext(BaseModel):
88106

89107
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
90108

109+
def _apply_execution_source(self) -> None:
110+
"""Derive execution_source from the command, if not already set.
111+
112+
Only assigns a mapped value, so the field stays unset (absent under
113+
``model_dump(exclude_unset=True)``) for unmapped commands, and an
114+
explicitly-provided value is never overwritten.
115+
"""
116+
if self.execution_source is None and self.command is not None:
117+
source = _EXECUTION_SOURCE_BY_COMMAND.get(self.command)
118+
if source is not None:
119+
self.execution_source = source
120+
121+
@model_validator(mode="after")
122+
def _derive_execution_source(self) -> "UiPathRuntimeContext":
123+
"""Derive execution_source on the constructor path (e.g. dev/init).
124+
125+
``with_defaults`` mutates ``command`` via ``setattr`` after construction,
126+
so it re-applies the derivation itself.
127+
"""
128+
self._apply_execution_source()
129+
return self
130+
91131
def get_input(self) -> dict[str, Any] | None:
92132
"""Get parsed input data.
93133
@@ -337,6 +377,9 @@ def with_defaults(
337377
for k, v in kwargs.items():
338378
setattr(base, k, v)
339379

380+
# setattr does not re-run the validator, so derive explicitly.
381+
base._apply_execution_source()
382+
340383
return base
341384

342385
@classmethod
@@ -364,6 +407,8 @@ def from_config(
364407
"conversationalService.conversationId": "conversation_id",
365408
"conversationalService.exchangeId": "exchange_id",
366409
"conversationalService.messageId": "message_id",
410+
"conversationalService.endExchange": "end_exchange",
411+
"conversationalService.conversationalUserId": "conversational_user_id",
367412
"mcpServer.id": "mcp_server_id",
368413
"mcpServer.slug": "mcp_server_slug",
369414
"voice.mode": "voice_mode",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Workspace persistence primitives for runtime implementations."""
2+
3+
from uipath.runtime.workspace.hydration import (
4+
HydrationPolicy,
5+
HydrationRuntime,
6+
)
7+
from uipath.runtime.workspace.hydrator import (
8+
AttachmentRegistryEntry,
9+
WorkspaceHydrator,
10+
)
11+
from uipath.runtime.workspace.registry_store import WorkspaceRegistryStore
12+
from uipath.runtime.workspace.workspace import Workspace
13+
14+
__all__ = [
15+
"AttachmentRegistryEntry",
16+
"HydrationPolicy",
17+
"HydrationRuntime",
18+
"Workspace",
19+
"WorkspaceHydrator",
20+
"WorkspaceRegistryStore",
21+
]

0 commit comments

Comments
 (0)