Skip to content

Commit baa54e0

Browse files
feat(business-rules): add debug runs to run()
Adds DebugRunContext, the second run context of run()/run_async(), for undeployed DMNs read from a Studio project. As in the .NET client's RunAsync, exactly one of deployed/debug must be set and it selects the endpoint: - DeployedRunContext -> /v1/business-rules/evaluate - DebugRunContext(project_id | rule_name, file_name, job_key, organization_unit_id) -> /v1/business-rules/debug/evaluate A debug run named by rule_name also needs job_key (defaults to UIPATH_JOB_KEY) and organization_unit_id. Following business-rules#104, explain=True needs a folder key in both modes, and debug runs send the key whenever one is known so their spans can be stored. BusinessRuleRunResult gains project_id and file_name for debug runs, and RunMode gains DEBUG. Bumps uipath-platform to 0.2.34. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3795b30 commit baa54e0

8 files changed

Lines changed: 350 additions & 30 deletions

File tree

‎packages/uipath-platform/CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Services provide both sync and async variants (e.g., `.invoke()` and `.invoke_as
9696
| `action_center/` | Task management for human-in-the-loop workflows |
9797
| `agenthub/` | System agents and LLM model discovery |
9898
| `automation_tracker/` | Business Transaction Service (BTS) for Process Mining |
99-
| `business_rules/` | DMN business rule runs for rules deployed to Orchestrator, behind one `run()` |
99+
| `business_rules/` | DMN business rule runs: deployed rules or undeployed Studio-project DMNs, behind one `run()` |
100100
| `chat/` | LLM gateway, conversations, throttling |
101101
| `connections/` | External connection management |
102102
| `context_grounding/` | RAG services (DeepRAG, batch RAG, ephemeral indexes) |

‎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.2.33"
3+
version = "0.2.34"
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/business_rules/__init__.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Business Rules service package.
22
3-
Provides the ``BusinessRulesService`` client for running DMN decision models
4-
deployed to Orchestrator as UiPath Business Rules, and the Pydantic models for
5-
its run context and results.
3+
Provides the ``BusinessRulesService`` client for running DMN decision models,
4+
either deployed to Orchestrator as UiPath Business Rules or read from a Studio
5+
project, and the Pydantic models for its run contexts and results.
66
"""
77

88
from ._business_rules_service import BusinessRulesService
@@ -11,6 +11,7 @@
1111
BusinessRuleError,
1212
BusinessRuleRunResult,
1313
BusinessRuleStatus,
14+
DebugRunContext,
1415
DeployedRunContext,
1516
RunMode,
1617
)
@@ -21,6 +22,7 @@
2122
"BusinessRuleRunResult",
2223
"BusinessRuleStatus",
2324
"BusinessRulesService",
25+
"DebugRunContext",
2426
"DeployedRunContext",
2527
"RunMode",
2628
]

‎packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py‎

Lines changed: 128 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Business Rules service for UiPath Platform.
22
3-
Runs DMN decision models deployed to Orchestrator as business rules.
3+
Runs DMN decision models: a business rule deployed to Orchestrator, or an
4+
undeployed DMN read straight from a Studio project.
45
"""
56

67
from typing import Any, Dict, List, Optional, Tuple
78

89
from uipath.core.tracing import traced
910

1011
from ..common._base_service import BaseService
11-
from ..common._config import UiPathApiConfig
12+
from ..common._config import UiPathApiConfig, UiPathConfig
1213
from ..common._execution_context import UiPathExecutionContext
1314
from ..common._folder_context import FolderContext
1415
from ..common._models import Endpoint, RequestSpec
@@ -19,13 +20,18 @@
1920
BusinessRuleError,
2021
BusinessRuleRunResult,
2122
BusinessRuleStatus,
23+
DebugRunContext,
2224
DeployedRunContext,
2325
RunMode,
2426
_WireResponse,
2527
_WireResult,
2628
)
2729

2830
_EVALUATE_ENDPOINT = Endpoint("businessrules_/v1/business-rules/evaluate")
31+
_DEBUG_EVALUATE_ENDPOINT = Endpoint("businessrules_/v1/business-rules/debug/evaluate")
32+
33+
_HEADER_ORGANIZATION_UNIT_ID = "x-uipath-organizationunitid"
34+
_HEADER_JOB_KEY = "x-uipath-jobkey"
2935

3036
# The service's contract is a batch; this SDK submits exactly one input under this id.
3137
_SINGLE_INPUT_ID = "input-1"
@@ -37,8 +43,9 @@ class BusinessRulesService(FolderContext, BaseService):
3743
"""Service for running UiPath Business Rules (DMN decision models).
3844
3945
Each call runs one input and returns the decisions it produced. Which model
40-
runs is set by the run context; ``deployed`` names a rule deployed to
41-
Orchestrator. The caller never picks a service endpoint.
46+
runs is set by the run context: ``deployed`` for a rule deployed to
47+
Orchestrator, ``debug`` for an undeployed DMN in a Studio project. The caller
48+
never picks a service endpoint.
4249
"""
4350

4451
def __init__(
@@ -55,25 +62,32 @@ def run(
5562
self,
5663
input: Dict[str, Any],
5764
*,
58-
deployed: DeployedRunContext,
65+
deployed: Optional[DeployedRunContext] = None,
66+
debug: Optional[DebugRunContext] = None,
5967
decision_names: Optional[List[str]] = None,
6068
explain: bool = False,
6169
folder_key: Optional[str] = None,
6270
folder_path: Optional[str] = None,
6371
) -> BusinessRuleRunResult:
6472
"""Run a business rule against one input.
6573
74+
Exactly one of ``deployed`` and ``debug`` must be set, and it decides which
75+
model runs.
76+
6677
Args:
6778
input: The input to run, keyed by DMN input name. Declared inputs absent
6879
from it bind to null.
69-
deployed: The business rule deployed to Orchestrator to run.
80+
deployed: A business rule deployed to Orchestrator.
81+
debug: An undeployed DMN in a Studio project.
7082
decision_names: The decisions to evaluate; defaults to the whole model.
7183
explain: Whether to record condition-level explanations in the trace.
84+
Requires a folder.
7285
folder_key: The key of the folder to run in.
7386
folder_path: The path of the folder to run in. Resolved to a key, since
7487
the service accepts folder keys only.
7588
76-
A folder is required. When neither ``folder_key`` nor ``folder_path`` is given, it falls back to
89+
A folder is required for a deployed rule and whenever ``explain`` is set.
90+
When neither ``folder_key`` nor ``folder_path`` is given, it falls back to
7791
``UIPATH_FOLDER_KEY`` and then ``UIPATH_FOLDER_PATH``.
7892
7993
Returns:
@@ -87,24 +101,36 @@ def run(
87101
Examples:
88102
```python
89103
from uipath.platform import UiPath
90-
from uipath.platform.business_rules import DeployedRunContext
104+
from uipath.platform.business_rules import (
105+
DebugRunContext,
106+
DeployedRunContext,
107+
)
91108
92109
client = UiPath()
93110
111+
# A rule deployed to Orchestrator
94112
result = client.business_rules.run(
95113
{"creditScore": 740, "age": 34},
96114
deployed=DeployedRunContext(rule_name="Loan Pricing"),
97115
folder_path="Finance",
98116
)
99117
for decision in result.decisions:
100118
print(decision.decision_name, decision.outputs)
119+
120+
# An undeployed DMN in a Studio project
121+
result = client.business_rules.run(
122+
{"creditScore": 740},
123+
debug=DebugRunContext(project_id="0a1b2c3d-...", file_name="Loan.dmn"),
124+
)
101125
```
102126
"""
103-
_validate_run(input, deployed)
127+
_validate_run(input, deployed, debug)
104128
key, path = self._folder_source(folder_key, folder_path)
105129
if path:
106130
key = self._folders_service.retrieve_folder_key(path)
107-
mode, spec = self._run_spec(input, deployed, decision_names, explain, key)
131+
mode, spec = self._run_spec(
132+
input, deployed, debug, decision_names, explain, key
133+
)
108134
response = self.request(
109135
spec.method,
110136
url=spec.endpoint,
@@ -119,20 +145,26 @@ async def run_async(
119145
self,
120146
input: Dict[str, Any],
121147
*,
122-
deployed: DeployedRunContext,
148+
deployed: Optional[DeployedRunContext] = None,
149+
debug: Optional[DebugRunContext] = None,
123150
decision_names: Optional[List[str]] = None,
124151
explain: bool = False,
125152
folder_key: Optional[str] = None,
126153
folder_path: Optional[str] = None,
127154
) -> BusinessRuleRunResult:
128155
"""Asynchronously run a business rule against one input.
129156
157+
Exactly one of ``deployed`` and ``debug`` must be set, and it decides which
158+
model runs.
159+
130160
Args:
131161
input: The input to run, keyed by DMN input name. Declared inputs absent
132162
from it bind to null.
133-
deployed: The business rule deployed to Orchestrator to run.
163+
deployed: A business rule deployed to Orchestrator.
164+
debug: An undeployed DMN in a Studio project.
134165
decision_names: The decisions to evaluate; defaults to the whole model.
135166
explain: Whether to record condition-level explanations in the trace.
167+
Requires a folder.
136168
folder_key: The key of the folder to run in.
137169
folder_path: The path of the folder to run in. Resolved to a key, since
138170
the service accepts folder keys only.
@@ -145,11 +177,13 @@ async def run_async(
145177
ValueError: If the request is invalid or a required folder is missing.
146178
EnrichedException: If the service rejects the request.
147179
"""
148-
_validate_run(input, deployed)
180+
_validate_run(input, deployed, debug)
149181
key, path = self._folder_source(folder_key, folder_path)
150182
if path:
151183
key = await self._folders_service.retrieve_folder_key_async(path)
152-
mode, spec = self._run_spec(input, deployed, decision_names, explain, key)
184+
mode, spec = self._run_spec(
185+
input, deployed, debug, decision_names, explain, key
186+
)
153187
response = await self.request_async(
154188
spec.method,
155189
url=spec.endpoint,
@@ -176,11 +210,19 @@ def _folder_source(
176210
def _run_spec(
177211
self,
178212
input: Dict[str, Any],
179-
deployed: DeployedRunContext,
213+
deployed: Optional[DeployedRunContext],
214+
debug: Optional[DebugRunContext],
180215
decision_names: Optional[List[str]],
181216
explain: bool,
182217
folder_key: Optional[str],
183218
) -> Tuple[RunMode, RequestSpec]:
219+
if debug is not None:
220+
if explain and not folder_key:
221+
raise _missing_folder("explain=True")
222+
return RunMode.DEBUG, self._debug_spec(
223+
input, debug, decision_names, explain, folder_key
224+
)
225+
assert deployed is not None
184226
if not folder_key:
185227
raise _missing_folder("a deployed business rule")
186228
return RunMode.DEPLOYED, self._evaluate_spec(
@@ -211,6 +253,56 @@ def _evaluate_spec(
211253
headers={HEADER_FOLDER_KEY: folder_key},
212254
)
213255

256+
def _debug_spec(
257+
self,
258+
input: Dict[str, Any],
259+
debug: DebugRunContext,
260+
decision_names: Optional[List[str]],
261+
explain: bool,
262+
folder_key: Optional[str],
263+
) -> RequestSpec:
264+
job_key = debug.job_key or UiPathConfig.job_key
265+
named = _present(debug.rule_name)
266+
if not _present(debug.project_id) and not _present(job_key):
267+
raise ValueError(
268+
"debug.job_key must be specified when the run is named by rule_name: "
269+
"the service resolves the project from the job's lineage. "
270+
"Set it or UIPATH_JOB_KEY."
271+
)
272+
if named and not _present(debug.organization_unit_id):
273+
raise ValueError(
274+
"debug.organization_unit_id must be specified when the run is named "
275+
"by rule_name: it is the folder the job's lineage is read under"
276+
)
277+
278+
body: Dict[str, Any] = {
279+
"explain": explain,
280+
"inputs": [{"id": _SINGLE_INPUT_ID, "data": input}],
281+
}
282+
if debug.project_id:
283+
body["projectId"] = debug.project_id
284+
if named:
285+
body["businessRuleName"] = debug.rule_name
286+
if debug.file_name:
287+
body["fileName"] = debug.file_name
288+
if decision_names:
289+
body["decisionNames"] = decision_names
290+
291+
# Folder key: the traces service files the run's spans under it.
292+
headers: Dict[str, str] = {}
293+
if folder_key:
294+
headers[HEADER_FOLDER_KEY] = folder_key
295+
if debug.organization_unit_id:
296+
headers[_HEADER_ORGANIZATION_UNIT_ID] = debug.organization_unit_id
297+
if job_key:
298+
headers[_HEADER_JOB_KEY] = job_key
299+
return RequestSpec(
300+
method="POST",
301+
endpoint=_DEBUG_EVALUATE_ENDPOINT,
302+
json=body,
303+
headers=headers,
304+
)
305+
214306

215307
def _present(value: Optional[str]) -> bool:
216308
# Blank counts as absent, matching how the service reads these fields.
@@ -224,10 +316,22 @@ def _missing_folder(needed_for: str) -> ValueError:
224316
)
225317

226318

227-
def _validate_run(input: Dict[str, Any], deployed: DeployedRunContext) -> None:
228-
if deployed is None:
229-
raise ValueError("deployed must be set")
230-
_validate_rule_name(deployed.rule_name, "deployed.rule_name")
319+
def _validate_run(
320+
input: Dict[str, Any],
321+
deployed: Optional[DeployedRunContext],
322+
debug: Optional[DebugRunContext],
323+
) -> None:
324+
if deployed is None and debug is None:
325+
raise ValueError("Exactly one of deployed or debug must be set; neither was")
326+
if deployed is not None and debug is not None:
327+
raise ValueError("Exactly one of deployed or debug must be set; both were")
328+
if deployed is not None:
329+
_validate_rule_name(deployed.rule_name, "deployed.rule_name")
330+
if debug is not None:
331+
if not _present(debug.project_id) and not _present(debug.rule_name):
332+
raise ValueError("debug.project_id or debug.rule_name must be specified")
333+
if _present(debug.rule_name):
334+
_validate_rule_name(debug.rule_name, "debug.rule_name") # type: ignore[arg-type]
231335
_validate_input(input)
232336

233337

@@ -285,12 +389,15 @@ def _single_result(
285389

286390
def _to_run_result(mode: RunMode, response: _WireResponse) -> BusinessRuleRunResult:
287391
decisions, errors, status = _single_result(response)
392+
deployed = mode == RunMode.DEPLOYED
288393
return BusinessRuleRunResult(
289394
mode=mode,
290395
status=status,
291396
decisions=decisions,
292397
errors=errors,
293398
top_level_error=response.error.code if response.error else None,
294-
business_rule_name=response.business_rule_name,
295-
version=response.version,
399+
business_rule_name=response.business_rule_name if deployed else None,
400+
version=response.version if deployed else None,
401+
project_id=None if deployed else response.project_id,
402+
file_name=None if deployed else response.file_name,
296403
)

0 commit comments

Comments
 (0)