From 0b606e3e2cc01ec7ac64cb506bb2e19807c60629 Mon Sep 17 00:00:00 2001 From: radu-mocanu Date: Thu, 30 Apr 2026 16:41:30 +0300 Subject: [PATCH] feat: add uipath_langchain.agent.deep with optional deepagents extra --- pyproject.toml | 6 +- src/uipath_langchain/agent/deep/__init__.py | 59 +++++++ src/uipath_langchain/agent/deep/agent.py | 163 ++++++++++++++++++ src/uipath_langchain/agent/deep/types.py | 14 ++ src/uipath_langchain/agent/deep/utils.py | 26 +++ tests/agent/deep/__init__.py | 0 tests/agent/deep/test_create_deep_agent.py | 38 ++++ .../deep/test_create_deep_agent_graph.py | 49 ++++++ tests/agent/deep/test_import_without_extra.py | 54 ++++++ uv.lock | 47 ++++- 10 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 src/uipath_langchain/agent/deep/__init__.py create mode 100644 src/uipath_langchain/agent/deep/agent.py create mode 100644 src/uipath_langchain/agent/deep/types.py create mode 100644 src/uipath_langchain/agent/deep/utils.py create mode 100644 tests/agent/deep/__init__.py create mode 100644 tests/agent/deep/test_create_deep_agent.py create mode 100644 tests/agent/deep/test_create_deep_agent_graph.py create mode 100644 tests/agent/deep/test_import_without_extra.py diff --git a/pyproject.toml b/pyproject.toml index bb36cc976..8fa556e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.10.11" +version = "0.10.12" 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" @@ -53,8 +53,12 @@ bedrock = [ fireworks = [ "uipath-langchain-client[fireworks]>=1.10.0,<1.11.0", ] +deep = [ + "deepagents>=0.4.11, <0.5.0", +] all = [ "uipath-langchain-client[all]>=1.10.0,<1.11.0", + "deepagents>=0.4.11, <0.5.0", ] [project.entry-points."uipath.middlewares"] diff --git a/src/uipath_langchain/agent/deep/__init__.py b/src/uipath_langchain/agent/deep/__init__.py new file mode 100644 index 000000000..a316b94ae --- /dev/null +++ b/src/uipath_langchain/agent/deep/__init__.py @@ -0,0 +1,59 @@ +"""Deep agent support, built on the optional `deepagents` package. + +Install the optional extra to use this module: + + pip install 'uipath-langchain[deep]' + uv add 'uipath-langchain[deep]' + +The `deepagents` types re-exported here (``SubAgent``, ``CompiledSubAgent``, +``BackendProtocol``, ``BackendFactory``) are loaded lazily so importing this +package without the extra installed does not crash — only attribute access +will raise ``ImportError`` with the install hint. +""" + +from .agent import create_deep_agent, create_deep_agent_graph +from .types import DeepAgentGraphState +from .utils import create_state_with_input + +_INSTALL_HINT = ( + "deepagents is required for deep agents. Install with: " + "pip install 'uipath-langchain[deep]' " + "(or: uv add 'uipath-langchain[deep]')" +) + + +def __getattr__(name: str): + if name in ("SubAgent", "CompiledSubAgent"): + try: + import deepagents + + return getattr(deepagents, name) + except ImportError as exc: + raise ImportError(_INSTALL_HINT) from exc + if name == "BackendProtocol": + try: + from deepagents.backends import BackendProtocol + + return BackendProtocol + except ImportError as exc: + raise ImportError(_INSTALL_HINT) from exc + if name == "BackendFactory": + try: + from deepagents.backends.protocol import BackendFactory + + return BackendFactory + except ImportError as exc: + raise ImportError(_INSTALL_HINT) from exc + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [ + "BackendFactory", + "BackendProtocol", + "CompiledSubAgent", + "DeepAgentGraphState", + "SubAgent", + "create_deep_agent", + "create_deep_agent_graph", + "create_state_with_input", +] diff --git a/src/uipath_langchain/agent/deep/agent.py b/src/uipath_langchain/agent/deep/agent.py new file mode 100644 index 000000000..89cf4c412 --- /dev/null +++ b/src/uipath_langchain/agent/deep/agent.py @@ -0,0 +1,163 @@ +"""Deep agent builder. + +Thin UiPath wrapper around the `deepagents` library. The deepagents dependency +is optional — install with one of: + + pip install 'uipath-langchain[deep]' + uv add 'uipath-langchain[deep]' +""" + +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Any + +from langchain.agents.structured_output import ResponseFormat +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import HumanMessage +from langchain_core.tools import BaseTool +from langgraph.graph import END, START +from langgraph.graph.state import CompiledStateGraph, StateGraph +from pydantic import BaseModel + +from .types import DeepAgentGraphState +from .utils import create_state_with_input + +if TYPE_CHECKING: + from deepagents import CompiledSubAgent, SubAgent + from deepagents.backends import BackendProtocol + from deepagents.backends.protocol import BackendFactory + + +_INSTALL_HINT = ( + "deepagents is required for deep agents. Install with: " + "pip install 'uipath-langchain[deep]' " + "(or: uv add 'uipath-langchain[deep]')" +) + + +def _import_create_deep_agent() -> Any: + try: + from deepagents import create_deep_agent as _upstream + + return _upstream + except ImportError as exc: + raise ImportError(_INSTALL_HINT) from exc + + +def create_deep_agent( + model: BaseChatModel, + system_prompt: str = "", + tools: Sequence[BaseTool] = (), + subagents: "Sequence[SubAgent | CompiledSubAgent]" = (), + backend: "BackendProtocol | BackendFactory | None" = None, + response_format: ResponseFormat[Any] | None = None, +) -> CompiledStateGraph[Any, Any, Any, Any]: + """Create a deep agent. + + Deep agents provide built-in capabilities for: + - Planning (write_todos, read_todos) + - Filesystem operations (read_file, write_file, edit_file, ls, glob, grep) + - Sub-agent delegation (task) + - Auto-summarization for long conversations + + Args: + model: A BaseChatModel instance. + system_prompt: Instructions for the agent. + tools: Custom tools to provide to the agent. + subagents: Optional list of subagent configurations. Each entry is a + ``SubAgent`` (name, description, system_prompt, and optional tools/model/middleware) + or a ``CompiledSubAgent`` (name, description, and a pre-built runnable). + backend: Storage backend for filesystem operations. Can be a + ``BackendProtocol`` instance, a factory callable, or ``None`` + (uses the default in-state backend). + response_format: Structured output format for the agent response. + + Returns: + Compiled LangGraph agent ready for execution. + + Raises: + ImportError: If the ``deepagents`` package is not installed. Install + with ``pip install 'uipath-langchain[deep]'`` or + ``uv add 'uipath-langchain[deep]'``. + """ + upstream_create_deep_agent = _import_create_deep_agent() + return upstream_create_deep_agent( + model=model, + system_prompt=system_prompt, + tools=list(tools), + subagents=list(subagents), + backend=backend, + response_format=response_format, + ) + + +def create_deep_agent_graph( + model: BaseChatModel, + tools: Sequence[BaseTool], + system_prompt: str, + backend: "BackendProtocol | BackendFactory | None", + response_format: ResponseFormat[Any] | None, + input_schema: type[BaseModel] | None, + output_schema: type[BaseModel], + build_user_message: Callable[[dict[str, Any]], str], +) -> StateGraph[Any, Any, Any, Any]: + """Build a deep agent wrapped in a parent graph that handles I/O transformation. + + The deep agent only understands messages as input and produces + structured_response as output. The wrapper graph bridges the gap: + + START -> transform_input -> deep_agent -> transform_output -> END + + Args: + model: Chat model for the deep agent. + tools: Tools available to the deep agent. + system_prompt: Combined system + meta prompt. + backend: Filesystem backend for the deep agent. + response_format: Structured output format. + input_schema: Resolved input Pydantic model (or None). + output_schema: Resolved output Pydantic model. + build_user_message: Callable that converts input arguments dict to a user message string. + + Raises: + ImportError: If the ``deepagents`` package is not installed. Install + with ``pip install 'uipath-langchain[deep]'`` or + ``uv add 'uipath-langchain[deep]'``. + """ + inner_graph = create_deep_agent( + model=model, + tools=tools, + system_prompt=system_prompt, + backend=backend, + response_format=response_format, + ) + + wrapper_state = create_state_with_input(input_schema) + + internal_fields = set(DeepAgentGraphState.model_fields.keys()) + + def transform_input(state: BaseModel) -> dict[str, Any]: + state_data = state.model_dump() + input_data = {k: v for k, v in state_data.items() if k not in internal_fields} + input_args = ( + input_schema.model_validate(input_data).model_dump() + if input_schema is not None + else {} + ) + user_text = build_user_message(input_args) + return {"messages": [HumanMessage(content=user_text, id="user-input")]} + + def transform_output(state: BaseModel) -> dict[str, Any]: + structured = getattr(state, "structured_response", {}) + return output_schema.model_validate(structured).model_dump() + + wrapper: StateGraph[Any, Any, Any, Any] = StateGraph( + wrapper_state, input_schema=input_schema, output_schema=output_schema + ) + wrapper.add_node("transform_input", transform_input) + wrapper.add_node("deep_agent", inner_graph) + wrapper.add_node("transform_output", transform_output) + wrapper.add_edge(START, "transform_input") + wrapper.add_edge("transform_input", "deep_agent") + wrapper.add_edge("deep_agent", "transform_output") + wrapper.add_edge("transform_output", END) + + return wrapper diff --git a/src/uipath_langchain/agent/deep/types.py b/src/uipath_langchain/agent/deep/types.py new file mode 100644 index 000000000..69e76b605 --- /dev/null +++ b/src/uipath_langchain/agent/deep/types.py @@ -0,0 +1,14 @@ +"""State types for the deep agent wrapper graph.""" + +from typing import Annotated, Any + +from langchain_core.messages import AnyMessage +from langgraph.graph.message import add_messages +from pydantic import BaseModel + + +class DeepAgentGraphState(BaseModel): + """Graph state for the deep agent wrapper.""" + + messages: Annotated[list[AnyMessage], add_messages] = [] + structured_response: dict[str, Any] = {} diff --git a/src/uipath_langchain/agent/deep/utils.py b/src/uipath_langchain/agent/deep/utils.py new file mode 100644 index 000000000..9eb6ffd0a --- /dev/null +++ b/src/uipath_langchain/agent/deep/utils.py @@ -0,0 +1,26 @@ +"""Utilities for the deep agent wrapper graph.""" + +from typing import cast + +from pydantic import BaseModel + +from .types import DeepAgentGraphState + + +def create_state_with_input( + input_schema: type[BaseModel] | None, +) -> type[DeepAgentGraphState]: + """Create combined state by merging DeepAgentGraphState with the input schema. + + Mirrors the shallow agent's create_state_with_input pattern: + dynamic multi-inheritance + model_rebuild() for Pydantic resolution. + """ + if input_schema is None: + return DeepAgentGraphState + CompleteState = type( + "CompleteDeepAgentGraphState", + (DeepAgentGraphState, input_schema), + {}, + ) + cast(type[BaseModel], CompleteState).model_rebuild() + return CompleteState diff --git a/tests/agent/deep/__init__.py b/tests/agent/deep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/agent/deep/test_create_deep_agent.py b/tests/agent/deep/test_create_deep_agent.py new file mode 100644 index 000000000..eb2bfd587 --- /dev/null +++ b/tests/agent/deep/test_create_deep_agent.py @@ -0,0 +1,38 @@ +"""Smoke test for create_deep_agent. + +Verifies create_deep_agent forwards its arguments to deepagents.create_deep_agent. +We don't exercise the deepagents internals (those are tested by the deepagents +package itself); we only validate UiPath's pass-through. +""" + +from unittest.mock import MagicMock, patch + +import pytest + +pytest.importorskip("deepagents") + +from uipath_langchain.agent.deep import create_deep_agent # noqa: E402 + + +def test_create_deep_agent_forwards_to_deepagents() -> None: + sentinel_graph = MagicMock(name="compiled_deep_agent") + fake_upstream = MagicMock(return_value=sentinel_graph) + model = MagicMock() + + with patch( + "uipath_langchain.agent.deep.agent._import_create_deep_agent", + return_value=fake_upstream, + ): + graph = create_deep_agent( + model=model, system_prompt="sys", tools=[], subagents=[] + ) + + assert graph is sentinel_graph + fake_upstream.assert_called_once() + kwargs = fake_upstream.call_args.kwargs + assert kwargs["model"] is model + assert kwargs["system_prompt"] == "sys" + assert kwargs["tools"] == [] + assert kwargs["subagents"] == [] + assert kwargs["backend"] is None + assert kwargs["response_format"] is None diff --git a/tests/agent/deep/test_create_deep_agent_graph.py b/tests/agent/deep/test_create_deep_agent_graph.py new file mode 100644 index 000000000..64c057f3e --- /dev/null +++ b/tests/agent/deep/test_create_deep_agent_graph.py @@ -0,0 +1,49 @@ +"""Tests for create_deep_agent_graph wrapper I/O transformations.""" + +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +from langchain_core.language_models import BaseChatModel +from langgraph.graph.state import StateGraph +from pydantic import BaseModel + +pytest.importorskip("deepagents") + +from uipath_langchain.agent.deep import create_deep_agent_graph # noqa: E402 + + +class _Input(BaseModel): + topic: str = "" + + +class _Output(BaseModel): + answer: str = "" + + +def _build_user_message(args: dict[str, Any]) -> str: + return f"Research: {args.get('topic', '')}" + + +def test_create_deep_agent_graph_returns_state_graph() -> None: + model = MagicMock(spec=BaseChatModel) + + with patch( + "uipath_langchain.agent.deep.agent.create_deep_agent", + return_value=MagicMock(), + ): + wrapper = create_deep_agent_graph( + model=model, + tools=[], + system_prompt="hi", + backend=None, + response_format=None, + input_schema=_Input, + output_schema=_Output, + build_user_message=_build_user_message, + ) + + assert isinstance(wrapper, StateGraph) + assert "transform_input" in wrapper.nodes + assert "deep_agent" in wrapper.nodes + assert "transform_output" in wrapper.nodes diff --git a/tests/agent/deep/test_import_without_extra.py b/tests/agent/deep/test_import_without_extra.py new file mode 100644 index 000000000..2fb34200f --- /dev/null +++ b/tests/agent/deep/test_import_without_extra.py @@ -0,0 +1,54 @@ +"""Verify a clear ImportError is raised when the [deep] extra is missing.""" + +import builtins +import importlib +import sys +from typing import Any +from unittest.mock import MagicMock + +import pytest +from langchain_core.language_models import BaseChatModel + + +def _hide_deepagents(monkeypatch: pytest.MonkeyPatch) -> None: + real_import = builtins.__import__ + + def fake_import(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "deepagents" or name.startswith("deepagents."): + raise ImportError(f"No module named {name!r}") + return real_import(name, *args, **kwargs) + + for mod in [ + m for m in sys.modules if m == "deepagents" or m.startswith("deepagents.") + ]: + monkeypatch.delitem(sys.modules, mod, raising=False) + monkeypatch.setattr(builtins, "__import__", fake_import) + + +def test_create_deep_agent_raises_import_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _hide_deepagents(monkeypatch) + # Re-import the module so its lazy import path runs against the patched __import__. + sys.modules.pop("uipath_langchain.agent.deep.agent", None) + sys.modules.pop("uipath_langchain.agent.deep", None) + deep_agent_module = importlib.import_module("uipath_langchain.agent.deep.agent") + + with pytest.raises(ImportError, match=r"uipath-langchain\[deep\]"): + deep_agent_module.create_deep_agent( + model=MagicMock(spec=BaseChatModel), system_prompt="x", tools=[] + ) + + +@pytest.mark.parametrize( + "name", ["SubAgent", "CompiledSubAgent", "BackendProtocol", "BackendFactory"] +) +def test_lazy_reexports_raise_import_error( + monkeypatch: pytest.MonkeyPatch, name: str +) -> None: + _hide_deepagents(monkeypatch) + sys.modules.pop("uipath_langchain.agent.deep", None) + deep_pkg = importlib.import_module("uipath_langchain.agent.deep") + + with pytest.raises(ImportError, match=r"uipath-langchain\[deep\]"): + getattr(deep_pkg, name) diff --git a/uv.lock b/uv.lock index d6bd8be24..905e53f89 100644 --- a/uv.lock +++ b/uv.lock @@ -462,6 +462,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/23/c41006e42909ec5114a8961818412310aa54646d1eae0495dbff3598a095/bottleneck-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:174b80930ce82bd8456c67f1abb28a5975c68db49d254783ce2cb6983b4fea40", size = 117611, upload-time = "2025-09-08T16:30:37.055Z" }, ] +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -823,6 +832,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] +[[package]] +name = "deepagents" +version = "0.4.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "langchain-google-genai" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/e5/1dcb9e466d887bc42ddad66faa769d1c06b11ca751d1c43b4f2e5da3b1c8/deepagents-0.4.11.tar.gz", hash = "sha256:6ada9bd3b136bae294aa1520575bf85e806c0514807d6e3bf43c7b3d08b44306", size = 90529, upload-time = "2026-03-13T21:34:46.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/45/4d78759b991535f7ef7461ac25aad79c423f5ab3cb53ef75dbeff4e9617b/deepagents-0.4.11-py3-none-any.whl", hash = "sha256:bc5b973696f5e9f9ccea1c095a4b4af6bd057b99befa76a596fc3c02380ea7cb", size = 102588, upload-time = "2026-03-13T21:34:45.409Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -4375,7 +4400,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.10.11" +version = "0.10.12" source = { editable = "." } dependencies = [ { name = "a2a-sdk" }, @@ -4401,6 +4426,7 @@ dependencies = [ [package.optional-dependencies] all = [ + { name = "deepagents" }, { name = "uipath-langchain-client", extra = ["all"] }, ] anthropic = [ @@ -4410,6 +4436,9 @@ bedrock = [ { name = "boto3-stubs" }, { name = "uipath-langchain-client", extra = ["bedrock"] }, ] +deep = [ + { name = "deepagents" }, +] fireworks = [ { name = "uipath-langchain-client", extra = ["fireworks"] }, ] @@ -4437,6 +4466,8 @@ dev = [ requires-dist = [ { name = "a2a-sdk", specifier = ">=0.2.0,<1.0.0" }, { name = "boto3-stubs", marker = "extra == 'bedrock'", specifier = ">=1.41.4" }, + { name = "deepagents", marker = "extra == 'all'", specifier = ">=0.4.11,<0.5.0" }, + { name = "deepagents", marker = "extra == 'deep'", specifier = ">=0.4.11,<0.5.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "jsonpath-ng", specifier = ">=1.7.0" }, { name = "jsonschema-pydantic-converter", specifier = ">=0.4.0" }, @@ -4462,7 +4493,7 @@ requires-dist = [ { name = "uipath-platform", specifier = ">=0.1.36,<0.2.0" }, { name = "uipath-runtime", specifier = ">=0.10.0,<0.11.0" }, ] -provides-extras = ["anthropic", "vertex", "bedrock", "fireworks", "all"] +provides-extras = ["anthropic", "vertex", "bedrock", "fireworks", "deep", "all"] [package.metadata.requires-dev] dev = [ @@ -4658,6 +4689,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, ] +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + [[package]] name = "websockets" version = "15.0.1"