feat(cli): generate bindings.json from resources referenced in code [PC-5017] - #1908
tudormatei1 wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Resolve the reported output, expression/no-folder handling, scanner correctness, and test coverage issues.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 5
Open (5)
What changed in this PR
Adds opt-in AST-based resource-binding generation for coded agents through bindings generate and init --infer-bindings.
Changes:
- Added decorator-driven registry, scanner, emitter, and merge logic.
- Added CLI integration, tests, documentation, and platform metadata exposure.
- Preserved existing bindings during inference.
| File | Description |
|---|---|
packages/uipath/tests/cli/test_cli_bindings.py |
Tests scanning, emission, merging, CLI behavior, and runtime keys. |
packages/uipath/src/uipath/_resources/CLI_REFERENCE.md |
Updates the generated CLI reference. |
packages/uipath/src/uipath/_cli/cli_init.py |
Adds opt-in binding inference during initialization. |
packages/uipath/src/uipath/_cli/cli_bindings.py |
Implements the bindings generation command. |
packages/uipath/src/uipath/_cli/_bindings/_scanner.py |
Scans Python ASTs for resource calls. |
packages/uipath/src/uipath/_cli/_bindings/_registry.py |
Builds binding specifications from SDK decorators. |
packages/uipath/src/uipath/_cli/_bindings/_emitter.py |
Generates and merges binding entries. |
packages/uipath/src/uipath/_cli/_bindings/_apply.py |
Shares inference, merge, and persistence behavior. |
packages/uipath/src/uipath/_cli/_bindings/__init__.py |
Exports binding utilities. |
packages/uipath/src/uipath/_cli/__init__.py |
Registers the lazy CLI command. |
packages/uipath/docs/cli/index.md |
Documents binding generation. |
packages/uipath/CLAUDE.md |
Documents the new CLI command. |
packages/uipath-platform/src/uipath/platform/common/_bindings.py |
Exposes decorator metadata for registry discovery. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
46952ef to
dbb0679
Compare
🚨 Heads up:
|
dbb0679 to
c6fc27f
Compare
| BINDABLE_RESOURCE_TYPES = frozenset( | ||
| {"asset", "process", "bucket", "index", "app", "connection"} | ||
| ) |
There was a problem hiding this comment.
nit: how do we determine this is the exhaustive list? Any plans to keep this in sync in the future if more bindable resource types appear?
There was a problem hiding this comment.
the true source of truth lives in specs/bindings.schema.json. this isnt includede in the wheel so it cannot be referenced at runtime. we could have a partity test that ensures these two things dont deviate?
| return None | ||
|
|
||
|
|
||
| def _spec_for_call( |
There was a problem hiding this comment.
Many invocations of bindable resources might not be done by directly calling the sdk but by using interrupt() with the built InvokeProcess/CreateTask/CreateEscalation, etc.
For example:
child_result = interrupt(
[
InvokeProcess(
name=CHILD_PROCESS_NAME,
process_folder_path=CHILD_PROCESS_FOLDER_PATH,
input_arguments={"message": state.message},
),
WaitUntil(resume_time=resume_time),
]
)All of these ultimately do call @resource_override decorated functions in their internals, but the current approach cannot see it.
Simples way would be to explicitly scan for the constructors of the known interrupt models that invoke bindable resources.
| def _resolve(node: ast.expr, constants: dict[str, str]) -> tuple[str, bool]: | ||
| """Return the value and whether it had to be kept as an expression.""" | ||
| if isinstance(node, ast.Constant) and isinstance(node.value, str): | ||
| return node.value, False | ||
| if isinstance(node, ast.Name) and node.id in constants: | ||
| return constants[node.id], False | ||
| return ast.unparse(node), True |
There was a problem hiding this comment.
This makes any non-literal argument as an expression binding.
Something like:
def get_asset(client, name, folder_path):
return client.assets.retrieve(name=name, folder_path=folder_path)
get_asset(sdk, "MyAsset", "Shared")will produce
{ "resource": "asset", "key": "name.folder_path",
"value": { "name": { "defaultValue": "name", "isExpression": true },
"folderPath": { "defaultValue": "folder_path", "isExpression": true } } }This happens whenever the rhs of a parameter is not a literal or a constant value, can happen with f-strings, named variables or function invocations.
I am not sure who exactly is able to interpret expression-type bindings or what they are, but I am fairly certain they won't be able to do much with the python variable names.
So the runtime will look for a literal name.folder_path resource , instead of MyAsset.Shared.
Similarly push will create junk virtual solution resources.
Here we should skip these and warn it.
There was a problem hiding this comment.
addressed, skipped and saying the values are computed at runtime.
The bundled reference had drifted from the commands it documents. Running scripts/update_agents_md.py picks up options added to `run` and `eval` since it was last generated, plus the new `bindings` group. Split out from the bindings change so the unrelated churn is reviewable on its own.
Coded agents have never had working resource bindings. `uipath init` writes an empty `resources` array and nothing ever fills it, so an agent's assets, buckets and processes are invisible to solutions, to package requirements and to deploy-time overrides. Low-code projects get the file written for them by the designer; coded ones got nothing. `uipath bindings generate` fills it by parsing the project's Python and matching calls against the SDK's own `@resource_override` decorators, which already declare the resource type and which parameter carries the name. The decorators are now readable as data rather than only from closure cells, so the scanner stays in step with the SDK instead of carrying a list that rots. Discovery is best effort and says so: literals and module-level constants are recorded, runtime expressions are kept as expressions, and a call whose name is built at runtime is reported with file and line rather than guessed. Entries already in the file are never rewritten, since they carry connector metadata and display names a scan cannot reproduce. `uipath init --infer-bindings` runs the same scan, making good on a flag the bundled agent docs have been advertising in four places without it existing. Inference is opt-in in both places; plain `uipath init` still writes an empty array, and a test pins that.
Two debug prints were dumping raw ScanResult and Bindings objects to stdout on every run, ahead of the real report. Constant folding only looked at top-level assignments, so a name changed by `X += ...` or rebound inside a branch was still folded to its first literal, producing a binding key the runtime would never match. Folding now requires the name to be bound exactly once anywhere in the module, counting augmented assignments, nested rebinds, imports and def/class shadowing. Also covers a positionally passed folder argument, which the registry supported but nothing exercised, and pins the push-time behaviour of a folderless binding instead of only asserting that some action came back.
uipath-platform 0.2.32 -> 0.2.33 and uipath 2.14.24 -> 2.14.25, since both packages carry source changes here and the versions on main are already on PyPI. uipath's lower bound on uipath-platform moves to >=0.2.33 so a standalone install cannot resolve a release without the binding metadata. The overwrite fixtures in the binding tests were built with field aliases, which the pydantic mypy plugin rejects. They now go through ResourceOverwriteParser, the same path the runtime uses on the server's response, so the test also exercises production construction.
A LangGraph agent rarely calls the SDK directly. It builds an interrupt model and hands it to interrupt(), and the resume-trigger protocol makes the decorated call on its behalf. The call site is a constructor, so the decorator registry could not see it, and a project whose only resource came that way generated an empty bindings file with no warning. Each spec mirrors one branch of resume_triggers._protocol, which is what decides the SDK method a model routes to. That matters for CreateDeepRag and CreateBatchTransform, where `name` is the task's own name and `index_name` is the resource being bound. Constructors are matched by class name, which is generic enough to collide, so only names imported from a uipath module count. Wait models and the rest are listed as explicitly non-binding, and a test fails if a new interrupt model is neither mapped nor excluded. Checked against the checked-in samples: ticket-classification and wait-until-timeout-agent now produce exactly the bindings their hand-written files declare.
A non-literal argument was written into bindings.json as its own source text
with isExpression set. A helper such as
def get_asset(client, name, folder_path):
return client.assets.retrieve(name=name, folder_path=folder_path)
produced a binding keyed "name.folder_path" from the parameter names. The
runtime builds its lookup key from evaluated values, so that binding can never
match and the override silently does not apply; push meanwhile looks for a
resource called "name" and creates a virtual one. samples/asset-modifier-agent
does exactly this, and three of the four expression bindings found across the
checked-in samples were junk of the same kind.
Only literals and module-level constants are recorded now. Anything else is
reported with its file, line and the expression that could not be resolved, so
it can be declared by hand. A call with no folder argument still binds, since
the environment supplies the folder; only a computed one is refused, which
also keeps a half-resolved binding from reaching push.
Hand-written entries that use isExpression are still preserved on merge. The
one sample that ships such a binding, RAG-quiz-generator, is no longer
reproduced by the generator.
main released 2.14.25 while this branch was open, so the version it carried is now on PyPI.
d7008c1 to
50d099d
Compare
The file had grown to 856 lines as each review round added cases. Five tests were removed as redundant: two that the golden-file test already subsumes, one whose positional and keyword forms the golden sample exercises anyway, one duplicating the unresolvable-argument path through the other matching rule, and one exercising merge through init when merge is covered directly. Four rebind syntaxes, three expression shapes and three constructor forms collapse into parametrized cases. The file also carried its own copies of _binding_metadata and _iter_service_classes; it now imports them. The two merge tests became one behind a fixture builder, and the push-resolver tests share their mock setup. Coverage is unchanged, checked by mutation: reversing the key order fails 10 tests, dropping the expression skip fails 7, removing the rebind check fails 4, and disabling the directory exclusions, the opt-in guard on init, folder_index or the uipath-import guard each fail their own.
|




Coded agents have never had working resource bindings.
uipath initwrites an emptyresourcesarray and nothing ever fills it, so an agent's assets, buckets and processes are invisible to solutions, to Orchestrator's package requirements and to deploy-time overrides. Low-code projects getbindings.jsonwritten for them by the Studio Web designer; coded ones got nothing, and there is even a deadgenerate_bindings_content()stub incli_pack.pyleft over from an earlier attempt.uipath bindings generatefills the file by parsing the project's Python withastand matching calls against the SDK's own@resource_overridedecorators, which already declare the resource type and which parameter carries the resource name. Those decorators now expose that as data instead of only from closure cells, so the scanner tracks the SDK automatically rather than carrying a hand-written list that goes stale. 59 methods today.Discovery is best effort and reports its own gaps: literals and module-level constants are recorded, runtime expressions are kept with
isExpression: true, and a call whose name is assembled at runtime is reported with file and line rather than guessed at. Existing entries are never rewritten, because they carry connector slugs and display names a scan cannot reproduce. Measured on the repo's samples: 6/6 onresource-overrides, 4/10 on a sample that wraps the SDK behind a helper class.uipath init --infer-bindingsruns the same scan. That flag already appears in four places in the bundledCLI_REFERENCE.mdthat ships to coding agents, for something nobody had built. Inference is opt-in in both places; plainuipath initstill writes an empty array and a test pins that, so nothing changes for existing projects unless asked.Where to start reading
_registry.pybuilds{(service, method) -> BindingSpec}from the decorators,_scanner.pyfinds matching calls,_emitter.pyshapes and merges them,_apply.pyis the shared path behind both commands. The 10-line change inuipath-platformis the seam everything else hangs off.The test worth reviewing carefully is
TestRuntimeKeyAgreement. A binding key that does not match what the decorator computes at call time fails silently: no error, a successful request, and the agent reads its development resource in production. That test drives the real decorator rather than a mock, and reversing the key order breaks it across five resource types.Things a reviewer should push back on
folder_pathgetsfolderPath: ""and a key that is just the name.pushthen treats it like any uncatalogued resource and creates a virtual placeholder. Defensible, not obviously right.isExpression: truecopies a convention from an existing sample byte for byte, but the runtime decorator keys off runtime values, so it is unclear how such a binding ever matches. Someone who owns Resource Builder should confirm.@resource_overridedecorators declare aresource_identifierthat is not a parameter of the method, so they can never fire (one looks like a typo:index_namewhere the parameter isname). The registry skips them and a test documents why. Not fixed here.folder_indexis computed and used but no test exercises it.Verified with the full
uipathanduipath-platformsuites,mypy,ruff, and the repo's httpx linter. The pre-existing IPC and scaffold failures onmainare unaffected. First commit is just the regeneratedCLI_REFERENCE.md, which had drifted forrunandevalindependently of this work.