Skip to content

feat(cli): generate bindings.json from resources referenced in code [PC-5017] - #1908

Open
tudormatei1 wants to merge 8 commits into
mainfrom
feat/cli-generate-bindings
Open

tudormatei1 wants to merge 8 commits into
mainfrom
feat/cli-generate-bindings

Conversation

@tudormatei1

Copy link
Copy Markdown
Contributor

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 Orchestrator's package requirements and to deploy-time overrides. Low-code projects get bindings.json written for them by the Studio Web designer; coded ones got nothing, and there is even a dead generate_bindings_content() stub in cli_pack.py left over from an earlier attempt.

uipath bindings generate fills the file by parsing the project's Python with ast and matching calls against the SDK's own @resource_override decorators, 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 on resource-overrides, 4/10 on a sample that wraps the SDK behind a helper class.

uipath init --infer-bindings runs the same scan. That flag already appears in four places in the bundled CLI_REFERENCE.md that ships to coding agents, for something nobody had built. Inference is opt-in in both places; plain uipath init still writes an empty array and a test pins that, so nothing changes for existing projects unless asked.

Where to start reading

_registry.py builds {(service, method) -> BindingSpec} from the decorators, _scanner.py finds matching calls, _emitter.py shapes and merges them, _apply.py is the shared path behind both commands. The 10-line change in uipath-platform is 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

  • A call with no folder_path gets folderPath: "" and a key that is just the name. push then treats it like any uncatalogued resource and creates a virtual placeholder. Defensible, not obviously right.
  • isExpression: true copies 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.
  • Nine @resource_override decorators declare a resource_identifier that is not a parameter of the method, so they can never fire (one looks like a typo: index_name where the parameter is name). The registry skips them and a test documents why. Not fixed here.
  • Positional folder arguments are untested; folder_index is computed and used but no test exercises it.

Verified with the full uipath and uipath-platform suites, mypy, ruff, and the repo's httpx linter. The pre-existing IPC and scaffold failures on main are unaffected. First commit is just the regenerated CLI_REFERENCE.md, which had drifted for run and eval independently of this work.

Copilot AI lite review requested due to automatic review settings September 22, 2026 14:03
@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels Sep 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Medium severity

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.

Comment thread packages/uipath/src/uipath/_cli/_bindings/_apply.py Outdated
Comment thread packages/uipath/src/uipath/_cli/_bindings/_emitter.py
Comment thread packages/uipath/src/uipath/_cli/_bindings/_emitter.py Outdated
Comment thread packages/uipath/src/uipath/_cli/_bindings/_registry.py
Comment thread packages/uipath/src/uipath/_cli/_bindings/_scanner.py Outdated
@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-langchain cross-tests are FAILING 🚨

Your changes may break the uipath-langchain-python integration.

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@tudormatei1
tudormatei1 force-pushed the feat/cli-generate-bindings branch from dbb0679 to c6fc27f Compare September 22, 2026 14:35
Comment on lines +15 to +17
BINDABLE_RESOURCE_TYPES = frozenset(
{"asset", "process", "bucket", "index", "app", "connection"}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@tudormatei1 tudormatei1 Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

@andreitava-uip andreitava-uip Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tudormatei1 tudormatei1 Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed

Comment on lines +141 to +147
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

@andreitava-uip andreitava-uip Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@tudormatei1
tudormatei1 force-pushed the feat/cli-generate-bindings branch from d7008c1 to 50d099d Compare September 25, 2026 10:10
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.
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants