Skip to content

feat(cli): stream job logs and result to the handler over uipath-ipc - #1883

Merged
eduard-dumitru merged 18 commits into
mainfrom
coded_ipc
Sep 15, 2026
Merged

eduard-dumitru merged 18 commits into
mainfrom
coded_ipc

Conversation

@eduard-dumitru

@eduard-dumitru eduard-dumitru commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

What

uipath-python owns the uipath-ipc contract and connection (_job_api.py) and points uipath-runtime's in-memory sinks (from #167) at the handler, so a Python serverless job streams its logs and result back over CoreIPC instead of the files. Covers both transport roles. Version 2.14.10 → 2.14.11.

Two roles, one contract — IJobInvocationCommonApi (SendLog + SetResult)

  • Pooled (uipath server) — Register(message) grabs the handler's callback (message.client.get_callback(IJobInvocationCommonApi)) on the existing pooled pipe and installs the runtime sinks for the job. Gated per-job by a StreamOutputOverIpc flag on the run request; jobs run in-process (serialized), so the process-global sinks genuinely reach the callback.
  • Non-pooled (uipath run --handler-ipc-pipe) — dials the handler's per-job server (get_proxy) and installs the sinks on a dedicated loop/thread, so the result's blocking ack can't deadlock the job's own event loop.

SendLog carries logs; SetResult carries a bounded envelope + a pointer to the output-arguments file the runtime spilled (the payload never crosses IPC). Best-effort and version-guarded: a no-op if the runtime lacks output_sinks or uipath-ipc is absent, so an older runtime keeps the files.

Related PRs — one endeavor (merge in this order)

Three PRs, one endeavor — stream Python serverless job logs + result to the .NET Robot handler over CoreIPC:

  1. uipath-runtime — the in-memory log/result sinks the host installs: feat(runtime): in-memory output sinks for host-driven log/result delivery uipath-runtime-python#167
  2. uipath-python — owns the uipath-ipc contract; installs those sinks over the pipe: feat(cli): stream job logs and result to the handler over uipath-ipc #1883 ← this PR
  3. hdens — the .NET handler side: UiPath/hdens#8031

Testing

CLI/IPC tests green, incl. the non-pooled result-ack (deadlock) regression + DTO wire-key tests; ruff/format/mypy clean.

Jira

Tracking: ROBO-5981.

🤖 Generated with Claude Code

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.

🟡 Changes recommended

The pooled log forwarding path is documented as best-effort but currently can surface exceptions during callback acquisition/forwarding, and the new test includes a timing-based sleep that can be flaky.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds the “pooled” half of IPC log streaming by wiring a pooled job’s process-global runtime log sink to an IIpcLogSink callback provided by the handler over the existing uipath-ipc connection. The Register handshake is extended to receive an injected Message (reach-back handle) so the server can obtain the callback and forward logs from worker threads onto the server event loop.

Changes:

  • Extend the IPC contract so Register(self, message) receives the injected uipath_ipc.Message and uses it to wire a pooled log sink callback.
  • Add server-side forwarding logic (run_coroutine_threadsafe + drain) and lifecycle cleanup to unset the process-global sink when the IPC server exits.
  • Add tests that lock in the callback seam behavior and bump package version to 2.14.11.
File summaries
File Description
packages/uipath/src/uipath/_cli/cli_server_ipc.py Add Message-aware Register, wire pooled log sink to handler callback, and unset sink on server shutdown.
packages/uipath/tests/cli/test_server_ipc.py Update contract test for new Register signature and add pooled log sink seam tests.
packages/uipath/pyproject.toml Bump uipath version from 2.14.10 to 2.14.11.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +151 to +158
sink_proxy = client.get_callback(IIpcLogSink)
loop = asyncio.get_running_loop()

def _forward(job_id: str, log: object) -> None:
# Runs on the job's worker thread: hand the one-way SendLog to the server loop and return at
# once. A dropped log (pipe down) must never surface into the job, so failures are swallowed.
future = asyncio.run_coroutine_threadsafe(sink_proxy.SendLog(job_id, log), loop)
future.add_done_callback(_drain)
Comment on lines +389 to +393
sink = captured["sink"]
log = {"Message": "hello", "LogLevel": 2}
sink("job-key-42", log)
await asyncio.sleep(0.05)
assert sent == [("job-key-42", log)]
@eduard-dumitru eduard-dumitru changed the title feat: stream pooled job logs to the handler over uipath-ipc feat(cli): stream job logs and result to the handler over uipath-ipc Sep 9, 2026
@eduard-dumitru
eduard-dumitru force-pushed the coded_ipc branch 3 times, most recently from e9af2ca to bea284f Compare September 9, 2026 16:24

@robert-ursu robert-ursu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed against the PR head, the uipath-runtime 0.13.5 / uipath-ipc 2.5.2 sources, and the hdens#8031 C# side.

Main theme: the new IPC path removes the execution.log fallback but has no way to notice when the channel is broken, so logs can vanish while the job still reports success. The two blocking items are the stale Register callback on the pooled server and the lazy connect on the non-pooled path. Details inline.

try:
from ._job_api import IJobInvocationCommonApi

self._callback = client.get_callback(IJobInvocationCommonApi) # type: ignore[type-abstract]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The callback is captured once on the singleton service, but message.client is the per-accept connection. hdens calls Register only from Initialize() and never re-issues it, so after a pipe reconnect (_callback proxies a closed connection) or a Python server restart (_callback is None) every opted-in job for the rest of the pod's life either streams into a dead connection or skips streaming. Since hdens no longer watches execution.log for these jobs, their logs vanish with no signal.

Suggest declaring RunJob(self, request, message: Message) and deriving message.client.get_callback(...) plus the running loop per request, instead of storing connection state on the service.

callback, loop = self._callback, self._loop
on_run_start: "Any" = None
on_run_end: "Any" = None
if request.StreamOutputOverIpc and callback is not None and loop is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When the handler opted the job in (it has already skipped the execution.log watcher) but no callback/loop is available here, the job silently runs with file sinks nobody reads. The mismatch is detectable at this point; it should surface as an error in the RunJob result (or at least a warning) rather than fall through.

# Drop the sinks so they can't outlive this loop (matters on in-process restart).
from ._job_api import clear_runtime_sinks

clear_runtime_sinks()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a no-op: the sinks are ContextVars set inside the per-request handler task (uipath-ipc dispatches each request via asyncio.create_task), so clearing them from the server task's context can't affect them. Same for on_run_end=clear_runtime_sinks. The "process-global sinks" framing in the comment and PR description is wrong; suggest dropping this and fixing the comment so the next reader doesn't rely on it.

pass


class _IpcLogHandler(logging.Handler):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No self-exclusion filter. uipath_ipc is imported before the interceptor attaches this handler to every logger, so uipath_ipc.client.connection's own diagnostics (dropping malformed RESPONSE frame, receive loop failed, ...) become JobLogDto entries in the customer's job output, and each one schedules another SendLog on the very transport whose failure it reports. Suggest a filter dropping uipath_ipc.* and this module's records (route them to the original stderr).

future = asyncio.run_coroutine_threadsafe(
self._callback.SendLog(self._job_id, dto), self._loop
)
future.add_done_callback(_drain)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The SendLog futures are never awaited, so the tail of the log can be dropped. Non-pooled: disconnect_handler_ipc -> aclose() fails any still-queued sends with ConnectionError, which _drain swallows. Pooled: the RunJob response is written right after on_run_end with no wait for acks, so .NET can Unregister the job before the last frames arrive (the sink then logs "Dropped pooled Python job log for unregistered job"). Track the futures on the handler and do a bounded wait before aclose() / before returning from RunJob.

pass
self._loop.call_soon_threadsafe(self._loop.stop)
self._thread.join(timeout=_SET_RESULT_TIMEOUT_S)
self._loop.close()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If join(timeout=30) times out because the IPC thread is wedged (peer half-open, aclose() hanging), loop.close() raises RuntimeError: Cannot close a running event loop. Inside the context manager's finally that replaces the job's real exception, or turns a successful run into exit 1. Same issue on the failure path in connect_handler_ipc (line 266), where the bare raise never executes and the original TimeoutError is masked. Guard close() on thread.is_alive() and make teardown non-raising.

thread.start()

async def _build() -> Any:
client = IpcClient(transport=NamedPipeClientTransport(pipe))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

NamedPipeClientTransport connects lazily on the first call, so nothing here verifies the pipe is reachable. But installing the handler makes UiPathRuntimeLogsInterceptor skip the execution.log file handler, so an unreachable pipe (POSIX $TMPDIR mismatch, peer not yet bound, typo) loses 100% of the job's logs while uipath run exits 0 with "Successful execution". Do one eager round-trip at connect and fail loudly, or fall back to the file handler on first failure.

Also: no request_timeout on IpcClient. Against a peer that accepted the socket but stalls, every log line becomes an immortal task parked on the write lock with no cap, for the whole job.

Comment thread packages/uipath/pyproject.toml Outdated
[project]
name = "uipath"
version = "2.14.10"
version = "2.14.11"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

2.14.11 is already on PyPI (main is at 2.14.14), and the branch conflicts with main on pyproject.toml and uv.lock. Needs a rebase and a bump to >= 2.14.15. Worth getting right because the hdens VenvIpcCapability version floors will be set from this release: if they end up at 2.14.11, pods with 2.14.11-2.14.14 venvs would get StreamOutputOverIpc / --handler-ipc-pipe, the watcher skipped, and an older uipath that ignores or rejects the option.

from ._job_api import handler_ipc_connection

async with (
handler_ipc_connection(handler_ipc_pipe, ctx.job_id or ""),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ctx.job_id or "" sends an empty string where the .NET contract has Guid jobId. Without UIPATH_JOB_KEY (manual/dev runs) Newtonsoft can't convert "" to Guid, so every SendLog/SetResult errors and, given the issues above, nothing is reported. Skip connecting, or fail fast with a clear message, when ctx.job_id is None.

debug_port: int,
keep_state_file: bool,
simulation: str | None,
handler_ipc_pipe: str | None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

handler_ipc_pipe isn't forwarded to Middlewares.next("run", ...), and the sinks are installed only inside execute(). A run middleware that returns should_continue=False executes the job with the flag silently ignored, while the handler has already skipped the execution.log watcher. Either pass it through the middleware chain or refuse the flag when a run middleware takes over.

@eduard-dumitru
eduard-dumitru force-pushed the coded_ipc branch 3 times, most recently from 4608cd0 to de514dd Compare September 15, 2026 09:15
eduard-dumitru and others added 12 commits September 15, 2026 13:11
The pooled `uipath server` now grabs the handler's IIpcLogSink callback during Register
(via the injected Message) and points uipath-runtime's process-global log sink at it, so
a pooled job's logs stream back over the existing pipe. Best-effort and version-guarded:
an older uipath-runtime without the pooled sink API is a no-op and jobs keep the file path.

Register(self, message) gains the reach-back handle; the sink forwards each SendLog onto
the server loop from the job's worker thread. Bump 2.14.10 -> 2.14.11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
uipath-python owns the uipath-ipc contract and connection (_job_api.py) and
points uipath-runtime's in-memory sinks at it. The contract is
IJobInvocationCommonApi (SendLog + SetResult), the runtime-agnostic base the
handler hosts; the JS-only telemetry channel lives on a .NET superset Python
doesn't use, so Python keys on the common base directly. Pooled: `uipath server`
grabs the handler's IJobInvocationCommonApi callback at Register and installs the
sinks per job. Non-pooled: `uipath run --handler-ipc-pipe` dials the handler's
per-job server. The result envelope travels inline; output arguments go off-heap
via a file pointer.

Review fixes:
- run the non-pooled handler connection on its own loop/thread so the result
  sink's blocking ack can't deadlock the job's own event loop
- install/clear the process-global sinks inside the job core's lock so
  concurrently-dispatched RunJobs can't corrupt each other's routing
- log (not silently swallow) an IPC result-delivery failure
- always disconnect the handler IPC on the run's exit path (async context manager)
- register the default runtime factory in start_ipc_server
- pin the DTO wire key sets and the log-level mapping in tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IPC log/result sinks this CLI installs live in uipath.runtime.output_sinks, which first shipped
in uipath-runtime 0.13.5. The old >=0.13.1 floor (and a lockfile pinned to 0.13.1) would resolve a
runtime without that module, silently no-opping the feature via the ImportError fallback. Bump the
floor and relock to 0.13.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ping

The _to_result_dto tests set the fake result's status to bare strings
("faulted"/"successful"), but the runtime hands a UiPathRuntimeStatus
(str, Enum) member, where str(member) is "UiPathRuntimeStatus.FAULTED",
not "faulted". Only the getattr(status, "value", status) extraction
normalizes it — and the bare-string fakes never took that path, so
dropping it would report a faulted/suspended job as SUCCESSFUL while
every test stayed green.

Use real UiPathRuntimeStatus members in the fakes and add the SUSPENDED
case (previously untested), so the mapping is guarded against that
regression.
…Register

Nothing exercised the failure guarantees the design forbids violating: a
SendLog forward that fails must not escape emit(), a SetResult failure must
be logged and not raised (else the job faults and output.json is clobbered),
and a get_callback that raises at Register must leave the callback None
rather than break registration.

Add a test for each. Verified by mutation: making any of the three except
blocks re-raise fails the matching test.
The IPC pooled-sink tests fake _run_command_isolated, so the hook-placement
guarantees were only asserted against the fake. Drive real
on_run_start/on_run_end spies through the real core and pin: on_run_end
fires in the finally even when the job raises (SystemExit/Exception),
on_run_start runs only after env/cwd are applied, and neither hook runs on
the bad-working-dir early return.

Verified by mutation: relocating either hook fails these tests.
…gister

Register captured the caller's callback on the singleton service, but
message.client is the per-accept connection and hdens only calls Register
from Initialize(). After a pipe reconnect the stored callback proxied a
closed connection, and after a Python server restart it was None — so every
opted-in job for the rest of the pod's life either streamed into a dead
connection or skipped streaming. With execution.log no longer watched for
those jobs, their logs vanished with no signal.

RunJob now takes the injected Message and derives the callback plus the
running loop from that request's own connection; Register is a readiness
probe and the service holds no connection state.

Also:
- Fail loud when a job is opted in but no callback is available, rather than
  running it with file sinks nobody reads.
- Drop clear_runtime_sinks() from start_ipc_server's finally: uipath-ipc
  dispatches each request in its own task, so the sinks are per-request
  contextvars and clearing them from the server task was a no-op.
- Remove a type: ignore that the uipath-runtime 0.13.5 bump made unused.
- Capture the result-delivery log on the module logger directly; through
  caplog it was flaky, since other tests in the suite mutate propagation.
The runtime interceptor attaches the job log handler to the root logger, so
uipath_ipc's own diagnostics ("dropping malformed frame", "receive loop
failed", ...) became JobLogDto entries in the customer's job output — and
each one scheduled another SendLog on the very transport whose failure it
was reporting.

Route records from uipath_ipc.* and this module to the original stderr
instead of the IPC channel, so they stay visible in the pod without riding
the channel they describe.

Also correct the Register docstring: it still claimed to grab the caller's
callback, which moved to RunJob.
SendLog futures were fire-and-forget, so the tail of a job's log could be
lost: non-pooled, aclose() failed whatever was still queued; pooled, the
RunJob response was written straight after the job, letting the peer
unregister it before the last frames arrived.

Track pending sends on the handler and drain them, bounded, before teardown.
The two paths need different drains: non-pooled blocks inside _shutdown (it
already runs on a worker thread, and the sink loop has its own), while pooled
must await in RunJob, because on_run_end runs on the very loop the sends need
in order to complete.
Two teardown-path defects on the job-api channel.

SetResult's return value was discarded, but the handler returns false when it
cannot route the job (unknown job key, or the executor threw). A rejected
result was therefore treated as delivered and silently lost; it is now
reported. The report reaches the original stderr rather than the channel that
just rejected it.

loop.close() also ran unconditionally after a timeout-bounded join. A wedged
IPC thread leaves the loop running, so close() raised: inside the context
manager's finally that masked the job's real exception (or turned a success
into exit 1), and on the connect-failure path it pre-empted the bare raise,
masking the original error. Teardown now lives in one non-raising helper used
by both paths, which only closes the loop once its thread is gone.
NamedPipeClientTransport dials on first use, so an unreachable pipe (a
TMPDIR mismatch, a peer not yet bound, a typo) was not noticed at connect.
By then the interceptor has already suppressed execution.log for the job, so
every log line went nowhere while `uipath run` still exited 0 reporting a
successful execution.

Dial the transport eagerly and raise with the pipe name if it cannot be
reached. Losing the job is better than running it with its entire log
silently discarded.

Also give IpcClient a request timeout: against a peer that accepts the
connection and then stalls, each log line otherwise became an uncancellable
task parked on the write lock for the life of the job.

connect_handler_ipc is therefore no longer lazy - it now requires a
reachable pipe, and its test stands up a real server instead of relying on
the old behaviour.
The peer types the job id as a Guid and rejects anything it cannot parse as
one. Both lanes accepted whatever they were handed: the CLI turned a missing
UIPATH_JOB_KEY into "", and the pooled server passed an unset JobKey straight
through. Since requesting IPC suppresses the file sinks, every SendLog and
SetResult then failed on the wire and the run produced no logs anywhere while
still exiting 0.

Validate the shape in both places and fail fast instead.
A "run" middleware that returns should_continue=False runs the job itself and
skips the block that opens the pipe, so the caller -- which stopped watching the
log file because it was promised a pipe -- got no logs, no result, and
"Successful execution.".

Refuse the flag in that case instead. Both checks now test the pipe for
presence rather than truthiness, so an empty name cannot slip past them either.
Nineteen comments down to seven one-liners. What is left is the blocking/awaiting
asymmetry between the two lanes, why teardown must not raise, why the connect is
eager, where the contextvar sinks have to be installed, why the Message
placeholder survives import, and why the callback is never cached. The rest
restated the code or repeated an error message.
…orks

The dispatcher builds its plan from the contract, not the implementation, so
declaring `message` only on PythonRuntimeService left it uninjected: every
pooled job that opted into streaming was rejected with "no IPC callback is
available" before it ran. Every pooled test passed a message by hand, which hid
it -- the new test drives a real pipe with a client that hosts the callback.
SendLog failures landed in a future whose exception was retrieved and thrown
away, so a handler that went away mid-job produced an empty log stream and exit
0 -- the failure this channel exists to prevent. Report the first one to the
real stderr, then stay quiet so a dead pipe cannot spam.
Refuse an all-zeros job id: Guid's default parses as a UUID, so it passed the
shape check and then routed nowhere, the peer dropping every log on a debug line
nobody reads. The test meant to cover it used a 35-character string, so it had
only ever exercised "malformed".

Hide --handler-ipc-pipe: the job executor sets it, a person never does, yet it
was listed in every user's --help and in the generated CLI docs.

Drop the "no IPC callback available" guard down to what is knowable. The
dispatcher always injects a Message carrying the connection and get_callback
merely wraps it, so the guard could not fire over IPC while advertising a
wire-level problem it was incapable of seeing.

Stop claiming the serialization lock bounds per-job global state: cancelling the
RunJob task releases it while the job is still running on a thread cancellation
cannot reach.

On the tests, four that did not pin what they claimed:

- The sink helper swapped the runtime's ContextVars for a dict, recording THAT
  something was installed but not WHERE. Installing from a foreign thread -- the
  one thing the code forbids -- kept all 42 green; it now fails two.
- The non-pooled test proved the pipe was dialled, not that the job ran inside
  it. Hoisting the connection out of the run, or passing None for the job id,
  both passed; both now fail.
- No test asserted a log line crossing a real pipe, only the result half.
- The mapped error checked Code and Category alone, so swapping Title and Detail
  passed -- a stack trace would surface as the error's title.

Plus coverage for aflush_pending and for a cancelled send, whose future raises
rather than returns, and a note that the suspended mapping is defensive: the
runtime delivers SUCCESSFUL and FAULTED alone and the peer resolves suspend from
output.json.
Refuse to stream when the runtime has no output sinks. install_runtime_sinks
swallowed the ImportError and returned None, and neither lane noticed: the
connection came back live with no log handler and the job reported ExitCode 0,
while the peer had already stopped watching execution.log on our promise. The
whole job's output vanished and everything reported success. Teardown still
tolerates an older runtime; only install refuses.

Match the peer's frame cap. uipath-ipc caps an inbound frame at 2 MB, but every
.NET server on this contract already accepts 30 -- PythonJobApiServer for the
per-job pipe, IpcServerDefaults for the JS lane. Python was the only endpoint
still at the default.

Give the timeouts room. 30s for a call and 5s to drain in-flight sends were
picked with nothing behind them. They are ceilings, not delays -- a healthy peer
acks in under a millisecond -- so they only decide how long to hang on when the
peer is slow. Nothing throttles emit and production p99 is 7,500 log lines a job
(worst seen 58,694), so a backed-up peer can have thousands in flight when the
job ends, and five seconds truncates exactly the tail worth having. 120s for all
three.

Three tests did not pin what they claimed:

- An earlier commit deleted the line `class TestMiddleware:`, reparenting five
  unrelated middleware tests under TestHandlerIpcPipe, so `-k TestMiddleware`
  selected nothing.
- The non-pooled deadlock regression passed WITH the deadlock, in 30.9s instead
  of 0.19s: every assertion sat after `await disconnect_handler_ipc`, so the sink
  burned its timeout, gave up, and the queued ack completed during that await.
  Assert before any await, and bound the call.
- The pooled real-pipe test patched install_runtime_sinks away, so the callback
  it obtained was never used -- pointing get_callback at another contract kept
  the suite, mypy and ruff clean while every pooled send went to an endpoint the
  peer does not host. Install for real and assert a line arrives.

Also: drive the public context manager rather than connect/disconnect, since
cli_run only ever uses that; pin that the sinks are live when the runtime
context snapshots them; and clear what the batching revert left behind.
@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 →

@sonarqubecloud

Copy link
Copy Markdown

@eduard-dumitru
eduard-dumitru merged commit 457e71a into main Sep 15, 2026
167 of 186 checks passed
@eduard-dumitru
eduard-dumitru deleted the coded_ipc branch September 15, 2026 13:25
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