feat(business-rules): add BusinessRulesService with run() - #1912
ashishupadhyay88 wants to merge 13 commits into
Conversation
Adds sdk.business_rules, a client for the Business Rules service that evaluates a DMN business rule deployed to Orchestrator against one input. - evaluate / evaluate_async: single input in, decisions out; the service's batch contract stays internal, matching the .NET client - folder scoping by folder_key or folder_path (resolved to a key, as the service accepts keys only), falling back to UIPATH_FOLDER_KEY/PATH - client-side validation of rule name and input size, mirroring the service and the .NET client - overall status (Success / PartialSuccess / AllFailed) derived from decision- and input-level errors; 207 partial results are returned, error envelopes raise EnrichedException - auth, retry, tenant URL scoping and trace propagation are inherited from BaseService Bumps uipath-platform to 0.2.33. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Fix the folder key/path presence check and add coverage for the documented UIPATH_FOLDER_PATH fallback.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Adds Business Rules evaluation support to uipath-platform, including sync/async APIs, validation, folder resolution, and response handling.
Changes:
- Added Business Rules models and service client.
- Exposed
UiPath.business_rules. - Added tests, documentation, and version updates.
| File | Reviewed changes |
|---|---|
packages/uipath/uv.lock |
Dependency lock update |
packages/uipath-platform/uv.lock |
Platform version lock update |
packages/uipath-platform/tests/services/test_business_rules_service.py |
Service behavior tests |
packages/uipath-platform/src/uipath/platform/business_rules/business_rules.py |
Evaluation models and statuses |
packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py |
Evaluation service, validation, folder resolution, and response mapping |
packages/uipath-platform/src/uipath/platform/business_rules/__init__.py |
Public API exports |
packages/uipath-platform/src/uipath/platform/_uipath.py |
business_rules client integration |
packages/uipath-platform/pyproject.toml |
Package version bump |
packages/uipath-platform/CLAUDE.md |
Service documentation |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Replaces the public evaluate()/evaluate_async() with run()/run_async(), modelled on the .NET client's RunAsync: the caller passes a run context (DeployedRunContext(rule_name, version)) and never picks an endpoint. The result is BusinessRuleRunResult, stamped with the RunMode that ran. The evaluate request builder stays private, so a debug run context can be added later without changing the public entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tested against alphaThis tests Setup
What this confirms
Checks in the locked uv environment (Python 3.11)
|
- Add sync and async tests for the UIPATH_FOLDER_PATH env fallback, which resolves the path to a key before sending x-uipath-folderkey, and a test that UIPATH_FOLDER_KEY wins over UIPATH_FOLDER_PATH without a lookup (Copilot review). - Build DeployedRunContext outside pytest.raises so each block has a single call that can raise (Sonar python:S5778). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sync() Callers can now pass TraceContext(trace_id, parent_span_id), like the .NET client's TraceContext, to file the run's spans under a trace of their choosing. When it is omitted the header stays automatic: the trace from UIPATH_TRACE_ID and the current span, as for every service. - TraceContext validates at construction: 32-hex (or UUID) trace id, 16-hex parent span id, neither all zeros; ids are normalised. - BaseService always sets the ambient header, so a request hook on this service's own sync and async httpx clients replaces it with the explicit value just before sending. Shared code is unchanged. - The explicit value lives in a ContextVar for the duration of the call, so it never leaks into the next call or across concurrent async runs, and it survives retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NishankSiddharth
left a comment
There was a problem hiding this comment.
do we need to add get, list methods for business rules (listing BRs, getting details for BRs)?
Addresses review on #1912: - run()/run_async() take the rule name as the first parameter, like processes.invoke(name, input_arguments): run("Loan Pricing", {"age": 14}, version=..., folder_path=...). DeployedRunContext is removed (never released); version is a keyword. - @resource_override(resource_type="businessRule") on both, so a solution's bindings can remap the rule name and folder per environment. "businessRule" is the Studio resource kind. - Add "businessRule" to GenericResourceOverwrite so such bindings parse, as done for memorySpace (#1586) and remoteA2aAgent (#1581). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #1912's review fix (name-first run() with resource overrides) into the debug PR and reshapes debug runs to match: - run(name, input, *, debug=DebugRunContext(...)): the rule name is the first parameter for both modes, so bindings can remap it for debug runs too. DebugRunContext drops rule_name and keeps project_id, file_name, job_key and organization_unit_id. - Without project_id, the run resolves the project from the job's lineage and needs job_key (or UIPATH_JOB_KEY) and organization_unit_id. - version applies to deployed rules only and is rejected with debug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses review: describe the service in product terms in the package guide and docstrings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #1912's wording change into the debug PR and rewords the debug docstrings and field descriptions the same way. Literal .dmn file names in examples and tests are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes Sonar python:S7503 on the async request hook, which had to be `async` for httpx but awaited nothing. The explicit trace_context is now carried in the headers passed to BaseService: a small dict that ignores BaseService's later write of the ambient trace header when an explicit one is set. This removes both httpx request hooks, the async hook function and the ContextVar. Each call gets its own headers, so nothing leaks between calls or across concurrent async runs, and retries reuse them (new test). Shared code is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| if folder_key and folder_path: | ||
| raise ValueError("Only one of folder_key or folder_path can be provided") |
| if input is None: | ||
| raise ValueError("input must not be None") | ||
| if len(input) > _MAX_INPUT_KEYS: | ||
| raise ValueError(f"input must not exceed {_MAX_INPUT_KEYS} keys") |
…t non-dict input Addresses Copilot review on #1912: - A businessRule binding supplies its folder as a path. The override decorator on run() replaced folder_path but left the caller's folder_key, so run() saw both and raised "Only one of folder_key or folder_path". The binding is now applied by a private helper (_binding, decorated with resource_override); when it remaps the rule, its folder replaces whichever folder the caller gave, including folder_key. Sync and async tests cover it, plus a non-matching override keeping the caller's key. - input must be a mapping: a list or string is now a ValueError before anything is sent, instead of being posted as inputs[0].data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run()/run_async() now take the folder three ways: folder_key (sent as is), folder_path (looked up, sent as the key) or organization_unit_id (sent as-is as x-uipath-organizationunitid). The service accepts the numeric id for a deployed rule and prefers the key when both are sent. - A folder is required in one of those forms; explain=True still needs a folder key, since the numeric id can't stand in for it. - folder_key and folder_path stay exclusive, as elsewhere in the SDK. - The environment's folder only fills a gap: it isn't added when the caller named the folder by its numeric id, except to supply the key that explain=True needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… debug too Brings #1912's organization_unit_id into the debug PR and applies the "require only what's needed, accept optional values" rule to debug runs: - organization_unit_id moves from DebugRunContext to run(), next to folder_key / folder_path. Job-lineage mode requires it (and a job key, explicit or UIPATH_JOB_KEY); project mode needs no folder at all. - Project mode no longer rejects a job key or folder id the caller passes: they're sent as given. UIPATH_JOB_KEY is still not added on its own, and businessRuleName is still never sent with projectId. - version is sent with debug runs when given, instead of being rejected. - explain=True needs a folder key in every mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main released 0.2.33 (#1920), so this PR moves to the next version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1912 now takes 0.2.34 (main released 0.2.33), so this PR moves to the next version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🚨 Heads up:
|
Tested in a coded agent on alpha (
|
| Job | Question | Tool calls | Result |
|---|---|---|---|
0e3e9b0a… |
Category for a 78-year-old | run_business_rule({"age": 78}) |
✅ Deployed · Success · adult · 1.1.5 |
84c64b3a… |
Studio version, age 14, explain=true, no folder key passed |
debug_business_rule({"age": 14}) |
✅ Debug · Success · minor · Business rule.dmn |
5e4d9370… |
Compare 14, 35 and 78, explain=true |
3 × run_business_rule |
✅ Deployed · Success · minor / adult / adult |
Local runs before deploying: 5/5 correct with uip codedagent run, and a 3-case smoke eval scored 3/3 (1.0).
What this shows:
- In a job, the folder comes from the environment. In job 2,
explain=trueneeds a folder key and the agent passed none. The SDK readUIPATH_FOLDER_KEYfrom the job and the service accepted it. run_asyncworks inside LangGraph, including several calls in one turn.- No extra auth setup. The robot token and the gateway-injected account and tenant headers were enough.
Not checked: nesting of the Business Rules span in the agent trace. uip or jobs traces only serves Agent-type processes.





Summary
Jira: MST-15365
Adds
sdk.business_rules, a client for the Business Rules service. It runs a business rule deployed to Orchestrator against one input.This is PR 1 of 2. #1917 adds debug runs of undeployed rules in Studio projects.
run_async()has the same signature, for callers already running on an event loop, such as LangGraph or FastAPI.Flow
flowchart TD A["run(name, input) / run_async(name, input)"] --> O{"businessRule binding<br/>override for name?"} O -- yes --> O1["name and folder replaced<br/>(binding folder wins over folder_key)"] --> V O -- no --> V{"name safe, input a mapping<br/>with at most 256 keys?"} V -- no --> X["ValueError, nothing sent"] V -- yes --> F{"folder"} F -- "folder_key and folder_path" --> X F -- "folder_key" --> K["folder key"] F -- "folder_path" --> L["FolderService lookup"] --> K F -- "organization_unit_id only" --> U["numeric id, no env fallback"] F -- "nothing: UIPATH_FOLDER_KEY / _PATH" --> K F -- "nothing at all" --> X K --> EX{"explain=True?"} U --> EX EX -- "yes, no folder key" --> X EX -- ok --> EV["POST …/businessrules_/v1/business-rules/evaluate<br/>x-uipath-folderkey if known, x-uipath-organizationunitid if given<br/>businessRuleName, version?, decisionNames?, explain,<br/>inputs: [id: input-1, data]"] EV --> T{"trace_context?"} T -- yes --> T1["x-uipath-traceparent-id = explicit"] T -- no --> T2["x-uipath-traceparent-id = UIPATH_TRACE_ID / current span"] T1 --> BS["BaseService: bearer token, /org/tenant URL, retries"] T2 --> BS BS --> R{"HTTP status"} R -- "4xx / 5xx" --> E["EnrichedException"] R -- "200 / 207" --> S["result for input-1 → status<br/>Success / PartialSuccess / AllFailed"]Design
The request and result behaviour follows the .NET client in
business-rules/clients/dotnet. The call shape follows this SDK's resource conventions. The SDK checks only what's required; optional values the caller passes are sent as given.Name first, like every resource call.
run(name, input, *, version=None, …)matchesprocesses.invoke(name, input_arguments). The endpoint and the request builder (_evaluate_spec) are private.Folder, in any of three forms:
folder_key, sent as isfolder_path, looked up throughFolderServiceand sent as its keyorganization_unit_id, the numeric folder id, sent as is asx-uipath-organizationunitidThe service accepts either the key or the numeric id and prefers the key (business-rules cli: langchain enable traces #55); both are sent when both are given.
UIPATH_FOLDER_KEY, thenUIPATH_FOLDER_PATH). Withexplain=True, a folder key is required; the numeric id alone isn't enough.folder_keyandfolder_pathtogether are aValueError, as inheader_folder()across the SDK.explain=Trueneeds.Resource overrides. A solution's
businessRulebindings can remap the rule name and folder per environment, through a private helper decorated with@resource_override(resource_type="businessRule"). When a binding remaps the rule, its folder replaces the caller's, including afolder_key.Single input in, decisions out. The service's batch format stays internal. The SDK sends one input with id
input-1and returns only its result as aBusinessRuleRunResult, stamped withRunMode.DEPLOYED.Checks before sending:
/,\,..,%or control charactersStatus. An input-level error means
AllFailed. Otherwise the status comes from how many decisions failed. A 207 partial result is returned normally, withtop_level_errorsuch asBATCH_TIMEOUT. Error responses raiseEnrichedException.Trace header.
x-uipath-traceparent-idis automatic by default, fromUIPATH_TRACE_IDand the current span.trace_context=TraceContext(trace_id, parent_span_id), the same shape as .NET's, with the ids checked when it's created.BaseService, as a small dict that ignoresBaseService's later write of the ambient header. There are no request hooks and no shared-code change, and it survives retries.Inherited from
BaseService:UIPATH_SERVICE_URL_BUSINESSRULESlocal overrideFor reviewers
x-uipath-internal-accountid/-tenantid; the gateway adds them, confirmed on alpha."businessRule"inGenericResourceOverwrite(common/_bindings.py), as feat: add memorySpace to resource overwrite types #1586 / fix: accept remoteA2aAgent in GenericResourceOverwrite #1581 did for their kinds.uipath-platformgoes from 0.2.32 to 0.2.33.uv lock --checkpasses for both packages.@resource_overridefolder_keyUIPATH_FOLDER_PATHtestsTest plan
tests/services/test_business_rules_service.py, covering:explainneeding a key)businessRuleoverrides (including overfolder_key)run_asyncuipath-platformsuite passes: 1800 passed, 7 skipped (live credentials). Theuipathoverride and bindings tests also pass.ruff check,ruff format --checkandmypy src testsare clean in the locked uv environment.uip function run), with the combined branch (feat(business-rules): add debug runs to run() #1917): 60 passed, 0 failed.explainoff and on, checked on the wirex-uipath-organizationunitidsent →Success)explain; the numeric id with a different env folder (env not added)explain=True→ValueError🤖 Generated with Claude Code