Skip to content

Commit fab778b

Browse files
committed
refactor: client credentials logic
1 parent e94a6ba commit fab778b

12 files changed

Lines changed: 395 additions & 102 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
UIPATH_CLIENT_SECRET=
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# UiPath Coded Agent: Asset Value Checker
2+
3+
This project demonstrates how to create a Python-based UiPath Coded Agent that connects as an External Application to UiPath Orchestrator, retrieves an asset, and validates its IntValue against custom rules.
4+
5+
## Overview
6+
7+
The agent uses the UiPath Python SDK to:
8+
9+
* Connect to UiPath Orchestrator as an external application
10+
* Authenticate via the Client Credentials flow (Client ID + Client Secret)
11+
* Retrieve a specific asset from a given folder
12+
* Check whether the asset has an integer value and validate it against a range (100–1000)
13+
* Return a descriptive message with the validation result
14+
15+
## How to Set Up
16+
17+
### Step 1: Install UiPath Python SDK
18+
19+
1. Open it with your prefered editor
20+
2. In terminal run:
21+
```bash
22+
uv init
23+
uv add uipath
24+
uv run uipath init
25+
```
26+
27+
### Step 2: Configure Environment Variables
28+
```bash
29+
UIPATH_CLIENT_SECRET=your-client-secret
30+
```
31+
32+
### Step 3: Understanding the Event Flow
33+
34+
When this agent runs, it will:
35+
1. Pass the configured input values (`asset_name` and `folder_path`) into the agent
36+
2. Connect to Orchestrator using Client Credentials (Client ID + Client Secret)
37+
3. Retrieve the specified asset from the given folder
38+
4. Check whether the asset contains an IntValue and validates it against the allowed range (100–1000)
39+
5. Return a message with the validation result
40+
41+
### Step 4: Publish Your Coded Agent
42+
43+
1. Use `uipath pack` and `uipath publish` to create and publish the package
44+
2. Create an Orchestrator Automation from the published process
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import dataclasses
2+
import dotenv
3+
import logging
4+
import os
5+
6+
from typing import Optional
7+
from uipath import UiPath
8+
from uipath.tracing import traced
9+
10+
dotenv.load_dotenv()
11+
logger = logging.getLogger(__name__)
12+
13+
UIPATH_CLIENT_ID = "client_id"
14+
UIPATH_CLIENT_SECRET = os.getenv("UIPATH_CLIENT_SECRET")
15+
UIPATH_SCOPE = "OR.Assets"
16+
UIPATH_URL = "base_url"
17+
18+
uipath = UiPath(
19+
client_id=UIPATH_CLIENT_ID,
20+
client_secret=UIPATH_CLIENT_SECRET,
21+
scope=UIPATH_SCOPE,
22+
base_url=UIPATH_URL
23+
)
24+
25+
@dataclasses.dataclass
26+
class AgentInput:
27+
"""Input data structure for the UiPath agent.
28+
29+
Attributes:
30+
asset_name (str): The name of the UiPath asset.
31+
folder_path (str): The folder path where the asset is located.
32+
"""
33+
asset_name: str
34+
folder_path: str
35+
36+
def get_asset(name: str, folder_path: str) -> Optional[object]:
37+
"""Retrieve an asset from UiPath.
38+
39+
Args:
40+
name (str): The asset name.
41+
folder_path (str): The UiPath folder path.
42+
43+
Returns:
44+
Optional[object]: The asset object if found, else None.
45+
"""
46+
return uipath.assets.retrieve(name=name, folder_path=folder_path)
47+
48+
def check_asset(asset: object) -> str:
49+
"""Check if an asset's IntValue is within a valid range.
50+
51+
Args:
52+
asset (object): The asset object.
53+
54+
Returns:
55+
str: Result message depending on asset state.
56+
"""
57+
if asset is None:
58+
return "Asset not found."
59+
60+
int_value = getattr(asset, "int_value", None)
61+
if int_value is None:
62+
return "Asset does not have an IntValue."
63+
64+
if 100 <= int_value <= 1000:
65+
return f"Asset '{asset.name}' has a valid IntValue: {int_value}"
66+
else:
67+
return f"Asset '{asset.name}' has an out-of-range IntValue: {int_value}"
68+
69+
@traced()
70+
def main(input: AgentInput) -> str:
71+
"""Main entry point for the agent.
72+
73+
Args:
74+
input (AgentInput): The input containing asset details.
75+
76+
Returns:
77+
str: Message with the result of the asset check.
78+
"""
79+
asset = get_asset(input.asset_name, input.folder_path)
80+
return check_asset(asset)
81+
82+
if __name__ == "__main__":
83+
input_data = AgentInput(asset_name="test-asset", folder_path="TestFolder")
84+
result = main(input_data)
85+
print(result)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "asset-checker-agent"
3+
version = "0.0.1"
4+
description = "UiPath agent to validate asset values via External Application integration with Orchestrator."
5+
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
6+
dependencies = [
7+
"uipath>=2.1.45",
8+
]
9+
requires-python = ">=3.10"

src/uipath/_cli/_auth/_auth_service.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
from urllib.parse import urlparse
88

99
from uipath._cli._auth._auth_server import HTTPServer
10-
from uipath._cli._auth._client_credentials import ClientCredentialsService
1110
from uipath._cli._auth._oidc_utils import OidcUtils
1211
from uipath._cli._auth._portal_service import (
1312
PortalService,
1413
get_tenant_id,
1514
select_tenant,
1615
)
1716
from uipath._cli._auth._url_utils import set_force_flag
18-
from uipath._cli._auth._utils import update_auth_file, update_env_file
17+
from uipath._cli._auth._utils import update_auth_file
1918
from uipath._cli._utils._console import ConsoleLogger
19+
from uipath._services import ExternalApplicationService
20+
from uipath._utils._auth import parse_access_token, update_env_file
2021

2122

2223
class AuthService:
@@ -63,18 +64,22 @@ def authenticate(self) -> None:
6364
self._authenticate_authorization_code()
6465

6566
def _authenticate_client_credentials(self) -> None:
66-
if not self._base_url:
67-
self._console.error(
68-
"--base-url is required when using client credentials authentication."
69-
)
70-
return
7167
self._console.hint("Using client credentials authentication.")
72-
credentials_service = ClientCredentialsService(self._base_url)
73-
credentials_service.authenticate(
68+
external_app_service = ExternalApplicationService(self._base_url)
69+
access_token = external_app_service.get_access_token(
7470
self._client_id, # type: ignore
7571
self._client_secret, # type: ignore
7672
self._scope,
7773
)
74+
parsed_access_token = parse_access_token(access_token)
75+
76+
env_vars = {
77+
"UIPATH_ACCESS_TOKEN": access_token,
78+
"UIPATH_URL": external_app_service._base_url,
79+
"UIPATH_ORGANIZATION_ID": parsed_access_token.get("prt_id", ""),
80+
}
81+
82+
update_env_file(env_vars)
7883

7984
def _authenticate_authorization_code(self) -> None:
8085
with PortalService(self._domain) as portal_service:

src/uipath/_cli/_auth/_portal_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import click
66
import httpx
77

8+
from ..._utils._auth import update_env_file
89
from ..._utils._ssl_context import get_httpx_client_kwargs
910
from .._utils._console import ConsoleLogger
1011
from ._models import TenantsAndOrganizationInfoResponse, TokenData
@@ -14,7 +15,6 @@
1415
get_auth_data,
1516
get_parsed_token_data,
1617
update_auth_file,
17-
update_env_file,
1818
)
1919

2020
console = ConsoleLogger()

src/uipath/_cli/_auth/_utils.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import base64
21
import json
32
import os
43
from pathlib import Path
54
from typing import Optional
65

6+
from ..._utils._auth import parse_access_token
77
from ._models import AccessTokenData, TokenData
88

99

@@ -21,31 +21,7 @@ def get_auth_data() -> TokenData:
2121
return json.load(open(auth_file))
2222

2323

24-
def parse_access_token(access_token: str) -> AccessTokenData:
25-
token_parts = access_token.split(".")
26-
if len(token_parts) < 2:
27-
raise Exception("Invalid access token")
28-
payload = base64.urlsafe_b64decode(
29-
token_parts[1] + "=" * (-len(token_parts[1]) % 4)
30-
)
31-
return json.loads(payload)
32-
33-
3424
def get_parsed_token_data(token_data: Optional[TokenData] = None) -> AccessTokenData:
3525
if not token_data:
3626
token_data = get_auth_data()
3727
return parse_access_token(token_data["access_token"])
38-
39-
40-
def update_env_file(env_contents):
41-
env_path = Path.cwd() / ".env"
42-
if env_path.exists():
43-
with open(env_path, "r") as f:
44-
for line in f:
45-
if "=" in line:
46-
key, value = line.strip().split("=", 1)
47-
if key not in env_contents:
48-
env_contents[key] = value
49-
lines = [f"{key}={value}\n" for key, value in env_contents.items()]
50-
with open(env_path, "w") as f:
51-
f.writelines(lines)

src/uipath/_services/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .connections_service import ConnectionsService
77
from .context_grounding_service import ContextGroundingService
88
from .entities_service import EntitiesService
9+
from .external_application_service import ExternalApplicationService
910
from .folder_service import FolderService
1011
from .jobs_service import JobsService
1112
from .llm_gateway_service import UiPathLlmChatService, UiPathOpenAIService
@@ -27,4 +28,5 @@
2728
"UiPathLlmChatService",
2829
"FolderService",
2930
"EntitiesService",
31+
"ExternalApplicationService",
3032
]

0 commit comments

Comments
 (0)