Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.16.21"
version = "0.16.22"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
2 changes: 2 additions & 0 deletions src/uipath_langchain/agent/advanced/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
build_code_interpreter_middleware,
ptc_tool_names,
subagent_dispatch_is_replay_safe,
warm_code_interpreter,
)
from .types import AdvancedAgentGraphState, ConversationalAdvancedAgentGraphState
from .utils import (
Expand Down Expand Up @@ -42,4 +43,5 @@
"create_state_with_input",
"ptc_tool_names",
"subagent_dispatch_is_replay_safe",
"warm_code_interpreter",
]
28 changes: 28 additions & 0 deletions src/uipath_langchain/agent/advanced/code_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""

import atexit
import functools
import logging
from collections.abc import Iterable, Sequence
from typing import Any, Literal, get_args
Expand Down Expand Up @@ -214,6 +215,7 @@ def build_code_interpreter_middleware(
ImportError: If the ``code-interpreter`` extra is not installed.
"""
middleware_cls = _code_interpreter_middleware_cls()
warm_code_interpreter()
exposed = ptc_tool_names(tools)
dispatch = subagent_dispatch_is_replay_safe(subagents, tools)
logger.info(
Expand All @@ -234,6 +236,32 @@ def build_code_interpreter_middleware(
return [middleware]


@functools.cache
def warm_code_interpreter() -> None:
"""Compile the interpreter's WebAssembly modules before any eval deadline is armed.

quickjs_rs compiles its source-transform module lazily, inside the first eval of
the process, and that eval runs under the same per-call deadline as user code.
On a CPU-starved instance the compile alone can outlast the deadline, which
fails the run at its first model call, before the model ever asks for ``eval``.
The compiled modules are cached for the life of the process, so one call here
serves every REPL built afterwards. Without the ``code-interpreter`` extra it
does nothing.
"""
try:
from quickjs_rs import Runtime, SourceTransform, transform_source
except ImportError:
return
transform_source(
"warmup.js", "const x = 1;", flags=SourceTransform.TOP_LEVEL_CONST_TO_VAR
)
runtime = Runtime()
try:
runtime.new_context().close()
finally:
runtime.close()


def _close_at_exit(middleware: Any) -> None:
"""Close the REPL registry from ``atexit`` instead of leaving it to ``__del__``.

Expand Down
11 changes: 11 additions & 0 deletions src/uipath_langchain/agent/advanced/code_interpreter_preload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Import-time warm-up for hosts that preload modules while a process boots.

``uipath server`` imports a configurable list of modules before a pooled
instance accepts its first job. Importing this module runs
:func:`warm_code_interpreter` at that point, so the first advanced agent on the
instance does not pay the WebAssembly compile itself.
"""

from uipath_langchain.agent.advanced.code_interpreter import warm_code_interpreter

warm_code_interpreter()
71 changes: 71 additions & 0 deletions tests/agent/advanced/test_code_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""

import asyncio
import importlib
import sys
from pathlib import Path
from typing import Any, Sequence, cast, get_args
Expand All @@ -29,6 +30,7 @@
create_advanced_agent,
ptc_tool_names,
subagent_dispatch_is_replay_safe,
warm_code_interpreter,
)
from uipath_langchain.agent.advanced.code_interpreter import (
EVAL_TOOL_NAME,
Expand Down Expand Up @@ -293,6 +295,75 @@ def test_factory_closes_the_repl_registry_at_exit(
assert registered == [cast(Any, middleware)._registry.close]


def test_factory_warms_the_interpreter_before_building_the_repl(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The WebAssembly compile happens at graph build, outside any eval deadline.

Upstream compiles its source-transform module inside the first eval, under
the per-call deadline. On a slow instance that compile alone exceeds the
deadline and the first model call fails, so the factory must pay it first.
"""
calls: list[str] = []
monkeypatch.setattr(
"uipath_langchain.agent.advanced.code_interpreter.warm_code_interpreter",
lambda: calls.append("warm"),
)

build_code_interpreter_middleware([_tool("read_invoice")])

assert calls == ["warm"]


def test_warm_up_compiles_the_transform_module_upstream_uses_once(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import quickjs_rs

seen: list[Any] = []
monkeypatch.setattr(
quickjs_rs,
"transform_source",
lambda name, source, *, flags=None: seen.append(flags) or source,
)
warm_code_interpreter.cache_clear()

warm_code_interpreter()
warm_code_interpreter()

assert seen == [quickjs_rs.SourceTransform.TOP_LEVEL_CONST_TO_VAR]
warm_code_interpreter.cache_clear()


def test_warm_up_without_the_extra_is_a_no_op(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setitem(sys.modules, "quickjs_rs", None)
warm_code_interpreter.cache_clear()

warm_code_interpreter()

warm_code_interpreter.cache_clear()


def test_preload_module_warms_on_import(monkeypatch: pytest.MonkeyPatch) -> None:
"""``uipath server`` can only import modules, so the import must do the work."""
calls: list[str] = []
monkeypatch.setattr(
"uipath_langchain.agent.advanced.code_interpreter.warm_code_interpreter",
lambda: calls.append("warm"),
)
monkeypatch.delitem(
sys.modules,
"uipath_langchain.agent.advanced.code_interpreter_preload",
raising=False,
)

importlib.import_module("uipath_langchain.agent.advanced.code_interpreter_preload")

assert calls == ["warm"]


def test_factory_without_the_extra_raises_install_guidance(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading