Skip to content
Merged
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.1.42"
version = "0.1.43"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
18 changes: 18 additions & 0 deletions packages/uipath-platform/src/uipath/platform/common/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ def project_id(self) -> str | None:

return os.getenv(ENV_UIPATH_PROJECT_ID, None)

@property
def agent_id(self) -> str | None:
from uipath.platform.common.constants import ENV_UIPATH_AGENT_ID

return os.getenv(ENV_UIPATH_AGENT_ID) or self.project_id

@property
def cloud_user_id(self) -> str | None:
from uipath.platform.common.constants import ENV_UIPATH_CLOUD_USER_ID

return os.getenv(ENV_UIPATH_CLOUD_USER_ID, None)

@property
def project_files_source(self) -> str | None:
from uipath.platform.common.constants import ENV_UIPATH_PROJECT_FILES_SOURCE

return os.getenv(ENV_UIPATH_PROJECT_FILES_SOURCE, None)

@property
def project_key(self) -> str | None:
from uipath.platform.common.constants import ENV_PROJECT_KEY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
ENV_TELEMETRY_ENABLED = "UIPATH_TELEMETRY_ENABLED"
ENV_TRACING_ENABLED = "UIPATH_TRACING_ENABLED"
ENV_UIPATH_PROJECT_ID = "UIPATH_PROJECT_ID"
ENV_UIPATH_AGENT_ID = "UIPATH_AGENT_ID"
ENV_UIPATH_CLOUD_USER_ID = "UIPATH_CLOUD_USER_ID"
ENV_UIPATH_PROJECT_FILES_SOURCE = "UIPATH_PROJECT_FILES_SOURCE"
ENV_PROJECT_KEY = "PROJECT_KEY"
ENV_PROCESS_KEY = "UIPATH_PROCESS_KEY"
ENV_UIPATH_PROCESS_UUID = "UIPATH_PROCESS_UUID"
Expand Down
55 changes: 55 additions & 0 deletions packages/uipath-platform/tests/common/test_config_env_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest

from uipath.platform.common._config import UiPathConfig


@pytest.fixture(autouse=True)
def _clear_env(monkeypatch):
for var in (
"UIPATH_PROJECT_ID",
"UIPATH_AGENT_ID",
"UIPATH_CLOUD_USER_ID",
"UIPATH_PROJECT_FILES_SOURCE",
):
monkeypatch.delenv(var, raising=False)


class TestProjectId:
def test_reads_env_var(self, monkeypatch):
monkeypatch.setenv("UIPATH_PROJECT_ID", "file-source-id")
assert UiPathConfig.project_id == "file-source-id"

def test_returns_none_when_unset(self):
assert UiPathConfig.project_id is None


class TestAgentId:
def test_returns_explicit_agent_id_when_set(self, monkeypatch):
monkeypatch.setenv("UIPATH_PROJECT_ID", "debug-project-guid")
monkeypatch.setenv("UIPATH_AGENT_ID", "real-agent-id")
assert UiPathConfig.agent_id == "real-agent-id"

def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch):
monkeypatch.setenv("UIPATH_PROJECT_ID", "cloud-project-id")
assert UiPathConfig.agent_id == "cloud-project-id"

def test_returns_none_when_neither_set(self):
assert UiPathConfig.agent_id is None


class TestCloudUserId:
def test_returns_value_when_set(self, monkeypatch):
monkeypatch.setenv("UIPATH_CLOUD_USER_ID", "user-guid")
assert UiPathConfig.cloud_user_id == "user-guid"

def test_returns_none_when_unset(self):
assert UiPathConfig.cloud_user_id is None


class TestProjectFilesSource:
def test_returns_value_when_set(self, monkeypatch):
monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", "Local")
assert UiPathConfig.project_files_source == "Local"

def test_returns_none_when_unset(self):
assert UiPathConfig.project_files_source is None
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath"
version = "2.10.60"
version = "2.10.61"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.5.8, <0.6.0",
"uipath-runtime>=0.10.1, <0.11.0",
"uipath-platform>=0.1.42, <0.2.0",
"uipath-platform>=0.1.43, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
59 changes: 49 additions & 10 deletions packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,18 @@ def __init__(self):
self._console = console_logger
self._rich_console = Console()
self._project_id = os.getenv("UIPATH_PROJECT_ID", None)
if not self._project_id:
self._agent_id = os.getenv("UIPATH_AGENT_ID") or self._project_id
if not self._agent_id:
logger.warning(
"Cannot report data to StudioWeb. Please set UIPATH_PROJECT_ID."
)

# Map UIPATH_PROJECT_FILES_SOURCE (Local/Cloud) to the backend's
# ProjectFilesSource enum integer. Without this every row the worker
# creates lands as Cloud, and the UI's `?projectFilesSource=1` filter
# never matches local-workspace runs.
self._project_files_source = self._resolve_project_files_source()

self.eval_set_ids: dict[str, str] = {} # Track eval_set_id per execution
self.eval_set_run_ids: dict[str, str] = {}
self.evaluators: dict[str, Any] = {}
Expand Down Expand Up @@ -1089,6 +1096,29 @@ def _collect_coded_results(
evaluator_runs.append(evaluator_run)
return evaluator_runs, evaluator_scores_list

@staticmethod
def _resolve_project_files_source() -> int | None:
raw = os.getenv("UIPATH_PROJECT_FILES_SOURCE")
if not raw:
return None
normalized = raw.strip().lower()
if normalized == "local":
return 1
if normalized == "cloud":
return 0
try:
return int(normalized)
except ValueError:
logger.warning(
f"Unrecognized UIPATH_PROJECT_FILES_SOURCE value: {raw!r}; ignoring."
)
return None

def _project_files_source_field(self) -> dict[str, int]:
if self._project_files_source is None:
return {}
return {"projectFilesSource": self._project_files_source}

def _update_eval_run_spec(
self,
assertion_runs: list[dict[str, Any]],
Expand All @@ -1115,6 +1145,7 @@ def _update_eval_run_spec(
},
"completionMetrics": {"duration": int(execution_time * 1000)},
"assertionRuns": assertion_runs,
**self._project_files_source_field(),
}

# Legacy backend expects payload wrapped in "request" field
Expand All @@ -1133,7 +1164,7 @@ def _update_eval_run_spec(
return RequestSpec(
method="PUT",
endpoint=Endpoint(
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun"
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun"
),
json=payload,
headers=self._tenant_header(),
Expand Down Expand Up @@ -1166,6 +1197,7 @@ def _update_coded_eval_run_spec(
},
"completionMetrics": {"duration": int(execution_time * 1000)},
"evaluatorRuns": evaluator_runs,
**self._project_files_source_field(),
}

# Log the payload for debugging coded eval run updates
Expand All @@ -1181,7 +1213,7 @@ def _update_coded_eval_run_spec(
return RequestSpec(
method="PUT",
endpoint=Endpoint(
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun"
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun"
),
json=payload,
headers=self._tenant_header(),
Expand Down Expand Up @@ -1235,6 +1267,7 @@ def _create_eval_run_spec(
"evalSnapshot": eval_snapshot,
# Backend expects integer status
"status": EvaluationStatus.IN_PROGRESS.value,
**self._project_files_source_field(),
}

# Legacy backend expects payload wrapped in "request" field
Expand All @@ -1253,7 +1286,7 @@ def _create_eval_run_spec(
return RequestSpec(
method="POST",
endpoint=Endpoint(
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun"
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun"
),
json=payload,
headers=self._tenant_header(),
Expand Down Expand Up @@ -1283,14 +1316,15 @@ def _create_eval_set_run_spec(
eval_set_id_value = str(uuid.uuid5(uuid.NAMESPACE_DNS, eval_set_id))

inner_payload: dict[str, Any] = {
"agentId": self._project_id,
"agentId": self._agent_id,
"evalSetId": eval_set_id_value,
"agentSnapshot": agent_snapshot.model_dump(by_alias=True),
# Backend expects integer status
"status": EvaluationStatus.IN_PROGRESS.value,
"numberOfEvalsExecuted": no_of_evals,
# Source is required by the backend (0 = coded SDK)
"source": 0,
**self._project_files_source_field(),
}

# Both coded and legacy send payload directly at root level
Expand All @@ -1309,7 +1343,7 @@ def _create_eval_set_run_spec(
return RequestSpec(
method="POST",
endpoint=Endpoint(
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun"
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun"
),
json=payload,
headers=self._tenant_header(),
Expand Down Expand Up @@ -1353,6 +1387,7 @@ def _update_eval_set_run_spec(
# Backend expects integer status
"status": status.value,
"evaluatorScores": evaluator_scores_list,
**self._project_files_source_field(),
}

# Legacy backend expects payload wrapped in "request" field
Expand All @@ -1374,7 +1409,7 @@ def _update_eval_set_run_spec(
return RequestSpec(
method="PUT",
endpoint=Endpoint(
f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun"
f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun"
),
json=payload,
headers=self._tenant_header(),
Expand Down Expand Up @@ -1406,12 +1441,12 @@ def _get_eval_runs_spec(

if is_coded:
endpoint_path = (
f"{prefix}execution/agents/{self._project_id}/coded/"
f"{prefix}execution/agents/{self._agent_id}/coded/"
f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns"
)
else:
endpoint_path = (
f"{prefix}execution/agents/{self._project_id}/"
f"{prefix}execution/agents/{self._agent_id}/"
f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns"
)

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

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

Expand Down
22 changes: 10 additions & 12 deletions packages/uipath/src/uipath/_cli/_evals/_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,28 +308,26 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None:
Args:
properties: The properties dictionary to enrich.
"""
# Add UiPath context
if UiPathConfig.project_id:
properties["ProjectId"] = UiPathConfig.project_id
properties["AgentId"] = UiPathConfig.project_id
if UiPathConfig.agent_id:
properties["AgentId"] = UiPathConfig.agent_id

# Get organization ID from UiPathConfig
if UiPathConfig.organization_id:
properties["CloudOrganizationId"] = UiPathConfig.organization_id

# Get CloudUserId from JWT token
try:
cloud_user_id = get_claim_from_token("sub")
if cloud_user_id:
properties["CloudUserId"] = cloud_user_id
except Exception:
pass # CloudUserId is optional
cloud_user_id = UiPathConfig.cloud_user_id
if not cloud_user_id:
try:
cloud_user_id = get_claim_from_token("sub")
except Exception:
cloud_user_id = None
if cloud_user_id:
properties["CloudUserId"] = cloud_user_id

# Get tenant ID from environment
tenant_id = os.getenv("UIPATH_TENANT_ID")
if tenant_id:
properties["TenantId"] = tenant_id

# Add source identifier
properties["Source"] = "uipath-python-cli"
properties["ApplicationName"] = "UiPath.Eval"
27 changes: 12 additions & 15 deletions packages/uipath/src/uipath/_cli/_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,26 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None:
Args:
properties: The properties dictionary to enrich.
"""
# Add UiPath context
project_key = _get_project_key()
if project_key:
properties["AgentId"] = project_key
agent_id = os.getenv("UIPATH_AGENT_ID") or _get_project_key()
if agent_id:
properties["AgentId"] = agent_id

# Get organization ID
if UiPathConfig.organization_id:
properties["CloudOrganizationId"] = UiPathConfig.organization_id

# Get tenant ID
if UiPathConfig.tenant_id:
properties["CloudTenantId"] = UiPathConfig.tenant_id

# Get CloudUserId from JWT token
try:
cloud_user_id = get_claim_from_token("sub")
if cloud_user_id:
properties["CloudUserId"] = cloud_user_id
except Exception:
pass
cloud_user_id = UiPathConfig.cloud_user_id
if not cloud_user_id:
try:
cloud_user_id = get_claim_from_token("sub")
except Exception:
cloud_user_id = None
if cloud_user_id:
properties["CloudUserId"] = cloud_user_id

properties["SessionId"] = "nosession" # Placeholder for session ID
properties["SessionId"] = "nosession"

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

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

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

Expand Down
Loading
Loading