Skip to content

Commit 0b606e3

Browse files
committed
feat: add uipath_langchain.agent.deep with optional deepagents extra
1 parent 87852f5 commit 0b606e3

10 files changed

Lines changed: 453 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.10.11"
3+
version = "0.10.12"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -53,8 +53,12 @@ bedrock = [
5353
fireworks = [
5454
"uipath-langchain-client[fireworks]>=1.10.0,<1.11.0",
5555
]
56+
deep = [
57+
"deepagents>=0.4.11, <0.5.0",
58+
]
5659
all = [
5760
"uipath-langchain-client[all]>=1.10.0,<1.11.0",
61+
"deepagents>=0.4.11, <0.5.0",
5862
]
5963

6064
[project.entry-points."uipath.middlewares"]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Deep agent support, built on the optional `deepagents` package.
2+
3+
Install the optional extra to use this module:
4+
5+
pip install 'uipath-langchain[deep]'
6+
uv add 'uipath-langchain[deep]'
7+
8+
The `deepagents` types re-exported here (``SubAgent``, ``CompiledSubAgent``,
9+
``BackendProtocol``, ``BackendFactory``) are loaded lazily so importing this
10+
package without the extra installed does not crash — only attribute access
11+
will raise ``ImportError`` with the install hint.
12+
"""
13+
14+
from .agent import create_deep_agent, create_deep_agent_graph
15+
from .types import DeepAgentGraphState
16+
from .utils import create_state_with_input
17+
18+
_INSTALL_HINT = (
19+
"deepagents is required for deep agents. Install with: "
20+
"pip install 'uipath-langchain[deep]' "
21+
"(or: uv add 'uipath-langchain[deep]')"
22+
)
23+
24+
25+
def __getattr__(name: str):
26+
if name in ("SubAgent", "CompiledSubAgent"):
27+
try:
28+
import deepagents
29+
30+
return getattr(deepagents, name)
31+
except ImportError as exc:
32+
raise ImportError(_INSTALL_HINT) from exc
33+
if name == "BackendProtocol":
34+
try:
35+
from deepagents.backends import BackendProtocol
36+
37+
return BackendProtocol
38+
except ImportError as exc:
39+
raise ImportError(_INSTALL_HINT) from exc
40+
if name == "BackendFactory":
41+
try:
42+
from deepagents.backends.protocol import BackendFactory
43+
44+
return BackendFactory
45+
except ImportError as exc:
46+
raise ImportError(_INSTALL_HINT) from exc
47+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
48+
49+
50+
__all__ = [
51+
"BackendFactory",
52+
"BackendProtocol",
53+
"CompiledSubAgent",
54+
"DeepAgentGraphState",
55+
"SubAgent",
56+
"create_deep_agent",
57+
"create_deep_agent_graph",
58+
"create_state_with_input",
59+
]
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Deep agent builder.
2+
3+
Thin UiPath wrapper around the `deepagents` library. The deepagents dependency
4+
is optional — install with one of:
5+
6+
pip install 'uipath-langchain[deep]'
7+
uv add 'uipath-langchain[deep]'
8+
"""
9+
10+
from collections.abc import Callable, Sequence
11+
from typing import TYPE_CHECKING, Any
12+
13+
from langchain.agents.structured_output import ResponseFormat
14+
from langchain_core.language_models import BaseChatModel
15+
from langchain_core.messages import HumanMessage
16+
from langchain_core.tools import BaseTool
17+
from langgraph.graph import END, START
18+
from langgraph.graph.state import CompiledStateGraph, StateGraph
19+
from pydantic import BaseModel
20+
21+
from .types import DeepAgentGraphState
22+
from .utils import create_state_with_input
23+
24+
if TYPE_CHECKING:
25+
from deepagents import CompiledSubAgent, SubAgent
26+
from deepagents.backends import BackendProtocol
27+
from deepagents.backends.protocol import BackendFactory
28+
29+
30+
_INSTALL_HINT = (
31+
"deepagents is required for deep agents. Install with: "
32+
"pip install 'uipath-langchain[deep]' "
33+
"(or: uv add 'uipath-langchain[deep]')"
34+
)
35+
36+
37+
def _import_create_deep_agent() -> Any:
38+
try:
39+
from deepagents import create_deep_agent as _upstream
40+
41+
return _upstream
42+
except ImportError as exc:
43+
raise ImportError(_INSTALL_HINT) from exc
44+
45+
46+
def create_deep_agent(
47+
model: BaseChatModel,
48+
system_prompt: str = "",
49+
tools: Sequence[BaseTool] = (),
50+
subagents: "Sequence[SubAgent | CompiledSubAgent]" = (),
51+
backend: "BackendProtocol | BackendFactory | None" = None,
52+
response_format: ResponseFormat[Any] | None = None,
53+
) -> CompiledStateGraph[Any, Any, Any, Any]:
54+
"""Create a deep agent.
55+
56+
Deep agents provide built-in capabilities for:
57+
- Planning (write_todos, read_todos)
58+
- Filesystem operations (read_file, write_file, edit_file, ls, glob, grep)
59+
- Sub-agent delegation (task)
60+
- Auto-summarization for long conversations
61+
62+
Args:
63+
model: A BaseChatModel instance.
64+
system_prompt: Instructions for the agent.
65+
tools: Custom tools to provide to the agent.
66+
subagents: Optional list of subagent configurations. Each entry is a
67+
``SubAgent`` (name, description, system_prompt, and optional tools/model/middleware)
68+
or a ``CompiledSubAgent`` (name, description, and a pre-built runnable).
69+
backend: Storage backend for filesystem operations. Can be a
70+
``BackendProtocol`` instance, a factory callable, or ``None``
71+
(uses the default in-state backend).
72+
response_format: Structured output format for the agent response.
73+
74+
Returns:
75+
Compiled LangGraph agent ready for execution.
76+
77+
Raises:
78+
ImportError: If the ``deepagents`` package is not installed. Install
79+
with ``pip install 'uipath-langchain[deep]'`` or
80+
``uv add 'uipath-langchain[deep]'``.
81+
"""
82+
upstream_create_deep_agent = _import_create_deep_agent()
83+
return upstream_create_deep_agent(
84+
model=model,
85+
system_prompt=system_prompt,
86+
tools=list(tools),
87+
subagents=list(subagents),
88+
backend=backend,
89+
response_format=response_format,
90+
)
91+
92+
93+
def create_deep_agent_graph(
94+
model: BaseChatModel,
95+
tools: Sequence[BaseTool],
96+
system_prompt: str,
97+
backend: "BackendProtocol | BackendFactory | None",
98+
response_format: ResponseFormat[Any] | None,
99+
input_schema: type[BaseModel] | None,
100+
output_schema: type[BaseModel],
101+
build_user_message: Callable[[dict[str, Any]], str],
102+
) -> StateGraph[Any, Any, Any, Any]:
103+
"""Build a deep agent wrapped in a parent graph that handles I/O transformation.
104+
105+
The deep agent only understands messages as input and produces
106+
structured_response as output. The wrapper graph bridges the gap:
107+
108+
START -> transform_input -> deep_agent -> transform_output -> END
109+
110+
Args:
111+
model: Chat model for the deep agent.
112+
tools: Tools available to the deep agent.
113+
system_prompt: Combined system + meta prompt.
114+
backend: Filesystem backend for the deep agent.
115+
response_format: Structured output format.
116+
input_schema: Resolved input Pydantic model (or None).
117+
output_schema: Resolved output Pydantic model.
118+
build_user_message: Callable that converts input arguments dict to a user message string.
119+
120+
Raises:
121+
ImportError: If the ``deepagents`` package is not installed. Install
122+
with ``pip install 'uipath-langchain[deep]'`` or
123+
``uv add 'uipath-langchain[deep]'``.
124+
"""
125+
inner_graph = create_deep_agent(
126+
model=model,
127+
tools=tools,
128+
system_prompt=system_prompt,
129+
backend=backend,
130+
response_format=response_format,
131+
)
132+
133+
wrapper_state = create_state_with_input(input_schema)
134+
135+
internal_fields = set(DeepAgentGraphState.model_fields.keys())
136+
137+
def transform_input(state: BaseModel) -> dict[str, Any]:
138+
state_data = state.model_dump()
139+
input_data = {k: v for k, v in state_data.items() if k not in internal_fields}
140+
input_args = (
141+
input_schema.model_validate(input_data).model_dump()
142+
if input_schema is not None
143+
else {}
144+
)
145+
user_text = build_user_message(input_args)
146+
return {"messages": [HumanMessage(content=user_text, id="user-input")]}
147+
148+
def transform_output(state: BaseModel) -> dict[str, Any]:
149+
structured = getattr(state, "structured_response", {})
150+
return output_schema.model_validate(structured).model_dump()
151+
152+
wrapper: StateGraph[Any, Any, Any, Any] = StateGraph(
153+
wrapper_state, input_schema=input_schema, output_schema=output_schema
154+
)
155+
wrapper.add_node("transform_input", transform_input)
156+
wrapper.add_node("deep_agent", inner_graph)
157+
wrapper.add_node("transform_output", transform_output)
158+
wrapper.add_edge(START, "transform_input")
159+
wrapper.add_edge("transform_input", "deep_agent")
160+
wrapper.add_edge("deep_agent", "transform_output")
161+
wrapper.add_edge("transform_output", END)
162+
163+
return wrapper
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""State types for the deep agent wrapper graph."""
2+
3+
from typing import Annotated, Any
4+
5+
from langchain_core.messages import AnyMessage
6+
from langgraph.graph.message import add_messages
7+
from pydantic import BaseModel
8+
9+
10+
class DeepAgentGraphState(BaseModel):
11+
"""Graph state for the deep agent wrapper."""
12+
13+
messages: Annotated[list[AnyMessage], add_messages] = []
14+
structured_response: dict[str, Any] = {}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Utilities for the deep agent wrapper graph."""
2+
3+
from typing import cast
4+
5+
from pydantic import BaseModel
6+
7+
from .types import DeepAgentGraphState
8+
9+
10+
def create_state_with_input(
11+
input_schema: type[BaseModel] | None,
12+
) -> type[DeepAgentGraphState]:
13+
"""Create combined state by merging DeepAgentGraphState with the input schema.
14+
15+
Mirrors the shallow agent's create_state_with_input pattern:
16+
dynamic multi-inheritance + model_rebuild() for Pydantic resolution.
17+
"""
18+
if input_schema is None:
19+
return DeepAgentGraphState
20+
CompleteState = type(
21+
"CompleteDeepAgentGraphState",
22+
(DeepAgentGraphState, input_schema),
23+
{},
24+
)
25+
cast(type[BaseModel], CompleteState).model_rebuild()
26+
return CompleteState

tests/agent/deep/__init__.py

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Smoke test for create_deep_agent.
2+
3+
Verifies create_deep_agent forwards its arguments to deepagents.create_deep_agent.
4+
We don't exercise the deepagents internals (those are tested by the deepagents
5+
package itself); we only validate UiPath's pass-through.
6+
"""
7+
8+
from unittest.mock import MagicMock, patch
9+
10+
import pytest
11+
12+
pytest.importorskip("deepagents")
13+
14+
from uipath_langchain.agent.deep import create_deep_agent # noqa: E402
15+
16+
17+
def test_create_deep_agent_forwards_to_deepagents() -> None:
18+
sentinel_graph = MagicMock(name="compiled_deep_agent")
19+
fake_upstream = MagicMock(return_value=sentinel_graph)
20+
model = MagicMock()
21+
22+
with patch(
23+
"uipath_langchain.agent.deep.agent._import_create_deep_agent",
24+
return_value=fake_upstream,
25+
):
26+
graph = create_deep_agent(
27+
model=model, system_prompt="sys", tools=[], subagents=[]
28+
)
29+
30+
assert graph is sentinel_graph
31+
fake_upstream.assert_called_once()
32+
kwargs = fake_upstream.call_args.kwargs
33+
assert kwargs["model"] is model
34+
assert kwargs["system_prompt"] == "sys"
35+
assert kwargs["tools"] == []
36+
assert kwargs["subagents"] == []
37+
assert kwargs["backend"] is None
38+
assert kwargs["response_format"] is None
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Tests for create_deep_agent_graph wrapper I/O transformations."""
2+
3+
from typing import Any
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
from langchain_core.language_models import BaseChatModel
8+
from langgraph.graph.state import StateGraph
9+
from pydantic import BaseModel
10+
11+
pytest.importorskip("deepagents")
12+
13+
from uipath_langchain.agent.deep import create_deep_agent_graph # noqa: E402
14+
15+
16+
class _Input(BaseModel):
17+
topic: str = ""
18+
19+
20+
class _Output(BaseModel):
21+
answer: str = ""
22+
23+
24+
def _build_user_message(args: dict[str, Any]) -> str:
25+
return f"Research: {args.get('topic', '')}"
26+
27+
28+
def test_create_deep_agent_graph_returns_state_graph() -> None:
29+
model = MagicMock(spec=BaseChatModel)
30+
31+
with patch(
32+
"uipath_langchain.agent.deep.agent.create_deep_agent",
33+
return_value=MagicMock(),
34+
):
35+
wrapper = create_deep_agent_graph(
36+
model=model,
37+
tools=[],
38+
system_prompt="hi",
39+
backend=None,
40+
response_format=None,
41+
input_schema=_Input,
42+
output_schema=_Output,
43+
build_user_message=_build_user_message,
44+
)
45+
46+
assert isinstance(wrapper, StateGraph)
47+
assert "transform_input" in wrapper.nodes
48+
assert "deep_agent" in wrapper.nodes
49+
assert "transform_output" in wrapper.nodes

0 commit comments

Comments
 (0)