Skip to content

Commit af3602e

Browse files
authored
feat(eval): honor UIPATH_AGENT_ID, UIPATH_CLOUD_USER_ID, UIPATH_PROJECT_FILES_SOURCE env vars [AE-1396] (#1608)
1 parent ee98f4d commit af3602e

11 files changed

Lines changed: 357 additions & 44 deletions

File tree

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.42"
3+
version = "0.1.43"
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/common/_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ def project_id(self) -> str | None:
6161

6262
return os.getenv(ENV_UIPATH_PROJECT_ID, None)
6363

64+
@property
65+
def agent_id(self) -> str | None:
66+
from uipath.platform.common.constants import ENV_UIPATH_AGENT_ID
67+
68+
return os.getenv(ENV_UIPATH_AGENT_ID) or self.project_id
69+
70+
@property
71+
def cloud_user_id(self) -> str | None:
72+
from uipath.platform.common.constants import ENV_UIPATH_CLOUD_USER_ID
73+
74+
return os.getenv(ENV_UIPATH_CLOUD_USER_ID, None)
75+
76+
@property
77+
def project_files_source(self) -> str | None:
78+
from uipath.platform.common.constants import ENV_UIPATH_PROJECT_FILES_SOURCE
79+
80+
return os.getenv(ENV_UIPATH_PROJECT_FILES_SOURCE, None)
81+
6482
@property
6583
def project_key(self) -> str | None:
6684
from uipath.platform.common.constants import ENV_PROJECT_KEY

packages/uipath-platform/src/uipath/platform/common/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
ENV_TELEMETRY_ENABLED = "UIPATH_TELEMETRY_ENABLED"
1818
ENV_TRACING_ENABLED = "UIPATH_TRACING_ENABLED"
1919
ENV_UIPATH_PROJECT_ID = "UIPATH_PROJECT_ID"
20+
ENV_UIPATH_AGENT_ID = "UIPATH_AGENT_ID"
21+
ENV_UIPATH_CLOUD_USER_ID = "UIPATH_CLOUD_USER_ID"
22+
ENV_UIPATH_PROJECT_FILES_SOURCE = "UIPATH_PROJECT_FILES_SOURCE"
2023
ENV_PROJECT_KEY = "PROJECT_KEY"
2124
ENV_PROCESS_KEY = "UIPATH_PROCESS_KEY"
2225
ENV_UIPATH_PROCESS_UUID = "UIPATH_PROCESS_UUID"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pytest
2+
3+
from uipath.platform.common._config import UiPathConfig
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def _clear_env(monkeypatch):
8+
for var in (
9+
"UIPATH_PROJECT_ID",
10+
"UIPATH_AGENT_ID",
11+
"UIPATH_CLOUD_USER_ID",
12+
"UIPATH_PROJECT_FILES_SOURCE",
13+
):
14+
monkeypatch.delenv(var, raising=False)
15+
16+
17+
class TestProjectId:
18+
def test_reads_env_var(self, monkeypatch):
19+
monkeypatch.setenv("UIPATH_PROJECT_ID", "file-source-id")
20+
assert UiPathConfig.project_id == "file-source-id"
21+
22+
def test_returns_none_when_unset(self):
23+
assert UiPathConfig.project_id is None
24+
25+
26+
class TestAgentId:
27+
def test_returns_explicit_agent_id_when_set(self, monkeypatch):
28+
monkeypatch.setenv("UIPATH_PROJECT_ID", "debug-project-guid")
29+
monkeypatch.setenv("UIPATH_AGENT_ID", "real-agent-id")
30+
assert UiPathConfig.agent_id == "real-agent-id"
31+
32+
def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch):
33+
monkeypatch.setenv("UIPATH_PROJECT_ID", "cloud-project-id")
34+
assert UiPathConfig.agent_id == "cloud-project-id"
35+
36+
def test_returns_none_when_neither_set(self):
37+
assert UiPathConfig.agent_id is None
38+
39+
40+
class TestCloudUserId:
41+
def test_returns_value_when_set(self, monkeypatch):
42+
monkeypatch.setenv("UIPATH_CLOUD_USER_ID", "user-guid")
43+
assert UiPathConfig.cloud_user_id == "user-guid"
44+
45+
def test_returns_none_when_unset(self):
46+
assert UiPathConfig.cloud_user_id is None
47+
48+
49+
class TestProjectFilesSource:
50+
def test_returns_value_when_set(self, monkeypatch):
51+
monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", "Local")
52+
assert UiPathConfig.project_files_source == "Local"
53+
54+
def test_returns_none_when_unset(self):
55+
assert UiPathConfig.project_files_source is None

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/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.60"
3+
version = "2.10.61"
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-runtime>=0.10.1, <0.11.0",
10-
"uipath-platform>=0.1.42, <0.2.0",
10+
"uipath-platform>=0.1.43, <0.2.0",
1111
"click>=8.3.1",
1212
"httpx>=0.28.1",
1313
"pyjwt>=2.10.1",

packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,18 @@ def __init__(self):
102102
self._console = console_logger
103103
self._rich_console = Console()
104104
self._project_id = os.getenv("UIPATH_PROJECT_ID", None)
105-
if not self._project_id:
105+
self._agent_id = os.getenv("UIPATH_AGENT_ID") or self._project_id
106+
if not self._agent_id:
106107
logger.warning(
107108
"Cannot report data to StudioWeb. Please set UIPATH_PROJECT_ID."
108109
)
109110

111+
# Map UIPATH_PROJECT_FILES_SOURCE (Local/Cloud) to the backend's
112+
# ProjectFilesSource enum integer. Without this every row the worker
113+
# creates lands as Cloud, and the UI's `?projectFilesSource=1` filter
114+
# never matches local-workspace runs.
115+
self._project_files_source = self._resolve_project_files_source()
116+
110117
self.eval_set_ids: dict[str, str] = {} # Track eval_set_id per execution
111118
self.eval_set_run_ids: dict[str, str] = {}
112119
self.evaluators: dict[str, Any] = {}
@@ -1089,6 +1096,29 @@ def _collect_coded_results(
10891096
evaluator_runs.append(evaluator_run)
10901097
return evaluator_runs, evaluator_scores_list
10911098

1099+
@staticmethod
1100+
def _resolve_project_files_source() -> int | None:
1101+
raw = os.getenv("UIPATH_PROJECT_FILES_SOURCE")
1102+
if not raw:
1103+
return None
1104+
normalized = raw.strip().lower()
1105+
if normalized == "local":
1106+
return 1
1107+
if normalized == "cloud":
1108+
return 0
1109+
try:
1110+
return int(normalized)
1111+
except ValueError:
1112+
logger.warning(
1113+
f"Unrecognized UIPATH_PROJECT_FILES_SOURCE value: {raw!r}; ignoring."
1114+
)
1115+
return None
1116+
1117+
def _project_files_source_field(self) -> dict[str, int]:
1118+
if self._project_files_source is None:
1119+
return {}
1120+
return {"projectFilesSource": self._project_files_source}
1121+
10921122
def _update_eval_run_spec(
10931123
self,
10941124
assertion_runs: list[dict[str, Any]],
@@ -1115,6 +1145,7 @@ def _update_eval_run_spec(
11151145
},
11161146
"completionMetrics": {"duration": int(execution_time * 1000)},
11171147
"assertionRuns": assertion_runs,
1148+
**self._project_files_source_field(),
11181149
}
11191150

11201151
# Legacy backend expects payload wrapped in "request" field
@@ -1133,7 +1164,7 @@ def _update_eval_run_spec(
11331164
return RequestSpec(
11341165
method="PUT",
11351166
endpoint=Endpoint(
1136-
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun"
1167+
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun"
11371168
),
11381169
json=payload,
11391170
headers=self._tenant_header(),
@@ -1166,6 +1197,7 @@ def _update_coded_eval_run_spec(
11661197
},
11671198
"completionMetrics": {"duration": int(execution_time * 1000)},
11681199
"evaluatorRuns": evaluator_runs,
1200+
**self._project_files_source_field(),
11691201
}
11701202

11711203
# Log the payload for debugging coded eval run updates
@@ -1181,7 +1213,7 @@ def _update_coded_eval_run_spec(
11811213
return RequestSpec(
11821214
method="PUT",
11831215
endpoint=Endpoint(
1184-
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun"
1216+
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun"
11851217
),
11861218
json=payload,
11871219
headers=self._tenant_header(),
@@ -1235,6 +1267,7 @@ def _create_eval_run_spec(
12351267
"evalSnapshot": eval_snapshot,
12361268
# Backend expects integer status
12371269
"status": EvaluationStatus.IN_PROGRESS.value,
1270+
**self._project_files_source_field(),
12381271
}
12391272

12401273
# Legacy backend expects payload wrapped in "request" field
@@ -1253,7 +1286,7 @@ def _create_eval_run_spec(
12531286
return RequestSpec(
12541287
method="POST",
12551288
endpoint=Endpoint(
1256-
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun"
1289+
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun"
12571290
),
12581291
json=payload,
12591292
headers=self._tenant_header(),
@@ -1283,14 +1316,15 @@ def _create_eval_set_run_spec(
12831316
eval_set_id_value = str(uuid.uuid5(uuid.NAMESPACE_DNS, eval_set_id))
12841317

12851318
inner_payload: dict[str, Any] = {
1286-
"agentId": self._project_id,
1319+
"agentId": self._agent_id,
12871320
"evalSetId": eval_set_id_value,
12881321
"agentSnapshot": agent_snapshot.model_dump(by_alias=True),
12891322
# Backend expects integer status
12901323
"status": EvaluationStatus.IN_PROGRESS.value,
12911324
"numberOfEvalsExecuted": no_of_evals,
12921325
# Source is required by the backend (0 = coded SDK)
12931326
"source": 0,
1327+
**self._project_files_source_field(),
12941328
}
12951329

12961330
# Both coded and legacy send payload directly at root level
@@ -1309,7 +1343,7 @@ def _create_eval_set_run_spec(
13091343
return RequestSpec(
13101344
method="POST",
13111345
endpoint=Endpoint(
1312-
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun"
1346+
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun"
13131347
),
13141348
json=payload,
13151349
headers=self._tenant_header(),
@@ -1353,6 +1387,7 @@ def _update_eval_set_run_spec(
13531387
# Backend expects integer status
13541388
"status": status.value,
13551389
"evaluatorScores": evaluator_scores_list,
1390+
**self._project_files_source_field(),
13561391
}
13571392

13581393
# Legacy backend expects payload wrapped in "request" field
@@ -1374,7 +1409,7 @@ def _update_eval_set_run_spec(
13741409
return RequestSpec(
13751410
method="PUT",
13761411
endpoint=Endpoint(
1377-
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun"
1412+
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun"
13781413
),
13791414
json=payload,
13801415
headers=self._tenant_header(),
@@ -1406,12 +1441,12 @@ def _get_eval_runs_spec(
14061441

14071442
if is_coded:
14081443
endpoint_path = (
1409-
f"{prefix}execution/agents/{self._project_id}/coded/"
1444+
f"{prefix}execution/agents/{self._agent_id}/coded/"
14101445
f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns"
14111446
)
14121447
else:
14131448
endpoint_path = (
1414-
f"{prefix}execution/agents/{self._project_id}/"
1449+
f"{prefix}execution/agents/{self._agent_id}/"
14151450
f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns"
14161451
)
14171452

@@ -1420,10 +1455,14 @@ def _get_eval_runs_spec(
14201455
f"eval_set_run_id={eval_set_run_id}, evaluation_id={evaluation_id}, coded={is_coded}"
14211456
)
14221457

1458+
# The backend's listing endpoint filters by projectFilesSource +
1459+
# cloudUserId so the UI only shows the caller's local rows. Mirror
1460+
# that here so resume lookups match the row written by the same
1461+
# worker session.
14231462
return RequestSpec(
14241463
method="GET",
14251464
endpoint=Endpoint(endpoint_path),
1426-
params={}, # No query params needed - evalSetRunId is in the path
1465+
params=self._project_files_source_field(),
14271466
headers=self._tenant_header(),
14281467
)
14291468

packages/uipath/src/uipath/_cli/_evals/_telemetry.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -308,28 +308,26 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None:
308308
Args:
309309
properties: The properties dictionary to enrich.
310310
"""
311-
# Add UiPath context
312311
if UiPathConfig.project_id:
313312
properties["ProjectId"] = UiPathConfig.project_id
314-
properties["AgentId"] = UiPathConfig.project_id
313+
if UiPathConfig.agent_id:
314+
properties["AgentId"] = UiPathConfig.agent_id
315315

316-
# Get organization ID from UiPathConfig
317316
if UiPathConfig.organization_id:
318317
properties["CloudOrganizationId"] = UiPathConfig.organization_id
319318

320-
# Get CloudUserId from JWT token
321-
try:
322-
cloud_user_id = get_claim_from_token("sub")
323-
if cloud_user_id:
324-
properties["CloudUserId"] = cloud_user_id
325-
except Exception:
326-
pass # CloudUserId is optional
319+
cloud_user_id = UiPathConfig.cloud_user_id
320+
if not cloud_user_id:
321+
try:
322+
cloud_user_id = get_claim_from_token("sub")
323+
except Exception:
324+
cloud_user_id = None
325+
if cloud_user_id:
326+
properties["CloudUserId"] = cloud_user_id
327327

328-
# Get tenant ID from environment
329328
tenant_id = os.getenv("UIPATH_TENANT_ID")
330329
if tenant_id:
331330
properties["TenantId"] = tenant_id
332331

333-
# Add source identifier
334332
properties["Source"] = "uipath-python-cli"
335333
properties["ApplicationName"] = "UiPath.Eval"

packages/uipath/src/uipath/_cli/_telemetry.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,26 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None:
4141
Args:
4242
properties: The properties dictionary to enrich.
4343
"""
44-
# Add UiPath context
45-
project_key = _get_project_key()
46-
if project_key:
47-
properties["AgentId"] = project_key
44+
agent_id = os.getenv("UIPATH_AGENT_ID") or _get_project_key()
45+
if agent_id:
46+
properties["AgentId"] = agent_id
4847

49-
# Get organization ID
5048
if UiPathConfig.organization_id:
5149
properties["CloudOrganizationId"] = UiPathConfig.organization_id
5250

53-
# Get tenant ID
5451
if UiPathConfig.tenant_id:
5552
properties["CloudTenantId"] = UiPathConfig.tenant_id
5653

57-
# Get CloudUserId from JWT token
58-
try:
59-
cloud_user_id = get_claim_from_token("sub")
60-
if cloud_user_id:
61-
properties["CloudUserId"] = cloud_user_id
62-
except Exception:
63-
pass
54+
cloud_user_id = UiPathConfig.cloud_user_id
55+
if not cloud_user_id:
56+
try:
57+
cloud_user_id = get_claim_from_token("sub")
58+
except Exception:
59+
cloud_user_id = None
60+
if cloud_user_id:
61+
properties["CloudUserId"] = cloud_user_id
6462

65-
properties["SessionId"] = "nosession" # Placeholder for session ID
63+
properties["SessionId"] = "nosession"
6664

6765
try:
6866
properties["SDKVersion"] = version("uipath")
@@ -71,7 +69,6 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None:
7169

7270
properties["IsGithubCI"] = bool(os.getenv("GITHUB_ACTIONS"))
7371

74-
# Add source identifier
7572
properties["Source"] = "uipath-python-cli"
7673
properties["ApplicationName"] = "UiPath.AgentCli"
7774

0 commit comments

Comments
 (0)