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
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"]
Expand Down
59 changes: 59 additions & 0 deletions src/uipath_langchain/agent/deep/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
163 changes: 163 additions & 0 deletions src/uipath_langchain/agent/deep/agent.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions src/uipath_langchain/agent/deep/types.py
Original file line number Diff line number Diff line change
@@ -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] = {}
26 changes: 26 additions & 0 deletions src/uipath_langchain/agent/deep/utils.py
Original file line number Diff line number Diff line change
@@ -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
Empty file added tests/agent/deep/__init__.py
Empty file.
38 changes: 38 additions & 0 deletions tests/agent/deep/test_create_deep_agent.py
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions tests/agent/deep/test_create_deep_agent_graph.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading