Skip to content

Commit 364e632

Browse files
cristipufuionmincu
authored andcommitted
feat: add connections retrieve payload
1 parent 2ebde90 commit 364e632

File tree

16 files changed

+1649
-858
lines changed

16 files changed

+1649
-858
lines changed

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"
3-
version = "2.1.17"
3+
version = "2.1.18"
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.10"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

samples/event-trigger/README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# UiPath Coded Agents with Event Triggers
2+
3+
This guide explains how to create Python-based UiPath Coded Agents that respond to event triggers, enabling seamless event-driven agents.
4+
5+
## Overview
6+
7+
UiPath Coded Agents allow you to write automation logic directly in Python while leveraging UiPath's event trigger system. This project demonstrates how to create agents that handle external events from systems like Gmail, Slack, and other connectors.
8+
9+
## How to Set Up UiPath Coded Agents with Event Triggers
10+
11+
### Step 1: Install UiPath Python SDK
12+
13+
1. Open it with your prefered editor
14+
2. In terminal run:
15+
```bash
16+
uv init
17+
uv add uipath
18+
uv run uipath new event-agent
19+
uv run uipath init
20+
```
21+
22+
### Step 2: Create Your Coded Agent
23+
24+
Create a Python file with your agent logic using the UiPath SDK:
25+
26+
```python
27+
from dataclasses import dataclass
28+
from uipath.models import EventArguments
29+
from uipath import UiPath
30+
import logging
31+
32+
logger = logging.getLogger(__name__)
33+
34+
@dataclass
35+
class EchoOut:
36+
message: dict
37+
38+
# use EventArguments when called by UiPath EventTriggers
39+
def main(input: EventArguments) -> EchoOut:
40+
sdk = UiPath()
41+
42+
# get the event payload, this will be different from event to event
43+
payload = sdk.connections.retrieve_event_payload(input)
44+
45+
logger.info(f"Received payload: {payload}")
46+
47+
return EchoOut(payload)
48+
```
49+
50+
Run `uipath init` again to update the input arguments.
51+
52+
### Step 3: Understanding the Event Flow
53+
54+
When an event trigger fires, UiPath will:
55+
1. Pass event data through `EventArguments`
56+
2. Your agent retrieves the full payload using `sdk.connections.retrieve_event_payload(input)`
57+
3. Process the payload based on your business logic
58+
4. Return structured output
59+
60+
### Step 4: Publish Your Coded Agent and setup Event Trigger
61+
62+
#### 4.1: Build and Publish
63+
1. Use `uipath pack` and `uipath publish` to create and publish the package
64+
2. Create an Orchestrator Automation from the published process
65+
66+
#### 4.2: Access Event Triggers
67+
1. Log into UiPath Orchestrator
68+
2. Navigate to **Automations****Processes**
69+
3. Click on your coded workflow process
70+
71+
#### 4.3: Create Event Trigger
72+
1. Go to the **Triggers** tab
73+
2. Click **Add Trigger****Event Trigger**
74+
75+
#### 4.4: Configure Event Trigger Settings
76+
1. **Name**: Descriptive name (e.g., "Gmail Event Handler")
77+
2. **Event Source**: Select connector type:
78+
- `uipath-google-gmailcustom` for Gmail
79+
- `uipath-slack` for Slack
80+
- `uipath-microsoft-outlookcustom` for Outlook
81+
- Custom connectors
82+
3. **Event Type**: Choose specific event:
83+
- `EMAIL_RECEIVED` for emails
84+
- `MESSAGE_RECEIVED` for chat messages
85+
- Custom event types
86+
4. **Filters**: Optional event filtering criteria
87+
88+
#### 4.5: Map Event to Input Arguments
89+
The event data will automatically be passed to your `EventArguments` parameter.
90+
91+
#### 4.6: Enable the Trigger
92+
1. Review configuration
93+
2. Click **Create** to save
94+
3. Ensure trigger status is **Enabled**
95+
96+
### Step 5: Test Your Setup
97+
98+
#### 5.1: Trigger Test Events
99+
- Send test email (Gmail triggers)
100+
- Post message in Slack (Slack triggers)
101+
- Perform action matching your event source
102+
103+
#### 5.2: Monitor Execution
104+
1. Check **Monitoring****Jobs** in Orchestrator
105+
2. View job details and execution logs
106+
3. Verify your coded agent processed the event correctly
107+
108+
#### 5.3: Debug with Logs
109+
110+
```python
111+
import logging
112+
113+
logger = logging.getLogger(__name__)
114+
115+
def main(input: EventArguments) -> EchoOut:
116+
sdk = UiPath()
117+
118+
# payload will be a json (dict) specific to your event
119+
payload = sdk.connections.retrieve_event_payload(input)
120+
logger.info(f"Successfully retrieved payload: {type(payload)}")
121+
logger.debug(f"Payload details: {payload}")
122+
123+
# Your processing logic here
124+
result = process_event(payload)
125+
126+
logger.info(f"Event processed successfully: {result}")
127+
return EchoOut(result)
128+
```

samples/event-trigger/main.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dataclasses import dataclass
2+
from uipath.models import EventArguments
3+
from uipath import UiPath
4+
from uipath.tracing import traced
5+
import logging
6+
7+
logger = logging.getLogger(__name__)
8+
9+
@dataclass
10+
class EchoOut:
11+
message: dict
12+
13+
@traced()
14+
def handle_slack_event(payload: dict[str, any]) -> EchoOut:
15+
"""Handle Slack message events"""
16+
message = payload['event']['text'] if 'event' in payload and 'text' in payload['event'] else "No message"
17+
user = payload['event']['user'] if 'event' in payload and 'user' in payload['event'] else "Unknown user"
18+
19+
logger.info(f"Slack message from {user}: {message}")
20+
21+
22+
# use InputTriggerEventArgs when called by UiPath EventTriggers
23+
@traced()
24+
def main(input: EventArguments) -> EchoOut:
25+
sdk = UiPath()
26+
27+
# get the event payload, this will be different from event to event
28+
payload = sdk.connections.retrieve_event_payload(input)
29+
30+
handle_slack_event(payload)
31+
32+
logger.info(f"Received payload: {payload}")
33+
34+
return EchoOut(payload)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "event-agent"
3+
version = "0.0.5"
4+
description = "event-agent"
5+
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
6+
dependencies = [
7+
"uipath>=2.1.18",
8+
]
9+
requires-python = ">=3.10"

src/uipath/_cli/_runtime/_runtime.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from opentelemetry import trace
1212
from opentelemetry.sdk.trace import TracerProvider
1313
from opentelemetry.sdk.trace.export import BatchSpanProcessor
14+
from pydantic import BaseModel
1415

1516
from uipath.tracing import LlmOpsHttpExporter
1617

@@ -162,9 +163,11 @@ async def _execute_python_script(self, script_path: str, input_data: Any) -> Any
162163
input_param = params[0]
163164
input_type = input_param.annotation
164165

165-
# Case 2: Class or dataclass parameter
166+
# Case 2: Class, dataclass, or Pydantic model parameter
166167
if input_type != inspect.Parameter.empty and (
167-
is_dataclass(input_type) or hasattr(input_type, "__annotations__")
168+
is_dataclass(input_type)
169+
or self._is_pydantic_model(input_type)
170+
or hasattr(input_type, "__annotations__")
168171
):
169172
try:
170173
valid_type = cast(Type[Any], input_type)
@@ -216,7 +219,16 @@ async def _execute_python_script(self, script_path: str, input_data: Any) -> Any
216219
)
217220

218221
def _convert_to_class(self, data: Dict[str, Any], cls: Type[T]) -> T:
219-
"""Convert a dictionary to either a dataclass or regular class instance."""
222+
"""Convert a dictionary to either a dataclass, Pydantic model, or regular class instance."""
223+
# Handle Pydantic models
224+
try:
225+
if inspect.isclass(cls) and issubclass(cls, BaseModel):
226+
return cast(T, cls.model_validate(data))
227+
except TypeError:
228+
# issubclass can raise TypeError if cls is not a class
229+
pass
230+
231+
# Handle dataclasses
220232
if is_dataclass(cls):
221233
field_types = get_type_hints(cls)
222234
converted_data = {}
@@ -227,13 +239,17 @@ def _convert_to_class(self, data: Dict[str, Any], cls: Type[T]) -> T:
227239

228240
value = data[field_name]
229241
if (
230-
is_dataclass(field_type) or hasattr(field_type, "__annotations__")
242+
is_dataclass(field_type)
243+
or self._is_pydantic_model(field_type)
244+
or hasattr(field_type, "__annotations__")
231245
) and isinstance(value, dict):
232246
typed_field = cast(Type[Any], field_type)
233247
value = self._convert_to_class(value, typed_field)
234248
converted_data[field_name] = value
235249

236250
return cast(T, cls(**converted_data))
251+
252+
# Handle regular classes
237253
else:
238254
sig = inspect.signature(cls.__init__)
239255
params = sig.parameters
@@ -254,6 +270,7 @@ def _convert_to_class(self, data: Dict[str, Any], cls: Type[T]) -> T:
254270

255271
if (
256272
is_dataclass(param_type)
273+
or self._is_pydantic_model(param_type)
257274
or hasattr(param_type, "__annotations__")
258275
) and isinstance(value, dict):
259276
typed_param = cast(Type[Any], param_type)
@@ -265,22 +282,41 @@ def _convert_to_class(self, data: Dict[str, Any], cls: Type[T]) -> T:
265282

266283
return cls(**init_args)
267284

285+
def _is_pydantic_model(self, cls: Type[Any]) -> bool:
286+
"""Safely check if a class is a Pydantic model."""
287+
try:
288+
return inspect.isclass(cls) and issubclass(cls, BaseModel)
289+
except TypeError:
290+
# issubclass can raise TypeError if cls is not a class
291+
return False
292+
268293
def _convert_from_class(self, obj: Any) -> Dict[str, Any]:
269-
"""Convert a class instance (dataclass or regular) to a dictionary."""
294+
"""Convert a class instance (dataclass, Pydantic model, or regular) to a dictionary."""
270295
if obj is None:
271296
return {}
272297

273-
if is_dataclass(obj):
298+
# Handle Pydantic models
299+
if isinstance(obj, BaseModel):
300+
return obj.model_dump()
301+
302+
# Handle dataclasses
303+
elif is_dataclass(obj):
274304
# Make sure obj is an instance, not a class
275305
if isinstance(obj, type):
276306
return {}
277307
return asdict(obj)
308+
309+
# Handle regular classes
278310
elif hasattr(obj, "__dict__"):
279311
result = {}
280312
for key, value in obj.__dict__.items():
281313
# Skip private attributes
282314
if not key.startswith("_"):
283-
if hasattr(value, "__dict__") or is_dataclass(value):
315+
if (
316+
isinstance(value, BaseModel)
317+
or hasattr(value, "__dict__")
318+
or is_dataclass(value)
319+
):
284320
result[key] = self._convert_from_class(value)
285321
else:
286322
result[key] = value

src/uipath/_cli/_utils/_input_args.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
get_type_hints,
1515
)
1616

17+
from pydantic import BaseModel
18+
1719
SchemaType = Literal["object", "integer", "double", "string", "boolean", "array"]
1820

1921
TYPE_MAP: Dict[str, SchemaType] = {
@@ -50,7 +52,30 @@ def get_type_schema(type_hint: Any) -> Dict[str, Any]:
5052
return {"type": "object"}
5153

5254
if inspect.isclass(type_hint):
53-
if is_dataclass(type_hint):
55+
# Handle Pydantic models
56+
if issubclass(type_hint, BaseModel):
57+
properties = {}
58+
required = []
59+
60+
# Get the model fields
61+
model_fields = type_hint.model_fields
62+
63+
for field_name, field_info in model_fields.items():
64+
# Use alias if defined, otherwise use field name
65+
schema_field_name = field_info.alias if field_info.alias else field_name
66+
67+
# Get the field type schema
68+
field_schema = get_type_schema(field_info.annotation)
69+
properties[schema_field_name] = field_schema
70+
71+
# Check if field is required using Pydantic's built-in method
72+
if field_info.is_required():
73+
required.append(schema_field_name)
74+
75+
return {"type": "object", "properties": properties, "required": required}
76+
77+
# Handle dataclasses
78+
elif is_dataclass(type_hint):
5479
properties = {}
5580
required = []
5681

@@ -61,6 +86,8 @@ def get_type_schema(type_hint: Any) -> Dict[str, Any]:
6186
required.append(field.name)
6287

6388
return {"type": "object", "properties": properties, "required": required}
89+
90+
# Handle regular classes with annotations
6491
elif hasattr(type_hint, "__annotations__"):
6592
properties = {}
6693
required = []

0 commit comments

Comments
 (0)