From 71b218c1bbf7e328576dd2a32e75a4110382d048 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Mon, 13 Jul 2026 17:39:21 +0300 Subject: [PATCH 1/6] feat: support coded deepagents --- samples/README.md | 3 + samples/coded-deepagent/README.md | 42 +++++ samples/coded-deepagent/agent.mermaid | 15 ++ samples/coded-deepagent/graph.py | 94 ++++++++++++ samples/coded-deepagent/input.json | 10 ++ samples/coded-deepagent/langgraph.json | 7 + samples/coded-deepagent/pyproject.toml | 15 ++ samples/coded-deepagent/uipath.json | 5 + src/uipath_langchain/agent/advanced/agent.py | 30 ++-- src/uipath_langchain/agent/advanced/utils.py | 55 ++++++- src/uipath_langchain/deepagents/__init__.py | 25 +++ src/uipath_langchain/deepagents/agent.py | 143 ++++++++++++++++++ src/uipath_langchain/deepagents/backend.py | 48 ++++++ src/uipath_langchain/deepagents/metadata.py | 62 ++++++++ src/uipath_langchain/runtime/factory.py | 74 ++++++++- src/uipath_langchain/runtime/runtime.py | 4 +- .../test_create_advanced_agent_graph.py | 17 +++ tests/agent/advanced/test_memory_injection.py | 10 ++ tests/agent/advanced/test_utils.py | 45 ++++++ tests/deepagents/__init__.py | 0 tests/deepagents/test_backend.py | 40 +++++ tests/deepagents/test_metadata.py | 93 ++++++++++++ tests/deepagents/test_runtime_config.py | 16 ++ tests/deepagents/test_runtime_factory.py | 68 +++++++++ 24 files changed, 904 insertions(+), 17 deletions(-) create mode 100644 samples/coded-deepagent/README.md create mode 100644 samples/coded-deepagent/agent.mermaid create mode 100644 samples/coded-deepagent/graph.py create mode 100644 samples/coded-deepagent/input.json create mode 100644 samples/coded-deepagent/langgraph.json create mode 100644 samples/coded-deepagent/pyproject.toml create mode 100644 samples/coded-deepagent/uipath.json create mode 100644 src/uipath_langchain/deepagents/__init__.py create mode 100644 src/uipath_langchain/deepagents/agent.py create mode 100644 src/uipath_langchain/deepagents/backend.py create mode 100644 src/uipath_langchain/deepagents/metadata.py create mode 100644 tests/deepagents/__init__.py create mode 100644 tests/deepagents/test_backend.py create mode 100644 tests/deepagents/test_metadata.py create mode 100644 tests/deepagents/test_runtime_config.py create mode 100644 tests/deepagents/test_runtime_factory.py diff --git a/samples/README.md b/samples/README.md index ade8b2c1e..a628be9c8 100644 --- a/samples/README.md +++ b/samples/README.md @@ -6,6 +6,9 @@ This sample demonstrates a simple LangGraph agent that performs basic arithmetic ## [Chat agent](chat-agent) This sample shows how to build an AI assistant using LangGraph and Tavily search for movie research and recommendations. +## [Coded DeepAgent](coded-deepagent) +This sample demonstrates a task-mode coded DeepAgent using the UiPath runtime workspace contract, typed input/output, a tool, and a review subagent. + ## [Company research agent](company-research-agent) This sample demonstrates how to create an agent that researches companies and develops outreach strategies using web search capabilities. diff --git a/samples/coded-deepagent/README.md b/samples/coded-deepagent/README.md new file mode 100644 index 000000000..1a1e750c0 --- /dev/null +++ b/samples/coded-deepagent/README.md @@ -0,0 +1,42 @@ +# Coded DeepAgent + +This sample demonstrates a task-mode coded agent built with the UiPath +DeepAgents contract. + +The graph uses `create_uipath_deep_agent_graph`, which tags the graph for the +UiPath LangGraph runtime. At runtime, UiPath creates a workspace, injects its +path into LangGraph config, and hydrates that workspace through job +attachments. Task-mode DeepAgents persist workspace changes when the run +successfully completes or suspends. The sample does not configure a custom +bucket backend. + +## What It Shows + +- Typed coded-agent input and output with Pydantic models. +- A standard LangChain tool used by the main DeepAgent. +- A DeepAgents subagent used for risk review. +- Runtime-provided workspace persistence through the UiPath contract. + +## Files + +- `graph.py`: task-mode coded DeepAgent graph. +- `input.json`: sample input payload. +- `langgraph.json`: LangGraph entrypoint. +- `uipath.json`: UiPath task-mode runtime configuration. +- `agent.mermaid`: high-level graph diagram. + +## Requirements + +- UiPath runtime credentials for `UiPathChat`. +- Access to the configured model, `gpt-4o-2024-08-06`. + +## Run + +```bash +cd samples/coded-deepagent +uv sync +uipath run agent "$(cat input.json)" +``` + +The agent writes `/launch/brief.md` and `/launch/risks.md` in the DeepAgents +workspace and returns those paths in `workspace_files`. diff --git a/samples/coded-deepagent/agent.mermaid b/samples/coded-deepagent/agent.mermaid new file mode 100644 index 000000000..5bec4cbef --- /dev/null +++ b/samples/coded-deepagent/agent.mermaid @@ -0,0 +1,15 @@ +flowchart TB + __start__(__start__) + transform_input(transform_input) + advanced_agent(advanced_agent) + risk_reviewer([risk_reviewer subagent]) + workspace[(runtime workspace)] + transform_output(transform_output) + __end__(__end__) + + __start__ --> transform_input + transform_input --> advanced_agent + advanced_agent -. delegates review .-> risk_reviewer + advanced_agent -. writes files .-> workspace + advanced_agent --> transform_output + transform_output --> __end__ diff --git a/samples/coded-deepagent/graph.py b/samples/coded-deepagent/graph.py new file mode 100644 index 000000000..1c6930d4a --- /dev/null +++ b/samples/coded-deepagent/graph.py @@ -0,0 +1,94 @@ +"""Task-mode coded DeepAgent using the UiPath DeepAgents contract.""" + +from langchain_core.tools import tool +from pydantic import BaseModel, Field + +from uipath_langchain.chat import UiPathChat +from uipath_langchain.deepagents import create_uipath_deep_agent_graph + + +class BriefInput(BaseModel): + """Input for the product launch brief.""" + + product_name: str = Field(description="Name of the product or feature.") + audience: str = Field(description="Primary customer or user audience.") + objective: str = Field(description="Main launch objective.") + constraints: list[str] = Field( + default_factory=list, + description="Important limits, requirements, or risks to account for.", + ) + + +class BriefOutput(BaseModel): + """Structured launch brief returned by the agent.""" + + executive_summary: str + launch_plan: list[str] + risk_review: list[str] + workspace_files: list[str] + + +@tool +def score_launch_readiness(audience: str, constraints: list[str]) -> str: + """Score launch readiness from simple deterministic planning signals.""" + score = 80 + if len(constraints) >= 3: + score -= 10 + if any("compliance" in item.lower() for item in constraints): + score -= 10 + if any("deadline" in item.lower() for item in constraints): + score -= 5 + return f"Launch readiness for {audience}: {max(score, 35)}/100" + + +MODEL = UiPathChat(model="gpt-4o-2024-08-06", temperature=0) + +RISK_REVIEWER_PROMPT = """You are a launch risk reviewer. +Review the proposed launch plan for practical delivery risks, compliance gaps, +unclear ownership, and missing follow-up work. Return concise findings that the +main agent can incorporate into the final brief.""" + +RISK_REVIEWER = { + "name": "risk_reviewer", + "description": "Reviews launch plans for execution risks and missing safeguards.", + "system_prompt": RISK_REVIEWER_PROMPT, + "model": MODEL, +} + +SYSTEM_PROMPT = """You are a product launch planning agent. + +Use the planning tools provided by DeepAgents. Use score_launch_readiness to get +a deterministic readiness signal. Delegate a plan review to risk_reviewer before +finalizing the answer. + +The UiPath runtime provides your DeepAgents filesystem through the tagged graph +contract. Write these workspace files before producing the final answer: +- /launch/brief.md with the final launch brief +- /launch/risks.md with the risk review + +Return structured output matching the schema. Include the workspace file paths +you wrote in workspace_files.""" + + +def build_user_message(args: dict) -> str: + constraints = args.get("constraints") or [] + constraint_lines = "\n".join(f"- {item}" for item in constraints) or "- None" + return f"""Create a launch brief. + +Product: {args["product_name"]} +Audience: {args["audience"]} +Objective: {args["objective"]} +Constraints: +{constraint_lines} +""" + + +graph = create_uipath_deep_agent_graph( + model=MODEL, + input_schema=BriefInput, + output_schema=BriefOutput, + system_prompt=SYSTEM_PROMPT, + tools=[score_launch_readiness], + subagents=[RISK_REVIEWER], + build_user_message=build_user_message, +) diff --git a/samples/coded-deepagent/input.json b/samples/coded-deepagent/input.json new file mode 100644 index 000000000..5543a9daf --- /dev/null +++ b/samples/coded-deepagent/input.json @@ -0,0 +1,10 @@ +{ + "product_name": "Invoice Exception Copilot", + "audience": "accounts payable operations managers", + "objective": "prepare a pilot launch plan for three enterprise customers", + "constraints": [ + "security review must complete before external rollout", + "deadline is six weeks away", + "support team needs enablement material" + ] +} diff --git a/samples/coded-deepagent/langgraph.json b/samples/coded-deepagent/langgraph.json new file mode 100644 index 000000000..c465a881b --- /dev/null +++ b/samples/coded-deepagent/langgraph.json @@ -0,0 +1,7 @@ +{ + "dependencies": ["."], + "graphs": { + "agent": "./graph.py:graph" + }, + "env": ".env" +} diff --git a/samples/coded-deepagent/pyproject.toml b/samples/coded-deepagent/pyproject.toml new file mode 100644 index 000000000..77c0692d7 --- /dev/null +++ b/samples/coded-deepagent/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "coded-deepagent" +version = "0.0.1" +description = "Task-mode coded DeepAgent using the UiPath runtime workspace contract" +authors = [{ name = "UiPath", email = "support@uipath.com" }] +requires-python = ">=3.11" +dependencies = [ + "uipath-langchain>=0.14.3, <0.15.0", + "uipath>=2.13.6, <2.14.0", +] + +[dependency-groups] +dev = [ + "uipath-dev", +] diff --git a/samples/coded-deepagent/uipath.json b/samples/coded-deepagent/uipath.json new file mode 100644 index 000000000..30c00e944 --- /dev/null +++ b/samples/coded-deepagent/uipath.json @@ -0,0 +1,5 @@ +{ + "runtimeOptions": { + "isConversational": false + } +} diff --git a/src/uipath_langchain/agent/advanced/agent.py b/src/uipath_langchain/agent/advanced/agent.py index 4baaf1f0b..ababb10ee 100644 --- a/src/uipath_langchain/agent/advanced/agent.py +++ b/src/uipath_langchain/agent/advanced/agent.py @@ -6,11 +6,11 @@ from deepagents import CompiledSubAgent, SubAgent from deepagents import create_deep_agent as _create_deep_agent from deepagents.backends import BackendProtocol -from deepagents.backends.filesystem import FilesystemBackend from deepagents.backends.protocol import BackendFactory from langchain.agents.structured_output import ResponseFormat from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage +from langchain_core.runnables.config import RunnableConfig from langchain_core.tools import BaseTool from langgraph.graph import END, START from langgraph.graph.state import CompiledStateGraph, StateGraph @@ -23,6 +23,7 @@ from .utils import ( MEMORY_INDEX_VIRTUAL_PATH, create_state_with_input, + is_workspace_filesystem_backend, resolve_input_attachments, ) @@ -62,22 +63,24 @@ def create_advanced_agent_graph( input_schema: type[BaseModel] | None, output_schema: type[BaseModel], build_user_message: Callable[[dict[str, Any]], str], + subagents: Sequence[SubAgent | CompiledSubAgent] = (), ) -> StateGraph[Any, Any, Any, Any]: """Wrap the advanced agent in a parent graph that maps typed I/O to/from messages. - With a ``FilesystemBackend``, attachment-shaped inputs are downloaded into the - workspace and given a ``FilePath`` before the user message is built. A - ``FilesystemBackend`` also enables workspace memory: deepagents' + With a filesystem workspace backend, attachment-shaped inputs are downloaded + into the workspace and given a ``FilePath`` before the user message is built. + Filesystem workspaces also enable workspace memory: deepagents' ``MemoryMiddleware`` reads ``/memory/MEMORY.md`` from the backend each turn. - Memory stays disabled for non-filesystem backends, which carry no workspace. + Memory stays disabled for backends which carry no workspace. """ memory_sources = ( - [MEMORY_INDEX_VIRTUAL_PATH] if isinstance(backend, FilesystemBackend) else [] + [MEMORY_INDEX_VIRTUAL_PATH] if is_workspace_filesystem_backend(backend) else [] ) inner_graph = create_advanced_agent( model=model, tools=tools, + subagents=subagents, system_prompt=system_prompt, backend=backend, response_format=response_format, @@ -90,7 +93,10 @@ def create_advanced_agent_graph( get_job_attachment_paths(input_schema) if input_schema is not None else [] ) - async def transform_input_async(state: BaseModel) -> dict[str, Any]: + async def transform_input_async( + state: BaseModel, + config: RunnableConfig, + ) -> 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 = ( @@ -100,7 +106,11 @@ async def transform_input_async(state: BaseModel) -> dict[str, Any]: ) if attachment_paths: input_args = await resolve_input_attachments( - backend, attachment_paths, input_args + backend, + attachment_paths, + input_args, + state=state, + config=config, ) user_text = build_user_message(input_args) return {"messages": [HumanMessage(content=user_text, id="user-input")]} @@ -128,6 +138,7 @@ def create_conversational_advanced_agent_graph( tools: Sequence[BaseTool], system_prompt: str, backend: BackendProtocol | BackendFactory | None, + subagents: Sequence[SubAgent | CompiledSubAgent] = (), ) -> StateGraph[Any, Any, Any, Any]: """Wrap the advanced agent in a parent graph that speaks the conversational contract. @@ -141,12 +152,13 @@ def create_conversational_advanced_agent_graph( from uipath_langchain.runtime.messages import UiPathChatMessagesMapper memory_sources = ( - [MEMORY_INDEX_VIRTUAL_PATH] if isinstance(backend, FilesystemBackend) else [] + [MEMORY_INDEX_VIRTUAL_PATH] if is_workspace_filesystem_backend(backend) else [] ) inner_graph = create_advanced_agent( model=model, tools=tools, + subagents=subagents, system_prompt=system_prompt, backend=backend, memory=memory_sources, diff --git a/src/uipath_langchain/agent/advanced/utils.py b/src/uipath_langchain/agent/advanced/utils.py index 0726bf5d2..1aea07cd5 100644 --- a/src/uipath_langchain/agent/advanced/utils.py +++ b/src/uipath_langchain/agent/advanced/utils.py @@ -10,6 +10,8 @@ from deepagents.backends import BackendProtocol, FilesystemBackend from deepagents.backends.protocol import BackendFactory from jsonpath_ng import parse as jsonpath_parse # type: ignore[import-untyped] +from langchain.tools import ToolRuntime +from langchain_core.runnables.config import RunnableConfig from pydantic import BaseModel from uipath.platform import UiPath from uipath.platform.attachments import Attachment @@ -25,12 +27,54 @@ # persisted via WorkspaceHydrator) rather than the cross-run StoreBackend. MEMORY_DIR_NAME = "memory" MEMORY_INDEX_FILENAME = "MEMORY.md" +WORKSPACE_FILESYSTEM_BACKEND_ATTR = "is_uipath_workspace_filesystem_backend" # Virtual path handed to MemoryMiddleware as a source; the agent's virtual-mode # FilesystemBackend resolves it under the workspace root. MEMORY_INDEX_VIRTUAL_PATH = f"/{MEMORY_DIR_NAME}/{MEMORY_INDEX_FILENAME}" +def is_workspace_filesystem_backend( + backend: BackendProtocol | BackendFactory | None, +) -> bool: + """Return whether a backend is backed by a runtime workspace filesystem.""" + return isinstance(backend, FilesystemBackend) or ( + callable(backend) + and getattr(backend, WORKSPACE_FILESYSTEM_BACKEND_ATTR, False) is True + ) + + +def _resolve_filesystem_backend( + backend: BackendProtocol | BackendFactory | None, + *, + state: BaseModel | None = None, + config: RunnableConfig | None = None, +) -> FilesystemBackend: + if isinstance(backend, FilesystemBackend): + return backend + if callable(backend) and is_workspace_filesystem_backend(backend): + resolved = backend( + ToolRuntime( + state=state, + context=None, + config=config or {}, + stream_writer=lambda _: None, + tool_call_id=None, + store=None, + ) + ) + if isinstance(resolved, FilesystemBackend): + return resolved + raise TypeError( + "UiPath workspace backend factory must resolve to FilesystemBackend, " + f"got {type(resolved).__name__}" + ) + raise NotImplementedError( + "Advanced agent with input attachments requires a FilesystemBackend, " + f"got {type(backend).__name__}" + ) + + def create_state_with_input( input_schema: type[BaseModel] | None, ) -> type[AdvancedAgentGraphState]: @@ -59,17 +103,16 @@ async def resolve_input_attachments( backend: BackendProtocol | BackendFactory | None, attachment_paths: list[str], input_args: dict[str, Any], + *, + state: BaseModel | None = None, + config: RunnableConfig | None = None, ) -> dict[str, Any]: """Download attachment-shaped inputs into the backend and add a ``FilePath``. Each ticket is streamed to ``/_`` and augmented with a - ``FilePath`` so the agent's file tools can open it. FilesystemBackend only. + ``FilePath`` so the agent's file tools can open it. """ - if not isinstance(backend, FilesystemBackend): - raise NotImplementedError( - "Advanced agent with input attachments requires a FilesystemBackend, " - f"got {type(backend).__name__}" - ) + backend = _resolve_filesystem_backend(backend, state=state, config=config) result = copy.deepcopy(input_args) client = UiPath() diff --git a/src/uipath_langchain/deepagents/__init__.py b/src/uipath_langchain/deepagents/__init__.py new file mode 100644 index 000000000..abf9badec --- /dev/null +++ b/src/uipath_langchain/deepagents/__init__.py @@ -0,0 +1,25 @@ +"""UiPath DeepAgents integration helpers.""" + +from .agent import ( + UiPathDeepAgent, + create_uipath_deep_agent, + create_uipath_deep_agent_graph, +) +from .backend import create_workspace_backend_factory +from .metadata import ( + UiPathDeepAgentHydrationPolicy, + UiPathDeepAgentRuntimeSpec, + get_uipath_deep_agent_runtime_spec, + set_uipath_deep_agent_runtime_spec, +) + +__all__ = [ + "UiPathDeepAgent", + "UiPathDeepAgentHydrationPolicy", + "UiPathDeepAgentRuntimeSpec", + "create_uipath_deep_agent", + "create_uipath_deep_agent_graph", + "create_workspace_backend_factory", + "get_uipath_deep_agent_runtime_spec", + "set_uipath_deep_agent_runtime_spec", +] diff --git a/src/uipath_langchain/deepagents/agent.py b/src/uipath_langchain/deepagents/agent.py new file mode 100644 index 000000000..59091c46d --- /dev/null +++ b/src/uipath_langchain/deepagents/agent.py @@ -0,0 +1,143 @@ +"""UiPath DeepAgents authoring helpers.""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import Any, Literal + +from deepagents import CompiledSubAgent, SubAgent +from deepagents.backends import BackendProtocol +from deepagents.backends.protocol import BackendFactory +from langchain.agents.structured_output import ResponseFormat, ToolStrategy +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool +from langgraph.graph.state import StateGraph +from pydantic import BaseModel + +from uipath_langchain.agent.advanced import create_advanced_agent_graph + +from .backend import create_workspace_backend_factory +from .metadata import ( + UiPathDeepAgentHydrationPolicy, + UiPathDeepAgentRuntimeSpec, + set_uipath_deep_agent_runtime_spec, +) + + +@dataclass(frozen=True) +class UiPathDeepAgent: + """Author-facing result for a UiPath DeepAgents graph.""" + + graph: StateGraph[Any, Any, Any, Any] + runtime_spec: UiPathDeepAgentRuntimeSpec + + +def create_uipath_deep_agent( + *, + model: BaseChatModel, + input_schema: type[BaseModel] | None = None, + output_schema: type[BaseModel], + system_prompt: str = "", + tools: Sequence[BaseTool] = (), + subagents: Sequence[SubAgent | CompiledSubAgent] = (), + backend: BackendProtocol | BackendFactory | None = None, + response_format: ResponseFormat[Any] | None = None, + build_user_message: Callable[[dict[str, Any]], str] | None = None, + interaction_mode: Literal["task", "conversation"] = "task", + workspace_scope: Literal["runtime", "conversation"] = "runtime", + workspace_config_key: str = "uipath_workspace_path", + hydration_policy: UiPathDeepAgentHydrationPolicy = "suspend_or_success", +) -> UiPathDeepAgent: + """Create a UiPath-runtime-aware DeepAgents graph. + + This PoC fully supports task-mode coded agents. Conversational mode is part + of the public contract but intentionally rejected until the chat exchange + adapter can preserve UiPath's conversational suspend/resume loop. + """ + graph = create_uipath_deep_agent_graph( + model=model, + input_schema=input_schema, + output_schema=output_schema, + system_prompt=system_prompt, + tools=tools, + subagents=subagents, + backend=backend, + response_format=response_format, + build_user_message=build_user_message, + interaction_mode=interaction_mode, + workspace_scope=workspace_scope, + workspace_config_key=workspace_config_key, + hydration_policy=hydration_policy, + ) + spec = UiPathDeepAgentRuntimeSpec( + interaction_mode=interaction_mode, + workspace_scope=workspace_scope, + workspace_config_key=workspace_config_key, + hydration_policy=hydration_policy, + ) + set_uipath_deep_agent_runtime_spec(graph, spec) + return UiPathDeepAgent(graph=graph, runtime_spec=spec) + + +def create_uipath_deep_agent_graph( + *, + model: BaseChatModel, + input_schema: type[BaseModel] | None = None, + output_schema: type[BaseModel], + system_prompt: str = "", + tools: Sequence[BaseTool] = (), + subagents: Sequence[SubAgent | CompiledSubAgent] = (), + backend: BackendProtocol | BackendFactory | None = None, + response_format: ResponseFormat[Any] | None = None, + build_user_message: Callable[[dict[str, Any]], str] | None = None, + interaction_mode: Literal["task", "conversation"] = "task", + workspace_scope: Literal["runtime", "conversation"] = "runtime", + workspace_config_key: str = "uipath_workspace_path", + hydration_policy: UiPathDeepAgentHydrationPolicy = "suspend_or_success", +) -> StateGraph[Any, Any, Any, Any]: + """Create a tagged UiPath DeepAgents graph for coded agents.""" + if interaction_mode == "conversation": + raise NotImplementedError( + "Conversational UiPath DeepAgents require a chat-exchange adapter; " + "task-mode is the only implemented PoC mode." + ) + if workspace_scope == "conversation": + raise NotImplementedError( + "Conversation-scoped DeepAgents workspaces require conversational " + "runtime support; use workspace_scope='runtime' for task mode." + ) + + backend = backend or create_workspace_backend_factory( + workspace_config_key=workspace_config_key, + ) + response_format = response_format or ToolStrategy(output_schema) + build_user_message = build_user_message or _default_user_message + + graph = create_advanced_agent_graph( + model=model, + tools=tools, + subagents=subagents, + system_prompt=system_prompt, + backend=backend, + response_format=response_format, + input_schema=input_schema, + output_schema=output_schema, + build_user_message=build_user_message, + ) + set_uipath_deep_agent_runtime_spec( + graph, + UiPathDeepAgentRuntimeSpec( + interaction_mode=interaction_mode, + workspace_scope=workspace_scope, + workspace_config_key=workspace_config_key, + hydration_policy=hydration_policy, + ), + ) + return graph + + +def _default_user_message(args: dict[str, Any]) -> str: + if not args: + return "" + return "\n".join(f"{key}: {value}" for key, value in args.items()) diff --git a/src/uipath_langchain/deepagents/backend.py b/src/uipath_langchain/deepagents/backend.py new file mode 100644 index 000000000..ce090e6db --- /dev/null +++ b/src/uipath_langchain/deepagents/backend.py @@ -0,0 +1,48 @@ +"""Workspace backend helpers for UiPath DeepAgents.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from deepagents.backends.filesystem import FilesystemBackend +from deepagents.backends.protocol import BackendFactory +from langchain.tools import ToolRuntime + +WORKSPACE_FILESYSTEM_BACKEND_ATTR = "is_uipath_workspace_filesystem_backend" + + +@dataclass(frozen=True) +class UiPathWorkspaceBackendFactory: + """DeepAgents backend factory resolved from UiPath runtime config.""" + + workspace_config_key: str = "uipath_workspace_path" + virtual_mode: bool = True + is_uipath_workspace_filesystem_backend: bool = field(default=True, init=False) + + def __call__(self, runtime: ToolRuntime) -> FilesystemBackend: + config = getattr(runtime, "config", None) or {} + configurable = ( + config.get("configurable", {}) if isinstance(config, dict) else {} + ) + workspace_path = configurable.get(self.workspace_config_key) + if not workspace_path: + raise RuntimeError( + f"UiPath DeepAgents workspace path missing from config key " + f"'{self.workspace_config_key}'." + ) + return FilesystemBackend( + root_dir=Path(workspace_path), virtual_mode=self.virtual_mode + ) + + +def create_workspace_backend_factory( + *, + workspace_config_key: str = "uipath_workspace_path", + virtual_mode: bool = True, +) -> BackendFactory: + """Create a DeepAgents backend factory bound to the runtime workspace path.""" + return UiPathWorkspaceBackendFactory( + workspace_config_key=workspace_config_key, + virtual_mode=virtual_mode, + ) diff --git a/src/uipath_langchain/deepagents/metadata.py b/src/uipath_langchain/deepagents/metadata.py new file mode 100644 index 000000000..0f05e6862 --- /dev/null +++ b/src/uipath_langchain/deepagents/metadata.py @@ -0,0 +1,62 @@ +"""Runtime metadata for UiPath DeepAgents graphs.""" + +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict + +_RUNTIME_SPEC_ATTR = "__uipath_deep_agent_runtime_spec__" + +UiPathDeepAgentHydrationPolicy = Literal[ + "suspend_only", + "suspend_or_success", + "always", +] + + +class UiPathDeepAgentRuntimeSpec(BaseModel): + """UiPath runtime contract for a DeepAgents-backed graph.""" + + model_config = ConfigDict(extra="forbid") + + kind: Literal["uipath.deepagents.v1"] = "uipath.deepagents.v1" + interaction_mode: Literal["task", "conversation"] = "task" + workspace_scope: Literal["runtime", "conversation"] = "runtime" + workspace_config_key: str = "uipath_workspace_path" + persistence: Literal["attachments"] = "attachments" + hydration_policy: UiPathDeepAgentHydrationPolicy = "suspend_or_success" + attachment_prefix: str = ".uipath-workspace" + + +def set_uipath_deep_agent_runtime_spec( + graph: Any, + spec: UiPathDeepAgentRuntimeSpec, +) -> Any: + """Attach UiPath DeepAgents runtime metadata to a graph-like object.""" + setattr(graph, _RUNTIME_SPEC_ATTR, spec) + builder = getattr(graph, "builder", None) + if builder is not None: + setattr(builder, _RUNTIME_SPEC_ATTR, spec) + return graph + + +def get_uipath_deep_agent_runtime_spec( + graph: Any, +) -> UiPathDeepAgentRuntimeSpec | None: + """Return UiPath DeepAgents runtime metadata from a graph-like object.""" + raw = getattr(graph, _RUNTIME_SPEC_ATTR, None) + if raw is None: + builder = getattr(graph, "builder", None) + raw = ( + getattr(builder, _RUNTIME_SPEC_ATTR, None) if builder is not None else None + ) + if raw is None: + return None + if isinstance(raw, UiPathDeepAgentRuntimeSpec): + return raw + if isinstance(raw, dict): + return UiPathDeepAgentRuntimeSpec.model_validate(raw) + raise TypeError( + "UiPath DeepAgents runtime spec must be a UiPathDeepAgentRuntimeSpec or dict." + ) diff --git a/src/uipath_langchain/runtime/factory.py b/src/uipath_langchain/runtime/factory.py index 9d16137b5..b92e20c06 100644 --- a/src/uipath_langchain/runtime/factory.py +++ b/src/uipath_langchain/runtime/factory.py @@ -1,5 +1,6 @@ import asyncio import os +from pathlib import Path from typing import Any, AsyncContextManager from langchain_core.callbacks import BaseCallbackHandler @@ -12,19 +13,30 @@ ) from uipath.core.adapters import EvaluatorProtocol from uipath.core.tracing import UiPathSpanUtils, UiPathTraceManager +from uipath.platform import UiPath from uipath.platform.resume_triggers import ( UiPathResumeTriggerHandler, ) from uipath.runtime import ( + HydrationPolicy, + HydrationRuntime, UiPathResumableRuntime, UiPathRuntimeContext, UiPathRuntimeFactorySettings, UiPathRuntimeProtocol, UiPathRuntimeStorageProtocol, + Workspace, + WorkspaceHydrator, + WorkspaceRegistryStore, ) from uipath.runtime.errors import UiPathErrorCategory from uipath_langchain._tracing import _instrument_traceable_attributes +from uipath_langchain.deepagents import ( + UiPathDeepAgentRuntimeSpec, + get_uipath_deep_agent_runtime_spec, + set_uipath_deep_agent_runtime_spec, +) from uipath_langchain.governance import GovernanceCallbackHandler from uipath_langchain.runtime.config import LangGraphConfig from uipath_langchain.runtime.errors import LangGraphErrorCode, LangGraphRuntimeError @@ -222,6 +234,9 @@ async def _resolve_and_compile_graph( loaded_graph = await self._load_graph(entrypoint, **kwargs) compiled_graph = await self._compile_graph(loaded_graph, memory) + spec = get_uipath_deep_agent_runtime_spec(loaded_graph) + if spec is not None: + set_uipath_deep_agent_runtime_spec(compiled_graph, spec) self._graph_cache[entrypoint] = compiled_graph @@ -300,21 +315,78 @@ async def _create_runtime_instance( else None ) + spec = get_uipath_deep_agent_runtime_spec(compiled_graph) + workspace = self._create_deep_agent_workspace(runtime_id, spec) + configurable = ( + {spec.workspace_config_key: str(workspace.path)} + if spec is not None and workspace is not None + else None + ) + base_runtime = UiPathLangGraphRuntime( graph=compiled_graph, runtime_id=runtime_id, entrypoint=entrypoint, callbacks=callbacks, storage=storage, + configurable=configurable, ) - return UiPathResumableRuntime( + resumable_runtime: UiPathRuntimeProtocol = UiPathResumableRuntime( delegate=base_runtime, storage=storage, trigger_manager=trigger_manager, runtime_id=runtime_id, ) + if spec is None or workspace is None: + return resumable_runtime + + sdk = UiPath() + registry_runtime_id = self._deep_agent_registry_runtime_id(runtime_id, spec) + return HydrationRuntime( + delegate=resumable_runtime, + workspace=workspace, + hydrator=WorkspaceHydrator( + workspace_path=workspace.path, + attachments=sdk.attachments, + jobs=getattr(sdk, "jobs", None), + current_job_key=self.context.job_id, + folder_key=self.context.folder_key, + attachment_prefix=spec.attachment_prefix, + ), + registry_store=WorkspaceRegistryStore( + storage, + registry_runtime_id, + ), + policy=HydrationPolicy(spec.hydration_policy), + ) + + def _create_deep_agent_workspace( + self, + runtime_id: str, + spec: UiPathDeepAgentRuntimeSpec | None, + ) -> Workspace | None: + if spec is None: + return None + if spec.interaction_mode == "conversation": + raise NotImplementedError( + "Conversational UiPath DeepAgents require a chat-runtime adapter. " + "Task-mode DeepAgents are supported by this PoC." + ) + + base_dir = Path(self.context.runtime_dir or "__uipath") / "workspaces" + return Workspace.create(base_dir / runtime_id, cleanup=True) + + def _deep_agent_registry_runtime_id( + self, + runtime_id: str, + spec: UiPathDeepAgentRuntimeSpec, + ) -> str: + if spec.workspace_scope == "conversation" and self.context.conversation_id: + return self.context.conversation_id + return runtime_id + async def new_runtime( self, entrypoint: str, diff --git a/src/uipath_langchain/runtime/runtime.py b/src/uipath_langchain/runtime/runtime.py index 04657b1e7..86ffdd299 100644 --- a/src/uipath_langchain/runtime/runtime.py +++ b/src/uipath_langchain/runtime/runtime.py @@ -91,6 +91,7 @@ def __init__( entrypoint: str | None = None, callbacks: list[BaseCallbackHandler] | None = None, storage: UiPathRuntimeStorageProtocol | None = None, + configurable: dict[str, Any] | None = None, ): """ Initialize the runtime. @@ -104,6 +105,7 @@ def __init__( self.runtime_id: str = runtime_id or "default" self.entrypoint: str | None = entrypoint self.callbacks: list[BaseCallbackHandler] = callbacks or [] + self.configurable = configurable or {} self.chat = UiPathChatMessagesMapper(self.runtime_id, storage) self.chat.tools_requiring_confirmation = self._get_tool_confirmation_info() self.chat.client_side_tools = self._get_client_side_tools() @@ -349,7 +351,7 @@ def create_runtime_error(self, e: Exception) -> UiPathBaseRuntimeError: def _get_graph_config(self) -> RunnableConfig: """Build graph execution configuration.""" graph_config: RunnableConfig = { - "configurable": {"thread_id": self.runtime_id}, + "configurable": {"thread_id": self.runtime_id, **self.configurable}, "callbacks": self.callbacks, } diff --git a/tests/agent/advanced/test_create_advanced_agent_graph.py b/tests/agent/advanced/test_create_advanced_agent_graph.py index 4a788d6b0..d46b3b201 100644 --- a/tests/agent/advanced/test_create_advanced_agent_graph.py +++ b/tests/agent/advanced/test_create_advanced_agent_graph.py @@ -56,6 +56,23 @@ def test_wrapper_graph_has_io_nodes() -> None: assert {"transform_input", "advanced_agent", "transform_output"} <= set(graph.nodes) +def test_subagents_are_passed_to_inner_advanced_agent() -> None: + """The typed I/O wrapper preserves DeepAgents sub-agent delegation.""" + subagent = { + "name": "researcher", + "description": "Researches one part of the task", + "system_prompt": "Research carefully.", + } + + with patch( + "uipath_langchain.agent.advanced.agent.create_advanced_agent", + return_value=MagicMock(), + ) as mock_create: + _build(subagents=[subagent]) + + assert mock_create.call_args.kwargs["subagents"] == [subagent] + + @pytest.mark.asyncio async def test_transform_input_without_schema_builds_single_user_message() -> None: """With no input schema, the built message comes straight from build_user_message.""" diff --git a/tests/agent/advanced/test_memory_injection.py b/tests/agent/advanced/test_memory_injection.py index 5a54e814e..cb31b2103 100644 --- a/tests/agent/advanced/test_memory_injection.py +++ b/tests/agent/advanced/test_memory_injection.py @@ -68,6 +68,16 @@ def test_enables_memory_for_filesystem_backend(self, tmp_path: Any) -> None: backend = FilesystemBackend(root_dir=tmp_path, virtual_mode=True) assert _memory_kwarg(backend) == [MEMORY_INDEX_VIRTUAL_PATH] + def test_enables_memory_for_marked_runtime_workspace_factory( + self, tmp_path: Any + ) -> None: + def backend_factory(runtime: Any) -> FilesystemBackend: + return FilesystemBackend(root_dir=tmp_path, virtual_mode=True) + + backend_factory.is_uipath_workspace_filesystem_backend = True # type: ignore[attr-defined] + + assert _memory_kwarg(backend_factory) == [MEMORY_INDEX_VIRTUAL_PATH] + def test_disables_memory_for_non_filesystem_backend(self) -> None: # The default in-state backend (None) carries no durable workspace, so # passing memory=None leaves MemoryMiddleware out of the stack entirely. diff --git a/tests/agent/advanced/test_utils.py b/tests/agent/advanced/test_utils.py index c26865cd4..202db0cb4 100644 --- a/tests/agent/advanced/test_utils.py +++ b/tests/agent/advanced/test_utils.py @@ -71,6 +71,51 @@ async def test_resolve_input_attachments_downloads_and_adds_filepath( assert result["question"] == "summarize" +@pytest.mark.asyncio +async def test_resolve_input_attachments_uses_marked_runtime_workspace_factory( + tmp_path: Path, +) -> None: + """Runtime workspace factories resolve from RunnableConfig before download.""" + attachment_id = uuid.uuid4() + input_args = { + "book": { + "ID": str(attachment_id), + "FullName": "novel.txt", + "MimeType": "text/plain", + }, + } + + seen_config: dict[str, Any] = {} + + def backend_factory(runtime: Any) -> FilesystemBackend: + seen_config.update(runtime.config) + return FilesystemBackend( + root_dir=runtime.config["configurable"]["workspace"], + virtual_mode=True, + ) + + backend_factory.is_uipath_workspace_filesystem_backend = True # type: ignore[attr-defined] + + mock_client = MagicMock() + mock_client.attachments.download_async = AsyncMock() + with patch( + "uipath_langchain.agent.advanced.utils.UiPath", + return_value=mock_client, + ): + result = await resolve_input_attachments( + backend_factory, + ["$.book"], + input_args, + config={"configurable": {"workspace": str(tmp_path)}}, + ) + + assert seen_config == {"configurable": {"workspace": str(tmp_path)}} + expected_name = f"{attachment_id}_novel.txt" + call_kwargs = mock_client.attachments.download_async.call_args.kwargs + assert call_kwargs["destination_path"] == str(tmp_path / expected_name) + assert result["book"]["FilePath"] == f"/{expected_name}" + + @pytest.mark.asyncio async def test_resolve_input_attachments_skips_non_ticket_matches( tmp_path: Path, diff --git a/tests/deepagents/__init__.py b/tests/deepagents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/deepagents/test_backend.py b/tests/deepagents/test_backend.py new file mode 100644 index 000000000..455249b72 --- /dev/null +++ b/tests/deepagents/test_backend.py @@ -0,0 +1,40 @@ +from typing import Any + +import pytest +from deepagents.backends.filesystem import FilesystemBackend +from langchain.tools import ToolRuntime + +from uipath_langchain.deepagents.backend import ( + WORKSPACE_FILESYSTEM_BACKEND_ATTR, + create_workspace_backend_factory, +) + + +def _tool_runtime(config: dict[str, Any]) -> ToolRuntime: + return ToolRuntime( + state=None, + context=None, + config=config, + stream_writer=lambda _: None, + tool_call_id=None, + store=None, + ) + + +def test_workspace_backend_factory_resolves_configured_workspace(tmp_path) -> None: + factory = create_workspace_backend_factory(workspace_config_key="workspace") + + assert getattr(factory, WORKSPACE_FILESYSTEM_BACKEND_ATTR) is True + + backend = factory(_tool_runtime({"configurable": {"workspace": str(tmp_path)}})) + + assert isinstance(backend, FilesystemBackend) + assert backend.cwd == tmp_path.resolve() + assert backend.virtual_mode is True + + +def test_workspace_backend_factory_raises_when_config_missing() -> None: + factory = create_workspace_backend_factory(workspace_config_key="workspace") + + with pytest.raises(RuntimeError, match="workspace path missing"): + factory(_tool_runtime({"configurable": {}})) diff --git a/tests/deepagents/test_metadata.py b/tests/deepagents/test_metadata.py new file mode 100644 index 000000000..1f5722839 --- /dev/null +++ b/tests/deepagents/test_metadata.py @@ -0,0 +1,93 @@ +from unittest.mock import patch + +import pytest +from langgraph.graph import END, START, StateGraph +from pydantic import BaseModel + +from uipath_langchain.deepagents import ( + UiPathDeepAgentRuntimeSpec, + get_uipath_deep_agent_runtime_spec, + set_uipath_deep_agent_runtime_spec, +) +from uipath_langchain.deepagents.agent import create_uipath_deep_agent_graph + + +class State(BaseModel): + value: str = "" + + +class Output(BaseModel): + result: str + + +def _graph() -> StateGraph: + async def node(state: State) -> State: + return state + + builder = StateGraph(State) + builder.add_node("node", node) + builder.add_edge(START, "node") + builder.add_edge("node", END) + return builder + + +def test_runtime_spec_round_trips_on_builder_and_compiled_graph() -> None: + graph = _graph() + spec = UiPathDeepAgentRuntimeSpec(workspace_config_key="workspace") + + set_uipath_deep_agent_runtime_spec(graph, spec) + compiled = graph.compile() + + assert get_uipath_deep_agent_runtime_spec(graph) == spec + assert get_uipath_deep_agent_runtime_spec(compiled) == spec + + +def test_runtime_spec_defaults_to_success_persistence() -> None: + spec = UiPathDeepAgentRuntimeSpec() + + assert spec.hydration_policy == "suspend_or_success" + + +def test_uipath_deep_agent_graph_exposes_hydration_policy() -> None: + with patch( + "uipath_langchain.deepagents.agent.create_advanced_agent_graph", + return_value=_graph(), + ): + graph = create_uipath_deep_agent_graph( + model=object(), # type: ignore[arg-type] + output_schema=Output, + hydration_policy="always", + ) + + spec = get_uipath_deep_agent_runtime_spec(graph) + assert spec is not None + assert spec.hydration_policy == "always" + + +def test_conversational_mode_is_explicitly_not_implemented() -> None: + with pytest.raises(NotImplementedError, match="Conversational UiPath DeepAgents"): + create_uipath_deep_agent_graph( + model=object(), # type: ignore[arg-type] + output_schema=Output, + interaction_mode="conversation", + ) + + +def test_subagents_are_forwarded_to_advanced_agent_graph() -> None: + subagent = { + "name": "researcher", + "description": "Researches one part of the task", + "system_prompt": "Research carefully.", + } + + with patch( + "uipath_langchain.deepagents.agent.create_advanced_agent_graph", + return_value=_graph(), + ) as mock_create: + create_uipath_deep_agent_graph( + model=object(), # type: ignore[arg-type] + output_schema=Output, + subagents=[subagent], + ) + + assert mock_create.call_args.kwargs["subagents"] == [subagent] diff --git a/tests/deepagents/test_runtime_config.py b/tests/deepagents/test_runtime_config.py new file mode 100644 index 000000000..6374d5250 --- /dev/null +++ b/tests/deepagents/test_runtime_config.py @@ -0,0 +1,16 @@ +from unittest.mock import MagicMock + +from uipath_langchain.runtime.runtime import UiPathLangGraphRuntime + + +def test_runtime_merges_deep_agent_configurable_values() -> None: + runtime = UiPathLangGraphRuntime( + graph=MagicMock(), + runtime_id="runtime-1", + configurable={"uipath_workspace_path": "/tmp/workspace"}, + ) + + config = runtime._get_graph_config() + + assert config["configurable"]["thread_id"] == "runtime-1" + assert config["configurable"]["uipath_workspace_path"] == "/tmp/workspace" diff --git a/tests/deepagents/test_runtime_factory.py b/tests/deepagents/test_runtime_factory.py new file mode 100644 index 000000000..9aa8fcc0e --- /dev/null +++ b/tests/deepagents/test_runtime_factory.py @@ -0,0 +1,68 @@ +from typing import Any, TypedDict +from unittest.mock import MagicMock, patch + +from langgraph.graph import END, START, StateGraph +from uipath.runtime import HydrationPolicy, HydrationRuntime, UiPathRuntimeContext + +from uipath_langchain.deepagents import ( + UiPathDeepAgentRuntimeSpec, + set_uipath_deep_agent_runtime_spec, +) +from uipath_langchain.runtime.factory import UiPathLangGraphRuntimeFactory + + +class _State(TypedDict): + value: str + + +def _build_graph() -> StateGraph[Any, Any, Any]: + graph = StateGraph(_State) + graph.add_node("noop", lambda state: state) + graph.add_edge(START, "noop") + graph.add_edge("noop", END) + return graph + + +async def test_tagged_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> None: + context = UiPathRuntimeContext( + runtime_dir=str(tmp_path), + state_file="state.db", + job_id="job-1", + folder_key="folder-1", + ) + factory = UiPathLangGraphRuntimeFactory(context) + compiled = _build_graph().compile() + spec = UiPathDeepAgentRuntimeSpec( + workspace_config_key="workspace", + hydration_policy="always", + attachment_prefix=".deepagent-workspace", + ) + set_uipath_deep_agent_runtime_spec(compiled, spec) + + sdk = MagicMock() + with patch("uipath_langchain.runtime.factory.UiPath", return_value=sdk): + runtime = await factory._create_runtime_instance( + compiled_graph=compiled, + runtime_id="runtime-1", + entrypoint="agent", + ) + + assert isinstance(runtime, HydrationRuntime) + assert runtime.policy == HydrationPolicy.ALWAYS + assert runtime.workspace.path.parent == tmp_path / "workspaces" + + resumable_runtime = runtime.delegate + langgraph_runtime = resumable_runtime.delegate + assert langgraph_runtime.configurable == { + "workspace": str(runtime.workspace.path), + } + + assert runtime.hydrator.workspace_path == runtime.workspace.path + assert runtime.hydrator.attachments is sdk.attachments + assert runtime.hydrator.jobs is sdk.jobs + assert runtime.hydrator.current_job_key == "job-1" + assert runtime.hydrator.folder_key == "folder-1" + assert runtime.hydrator.attachment_prefix == ".deepagent-workspace" + assert runtime.registry_store.runtime_id == "runtime-1" + + await factory.dispose() From 189d2ddc93ad0161b0591edc675ea7dcfbaa4a7f Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Mon, 13 Jul 2026 18:03:44 +0300 Subject: [PATCH 2/6] Support input-rendered coded DeepAgent prompts --- samples/coded-deepagent/README.md | 1 + samples/coded-deepagent/graph.py | 12 ++++-- src/uipath_langchain/agent/advanced/agent.py | 11 ++++- src/uipath_langchain/deepagents/__init__.py | 2 + src/uipath_langchain/deepagents/agent.py | 38 ++++++++++++++-- .../test_create_advanced_agent_graph.py | 37 +++++++++++++++- tests/deepagents/test_metadata.py | 43 +++++++++++++++++++ 7 files changed, 133 insertions(+), 11 deletions(-) diff --git a/samples/coded-deepagent/README.md b/samples/coded-deepagent/README.md index 1a1e750c0..604c64234 100644 --- a/samples/coded-deepagent/README.md +++ b/samples/coded-deepagent/README.md @@ -13,6 +13,7 @@ bucket backend. ## What It Shows - Typed coded-agent input and output with Pydantic models. +- System and user prompts rendered from typed input. - A standard LangChain tool used by the main DeepAgent. - A DeepAgents subagent used for risk review. - Runtime-provided workspace persistence through the UiPath contract. diff --git a/samples/coded-deepagent/graph.py b/samples/coded-deepagent/graph.py index 1c6930d4a..c73cc3595 100644 --- a/samples/coded-deepagent/graph.py +++ b/samples/coded-deepagent/graph.py @@ -55,12 +55,16 @@ def score_launch_readiness(audience: str, constraints: list[str]) -> str: "model": MODEL, } -SYSTEM_PROMPT = """You are a product launch planning agent. + +def build_system_prompt(args: dict) -> str: + return f"""You are a product launch planning agent for {args["product_name"]}. Use the planning tools provided by DeepAgents. Use score_launch_readiness to get a deterministic readiness signal. Delegate a plan review to risk_reviewer before finalizing the answer. +Tailor all planning to this audience: {args["audience"]}. + The UiPath runtime provides your DeepAgents filesystem through the tagged graph contract. Write these workspace files before producing the final answer: - /launch/brief.md with the final launch brief @@ -70,7 +74,7 @@ def score_launch_readiness(audience: str, constraints: list[str]) -> str: you wrote in workspace_files.""" -def build_user_message(args: dict) -> str: +def build_user_prompt(args: dict) -> str: constraints = args.get("constraints") or [] constraint_lines = "\n".join(f"- {item}" for item in constraints) or "- None" return f"""Create a launch brief. @@ -87,8 +91,8 @@ def build_user_message(args: dict) -> str: model=MODEL, input_schema=BriefInput, output_schema=BriefOutput, - system_prompt=SYSTEM_PROMPT, + system_prompt=build_system_prompt, + user_prompt=build_user_prompt, tools=[score_launch_readiness], subagents=[RISK_REVIEWER], - build_user_message=build_user_message, ) diff --git a/src/uipath_langchain/agent/advanced/agent.py b/src/uipath_langchain/agent/advanced/agent.py index ababb10ee..daa666177 100644 --- a/src/uipath_langchain/agent/advanced/agent.py +++ b/src/uipath_langchain/agent/advanced/agent.py @@ -9,7 +9,7 @@ from deepagents.backends.protocol import BackendFactory from langchain.agents.structured_output import ResponseFormat from langchain_core.language_models import BaseChatModel -from langchain_core.messages import HumanMessage +from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.runnables.config import RunnableConfig from langchain_core.tools import BaseTool from langgraph.graph import END, START @@ -63,6 +63,7 @@ def create_advanced_agent_graph( input_schema: type[BaseModel] | None, output_schema: type[BaseModel], build_user_message: Callable[[dict[str, Any]], str], + build_system_message: Callable[[dict[str, Any]], str] | None = None, subagents: Sequence[SubAgent | CompiledSubAgent] = (), ) -> StateGraph[Any, Any, Any, Any]: """Wrap the advanced agent in a parent graph that maps typed I/O to/from messages. @@ -112,8 +113,14 @@ async def transform_input_async( state=state, config=config, ) + messages = [] + if build_system_message is not None: + system_text = build_system_message(input_args) + if system_text: + messages.append(SystemMessage(content=system_text, id="system-input")) user_text = build_user_message(input_args) - return {"messages": [HumanMessage(content=user_text, id="user-input")]} + messages.append(HumanMessage(content=user_text, id="user-input")) + return {"messages": messages} def transform_output(state: BaseModel) -> dict[str, Any]: structured = getattr(state, "structured_response", {}) diff --git a/src/uipath_langchain/deepagents/__init__.py b/src/uipath_langchain/deepagents/__init__.py index abf9badec..3b3b4ef05 100644 --- a/src/uipath_langchain/deepagents/__init__.py +++ b/src/uipath_langchain/deepagents/__init__.py @@ -2,6 +2,7 @@ from .agent import ( UiPathDeepAgent, + UiPathDeepAgentPrompt, create_uipath_deep_agent, create_uipath_deep_agent_graph, ) @@ -16,6 +17,7 @@ __all__ = [ "UiPathDeepAgent", "UiPathDeepAgentHydrationPolicy", + "UiPathDeepAgentPrompt", "UiPathDeepAgentRuntimeSpec", "create_uipath_deep_agent", "create_uipath_deep_agent_graph", diff --git a/src/uipath_langchain/deepagents/agent.py b/src/uipath_langchain/deepagents/agent.py index 59091c46d..2276f4905 100644 --- a/src/uipath_langchain/deepagents/agent.py +++ b/src/uipath_langchain/deepagents/agent.py @@ -24,6 +24,8 @@ set_uipath_deep_agent_runtime_spec, ) +UiPathDeepAgentPrompt = str | Callable[[dict[str, Any]], str] + @dataclass(frozen=True) class UiPathDeepAgent: @@ -38,7 +40,8 @@ def create_uipath_deep_agent( model: BaseChatModel, input_schema: type[BaseModel] | None = None, output_schema: type[BaseModel], - system_prompt: str = "", + system_prompt: UiPathDeepAgentPrompt = "", + user_prompt: UiPathDeepAgentPrompt | None = None, tools: Sequence[BaseTool] = (), subagents: Sequence[SubAgent | CompiledSubAgent] = (), backend: BackendProtocol | BackendFactory | None = None, @@ -60,6 +63,7 @@ def create_uipath_deep_agent( input_schema=input_schema, output_schema=output_schema, system_prompt=system_prompt, + user_prompt=user_prompt, tools=tools, subagents=subagents, backend=backend, @@ -85,7 +89,8 @@ def create_uipath_deep_agent_graph( model: BaseChatModel, input_schema: type[BaseModel] | None = None, output_schema: type[BaseModel], - system_prompt: str = "", + system_prompt: UiPathDeepAgentPrompt = "", + user_prompt: UiPathDeepAgentPrompt | None = None, tools: Sequence[BaseTool] = (), subagents: Sequence[SubAgent | CompiledSubAgent] = (), backend: BackendProtocol | BackendFactory | None = None, @@ -112,18 +117,27 @@ def create_uipath_deep_agent_graph( workspace_config_key=workspace_config_key, ) response_format = response_format or ToolStrategy(output_schema) - build_user_message = build_user_message or _default_user_message + if user_prompt is not None and build_user_message is not None: + raise ValueError("Use either user_prompt or build_user_message, not both.") + + static_system_prompt, build_system_message = _split_system_prompt(system_prompt) + build_user_message = ( + _coerce_prompt_renderer(user_prompt) + if user_prompt is not None + else build_user_message or _default_user_message + ) graph = create_advanced_agent_graph( model=model, tools=tools, subagents=subagents, - system_prompt=system_prompt, + system_prompt=static_system_prompt, backend=backend, response_format=response_format, input_schema=input_schema, output_schema=output_schema, build_user_message=build_user_message, + build_system_message=build_system_message, ) set_uipath_deep_agent_runtime_spec( graph, @@ -137,6 +151,22 @@ def create_uipath_deep_agent_graph( return graph +def _split_system_prompt( + system_prompt: UiPathDeepAgentPrompt, +) -> tuple[str, Callable[[dict[str, Any]], str] | None]: + if callable(system_prompt): + return "", system_prompt + return system_prompt, None + + +def _coerce_prompt_renderer( + prompt: UiPathDeepAgentPrompt, +) -> Callable[[dict[str, Any]], str]: + if callable(prompt): + return prompt + return lambda _args: prompt + + def _default_user_message(args: dict[str, Any]) -> str: if not args: return "" diff --git a/tests/agent/advanced/test_create_advanced_agent_graph.py b/tests/agent/advanced/test_create_advanced_agent_graph.py index d46b3b201..9b9dce9bc 100644 --- a/tests/agent/advanced/test_create_advanced_agent_graph.py +++ b/tests/agent/advanced/test_create_advanced_agent_graph.py @@ -5,7 +5,7 @@ import pytest from langchain_core.language_models import BaseChatModel -from langchain_core.messages import HumanMessage +from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel, ConfigDict, Field from uipath_langchain.agent.advanced.agent import create_advanced_agent_graph @@ -86,6 +86,41 @@ async def test_transform_input_without_schema_builds_single_user_message() -> No assert message.id == "user-input" +@pytest.mark.asyncio +async def test_transform_input_includes_dynamic_system_message() -> None: + """Runtime input can render a system message before the user message.""" + graph = _build( + input_schema=_Input, + build_system_message=lambda args: f"system:{args['question']}", + build_user_message=lambda args: f"user:{args['question']}", + ) + state_cls = create_state_with_input(_Input) + state = state_cls(question="q") + + out = await graph.nodes["transform_input"].runnable.ainvoke(state) + + system_message, user_message = out["messages"] + assert isinstance(system_message, SystemMessage) + assert system_message.content == "system:q" + assert system_message.id == "system-input" + assert isinstance(user_message, HumanMessage) + assert user_message.content == "user:q" + assert user_message.id == "user-input" + + +@pytest.mark.asyncio +async def test_transform_input_omits_empty_dynamic_system_message() -> None: + """An empty rendered system prompt does not create a blank message.""" + graph = _build(build_system_message=lambda _args: "") + + out = await graph.nodes["transform_input"].runnable.ainvoke( + AdvancedAgentGraphState() + ) + + assert len(out["messages"]) == 1 + assert isinstance(out["messages"][0], HumanMessage) + + @pytest.mark.asyncio async def test_transform_input_resolves_attachments_when_present() -> None: """With an input schema and attachment paths, input attachments are resolved first.""" diff --git a/tests/deepagents/test_metadata.py b/tests/deepagents/test_metadata.py index 1f5722839..6008d2132 100644 --- a/tests/deepagents/test_metadata.py +++ b/tests/deepagents/test_metadata.py @@ -91,3 +91,46 @@ def test_subagents_are_forwarded_to_advanced_agent_graph() -> None: ) assert mock_create.call_args.kwargs["subagents"] == [subagent] + + +def test_dynamic_system_prompt_is_forwarded_as_runtime_renderer() -> None: + def build_system_prompt(args: dict[str, str]) -> str: + return f"System for {args['topic']}" + + with patch( + "uipath_langchain.deepagents.agent.create_advanced_agent_graph", + return_value=_graph(), + ) as mock_create: + create_uipath_deep_agent_graph( + model=object(), # type: ignore[arg-type] + output_schema=Output, + system_prompt=build_system_prompt, + ) + + assert mock_create.call_args.kwargs["system_prompt"] == "" + assert mock_create.call_args.kwargs["build_system_message"] is build_system_prompt + + +def test_static_user_prompt_is_forwarded_as_user_renderer() -> None: + with patch( + "uipath_langchain.deepagents.agent.create_advanced_agent_graph", + return_value=_graph(), + ) as mock_create: + create_uipath_deep_agent_graph( + model=object(), # type: ignore[arg-type] + output_schema=Output, + user_prompt="Create the brief.", + ) + + build_user_message = mock_create.call_args.kwargs["build_user_message"] + assert build_user_message({}) == "Create the brief." + + +def test_user_prompt_and_build_user_message_are_mutually_exclusive() -> None: + with pytest.raises(ValueError, match="either user_prompt or build_user_message"): + create_uipath_deep_agent_graph( + model=object(), # type: ignore[arg-type] + output_schema=Output, + user_prompt="Create the brief.", + build_user_message=lambda _args: "Create the brief.", + ) From 6b5e888eed0d7fcf9665ecc9359d0d114f7262e0 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Wed, 15 Jul 2026 12:07:16 +0300 Subject: [PATCH 3/6] Narrow coded DeepAgents public surface --- src/uipath_langchain/deepagents/__init__.py | 12 ---- src/uipath_langchain/deepagents/agent.py | 66 +++------------------ src/uipath_langchain/deepagents/metadata.py | 39 +++++++++++- src/uipath_langchain/runtime/factory.py | 2 +- tests/deepagents/test_metadata.py | 47 +++++++++------ tests/deepagents/test_runtime_factory.py | 2 +- 6 files changed, 77 insertions(+), 91 deletions(-) diff --git a/src/uipath_langchain/deepagents/__init__.py b/src/uipath_langchain/deepagents/__init__.py index 3b3b4ef05..c50cfdf61 100644 --- a/src/uipath_langchain/deepagents/__init__.py +++ b/src/uipath_langchain/deepagents/__init__.py @@ -6,22 +6,10 @@ create_uipath_deep_agent, create_uipath_deep_agent_graph, ) -from .backend import create_workspace_backend_factory -from .metadata import ( - UiPathDeepAgentHydrationPolicy, - UiPathDeepAgentRuntimeSpec, - get_uipath_deep_agent_runtime_spec, - set_uipath_deep_agent_runtime_spec, -) __all__ = [ "UiPathDeepAgent", - "UiPathDeepAgentHydrationPolicy", "UiPathDeepAgentPrompt", - "UiPathDeepAgentRuntimeSpec", "create_uipath_deep_agent", "create_uipath_deep_agent_graph", - "create_workspace_backend_factory", - "get_uipath_deep_agent_runtime_spec", - "set_uipath_deep_agent_runtime_spec", ] diff --git a/src/uipath_langchain/deepagents/agent.py b/src/uipath_langchain/deepagents/agent.py index 2276f4905..2577334c8 100644 --- a/src/uipath_langchain/deepagents/agent.py +++ b/src/uipath_langchain/deepagents/agent.py @@ -4,12 +4,10 @@ from collections.abc import Callable, Sequence from dataclasses import dataclass -from typing import Any, Literal +from typing import Any from deepagents import CompiledSubAgent, SubAgent -from deepagents.backends import BackendProtocol -from deepagents.backends.protocol import BackendFactory -from langchain.agents.structured_output import ResponseFormat, ToolStrategy +from langchain.agents.structured_output import ToolStrategy from langchain_core.language_models import BaseChatModel from langchain_core.tools import BaseTool from langgraph.graph.state import StateGraph @@ -19,7 +17,6 @@ from .backend import create_workspace_backend_factory from .metadata import ( - UiPathDeepAgentHydrationPolicy, UiPathDeepAgentRuntimeSpec, set_uipath_deep_agent_runtime_spec, ) @@ -32,7 +29,6 @@ class UiPathDeepAgent: """Author-facing result for a UiPath DeepAgents graph.""" graph: StateGraph[Any, Any, Any, Any] - runtime_spec: UiPathDeepAgentRuntimeSpec def create_uipath_deep_agent( @@ -44,20 +40,9 @@ def create_uipath_deep_agent( user_prompt: UiPathDeepAgentPrompt | None = None, tools: Sequence[BaseTool] = (), subagents: Sequence[SubAgent | CompiledSubAgent] = (), - backend: BackendProtocol | BackendFactory | None = None, - response_format: ResponseFormat[Any] | None = None, build_user_message: Callable[[dict[str, Any]], str] | None = None, - interaction_mode: Literal["task", "conversation"] = "task", - workspace_scope: Literal["runtime", "conversation"] = "runtime", - workspace_config_key: str = "uipath_workspace_path", - hydration_policy: UiPathDeepAgentHydrationPolicy = "suspend_or_success", ) -> UiPathDeepAgent: - """Create a UiPath-runtime-aware DeepAgents graph. - - This PoC fully supports task-mode coded agents. Conversational mode is part - of the public contract but intentionally rejected until the chat exchange - adapter can preserve UiPath's conversational suspend/resume loop. - """ + """Create a UiPath-runtime-aware DeepAgents graph.""" graph = create_uipath_deep_agent_graph( model=model, input_schema=input_schema, @@ -66,22 +51,11 @@ def create_uipath_deep_agent( user_prompt=user_prompt, tools=tools, subagents=subagents, - backend=backend, - response_format=response_format, build_user_message=build_user_message, - interaction_mode=interaction_mode, - workspace_scope=workspace_scope, - workspace_config_key=workspace_config_key, - hydration_policy=hydration_policy, - ) - spec = UiPathDeepAgentRuntimeSpec( - interaction_mode=interaction_mode, - workspace_scope=workspace_scope, - workspace_config_key=workspace_config_key, - hydration_policy=hydration_policy, ) + spec = UiPathDeepAgentRuntimeSpec() set_uipath_deep_agent_runtime_spec(graph, spec) - return UiPathDeepAgent(graph=graph, runtime_spec=spec) + return UiPathDeepAgent(graph=graph) def create_uipath_deep_agent_graph( @@ -93,30 +67,11 @@ def create_uipath_deep_agent_graph( user_prompt: UiPathDeepAgentPrompt | None = None, tools: Sequence[BaseTool] = (), subagents: Sequence[SubAgent | CompiledSubAgent] = (), - backend: BackendProtocol | BackendFactory | None = None, - response_format: ResponseFormat[Any] | None = None, build_user_message: Callable[[dict[str, Any]], str] | None = None, - interaction_mode: Literal["task", "conversation"] = "task", - workspace_scope: Literal["runtime", "conversation"] = "runtime", - workspace_config_key: str = "uipath_workspace_path", - hydration_policy: UiPathDeepAgentHydrationPolicy = "suspend_or_success", ) -> StateGraph[Any, Any, Any, Any]: """Create a tagged UiPath DeepAgents graph for coded agents.""" - if interaction_mode == "conversation": - raise NotImplementedError( - "Conversational UiPath DeepAgents require a chat-exchange adapter; " - "task-mode is the only implemented PoC mode." - ) - if workspace_scope == "conversation": - raise NotImplementedError( - "Conversation-scoped DeepAgents workspaces require conversational " - "runtime support; use workspace_scope='runtime' for task mode." - ) - - backend = backend or create_workspace_backend_factory( - workspace_config_key=workspace_config_key, - ) - response_format = response_format or ToolStrategy(output_schema) + backend = create_workspace_backend_factory() + response_format = ToolStrategy(output_schema) if user_prompt is not None and build_user_message is not None: raise ValueError("Use either user_prompt or build_user_message, not both.") @@ -141,12 +96,7 @@ def create_uipath_deep_agent_graph( ) set_uipath_deep_agent_runtime_spec( graph, - UiPathDeepAgentRuntimeSpec( - interaction_mode=interaction_mode, - workspace_scope=workspace_scope, - workspace_config_key=workspace_config_key, - hydration_policy=hydration_policy, - ), + UiPathDeepAgentRuntimeSpec(), ) return graph diff --git a/src/uipath_langchain/deepagents/metadata.py b/src/uipath_langchain/deepagents/metadata.py index 0f05e6862..c95030b7d 100644 --- a/src/uipath_langchain/deepagents/metadata.py +++ b/src/uipath_langchain/deepagents/metadata.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict _RUNTIME_SPEC_ATTR = "__uipath_deep_agent_runtime_spec__" +_DEEPAGENTS_INTEGRATION_NAME = "deepagents" UiPathDeepAgentHydrationPolicy = Literal[ "suspend_only", @@ -52,7 +53,7 @@ def get_uipath_deep_agent_runtime_spec( getattr(builder, _RUNTIME_SPEC_ATTR, None) if builder is not None else None ) if raw is None: - return None + return UiPathDeepAgentRuntimeSpec() if is_deep_agent_graph(graph) else None if isinstance(raw, UiPathDeepAgentRuntimeSpec): return raw if isinstance(raw, dict): @@ -60,3 +61,39 @@ def get_uipath_deep_agent_runtime_spec( raise TypeError( "UiPath DeepAgents runtime spec must be a UiPathDeepAgentRuntimeSpec or dict." ) + + +def is_deep_agent_graph(graph: Any) -> bool: + """Return whether a graph-like object carries DeepAgents LangSmith metadata.""" + return _contains_deepagents_metadata(graph, seen=set()) + + +def _contains_deepagents_metadata(graph: Any, seen: set[int]) -> bool: + graph_id = id(graph) + if graph_id in seen: + return False + seen.add(graph_id) + + if _has_deepagents_metadata(graph): + return True + + builder = getattr(graph, "builder", None) + if builder is not None and _contains_deepagents_metadata(builder, seen): + return True + + nodes = getattr(graph, "nodes", None) + if isinstance(nodes, dict): + for node in nodes.values(): + if _contains_deepagents_metadata(node, seen): + return True + runnable = getattr(node, "runnable", None) + if runnable is not None and _contains_deepagents_metadata(runnable, seen): + return True + + return False + + +def _has_deepagents_metadata(graph: Any) -> bool: + config = getattr(graph, "config", None) or {} + metadata = config.get("metadata", {}) if isinstance(config, dict) else {} + return metadata.get("ls_integration") == _DEEPAGENTS_INTEGRATION_NAME diff --git a/src/uipath_langchain/runtime/factory.py b/src/uipath_langchain/runtime/factory.py index b92e20c06..afc84d20b 100644 --- a/src/uipath_langchain/runtime/factory.py +++ b/src/uipath_langchain/runtime/factory.py @@ -32,7 +32,7 @@ from uipath.runtime.errors import UiPathErrorCategory from uipath_langchain._tracing import _instrument_traceable_attributes -from uipath_langchain.deepagents import ( +from uipath_langchain.deepagents.metadata import ( UiPathDeepAgentRuntimeSpec, get_uipath_deep_agent_runtime_spec, set_uipath_deep_agent_runtime_spec, diff --git a/tests/deepagents/test_metadata.py b/tests/deepagents/test_metadata.py index 6008d2132..59ec02581 100644 --- a/tests/deepagents/test_metadata.py +++ b/tests/deepagents/test_metadata.py @@ -1,15 +1,19 @@ +from types import SimpleNamespace from unittest.mock import patch import pytest +from langchain.agents.structured_output import ToolStrategy from langgraph.graph import END, START, StateGraph from pydantic import BaseModel -from uipath_langchain.deepagents import ( +from uipath_langchain.deepagents.agent import create_uipath_deep_agent_graph +from uipath_langchain.deepagents.backend import UiPathWorkspaceBackendFactory +from uipath_langchain.deepagents.metadata import ( UiPathDeepAgentRuntimeSpec, get_uipath_deep_agent_runtime_spec, + is_deep_agent_graph, set_uipath_deep_agent_runtime_spec, ) -from uipath_langchain.deepagents.agent import create_uipath_deep_agent_graph class State(BaseModel): @@ -48,30 +52,20 @@ def test_runtime_spec_defaults_to_success_persistence() -> None: assert spec.hydration_policy == "suspend_or_success" -def test_uipath_deep_agent_graph_exposes_hydration_policy() -> None: +def test_uipath_deep_agent_graph_uses_internal_runtime_defaults() -> None: with patch( "uipath_langchain.deepagents.agent.create_advanced_agent_graph", return_value=_graph(), - ): - graph = create_uipath_deep_agent_graph( - model=object(), # type: ignore[arg-type] - output_schema=Output, - hydration_policy="always", - ) - - spec = get_uipath_deep_agent_runtime_spec(graph) - assert spec is not None - assert spec.hydration_policy == "always" - - -def test_conversational_mode_is_explicitly_not_implemented() -> None: - with pytest.raises(NotImplementedError, match="Conversational UiPath DeepAgents"): + ) as mock_create: create_uipath_deep_agent_graph( model=object(), # type: ignore[arg-type] output_schema=Output, - interaction_mode="conversation", ) + kwargs = mock_create.call_args.kwargs + assert isinstance(kwargs["backend"], UiPathWorkspaceBackendFactory) + assert isinstance(kwargs["response_format"], ToolStrategy) + def test_subagents_are_forwarded_to_advanced_agent_graph() -> None: subagent = { @@ -134,3 +128,20 @@ def test_user_prompt_and_build_user_message_are_mutually_exclusive() -> None: user_prompt="Create the brief.", build_user_message=lambda _args: "Create the brief.", ) + + +def test_deepagents_langsmith_metadata_is_detected() -> None: + graph = SimpleNamespace(config={"metadata": {"ls_integration": "deepagents"}}) + + assert is_deep_agent_graph(graph) + assert get_uipath_deep_agent_runtime_spec(graph) == UiPathDeepAgentRuntimeSpec() + + +def test_deepagents_langsmith_metadata_is_detected_on_subgraph_node() -> None: + subgraph = SimpleNamespace(config={"metadata": {"ls_integration": "deepagents"}}) + graph = SimpleNamespace( + nodes={"advanced_agent": SimpleNamespace(runnable=subgraph)} + ) + + assert is_deep_agent_graph(graph) + assert get_uipath_deep_agent_runtime_spec(graph) == UiPathDeepAgentRuntimeSpec() diff --git a/tests/deepagents/test_runtime_factory.py b/tests/deepagents/test_runtime_factory.py index 9aa8fcc0e..cc1c6048c 100644 --- a/tests/deepagents/test_runtime_factory.py +++ b/tests/deepagents/test_runtime_factory.py @@ -4,7 +4,7 @@ from langgraph.graph import END, START, StateGraph from uipath.runtime import HydrationPolicy, HydrationRuntime, UiPathRuntimeContext -from uipath_langchain.deepagents import ( +from uipath_langchain.deepagents.metadata import ( UiPathDeepAgentRuntimeSpec, set_uipath_deep_agent_runtime_spec, ) From ff0c99e7004d33212a9eccfbc156fe6d0b9d7575 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Wed, 15 Jul 2026 12:27:30 +0300 Subject: [PATCH 4/6] Use standard builder for coded DeepAgents --- samples/coded-deepagent/README.md | 18 +-- samples/coded-deepagent/graph.py | 12 +- src/uipath_langchain/agent/advanced/agent.py | 17 +-- src/uipath_langchain/deepagents/__init__.py | 16 +-- src/uipath_langchain/deepagents/agent.py | 123 ----------------- src/uipath_langchain/deepagents/metadata.py | 60 +-------- src/uipath_langchain/runtime/factory.py | 47 ++----- .../test_create_advanced_agent_graph.py | 21 ++- tests/agent/advanced/test_memory_injection.py | 67 ++------- tests/deepagents/test_metadata.py | 127 ------------------ tests/deepagents/test_runtime_factory.py | 28 ++-- 11 files changed, 83 insertions(+), 453 deletions(-) delete mode 100644 src/uipath_langchain/deepagents/agent.py diff --git a/samples/coded-deepagent/README.md b/samples/coded-deepagent/README.md index 604c64234..2c3a7a72e 100644 --- a/samples/coded-deepagent/README.md +++ b/samples/coded-deepagent/README.md @@ -1,14 +1,14 @@ # Coded DeepAgent -This sample demonstrates a task-mode coded agent built with the UiPath -DeepAgents contract. - -The graph uses `create_uipath_deep_agent_graph`, which tags the graph for the -UiPath LangGraph runtime. At runtime, UiPath creates a workspace, injects its -path into LangGraph config, and hydrates that workspace through job -attachments. Task-mode DeepAgents persist workspace changes when the run -successfully completes or suspends. The sample does not configure a custom -bucket backend. +This sample demonstrates a task-mode coded agent built with the standard +UiPath advanced-agent graph builder. + +The graph itself is a DeepAgent. At runtime, UiPath detects the DeepAgents +metadata already present on the graph, creates a workspace, injects its path +into LangGraph config, and hydrates that workspace through job attachments. +Task-mode DeepAgents persist workspace changes when the run successfully +completes or suspends. The sample does not configure a backend or runtime +policy. ## What It Shows diff --git a/samples/coded-deepagent/graph.py b/samples/coded-deepagent/graph.py index c73cc3595..197df2bb5 100644 --- a/samples/coded-deepagent/graph.py +++ b/samples/coded-deepagent/graph.py @@ -1,10 +1,10 @@ -"""Task-mode coded DeepAgent using the UiPath DeepAgents contract.""" +"""Task-mode coded DeepAgent using the standard advanced-agent graph builder.""" from langchain_core.tools import tool from pydantic import BaseModel, Field +from uipath_langchain.agent.advanced import create_advanced_agent_graph from uipath_langchain.chat import UiPathChat -from uipath_langchain.deepagents import create_uipath_deep_agent_graph class BriefInput(BaseModel): @@ -65,8 +65,8 @@ def build_system_prompt(args: dict) -> str: Tailor all planning to this audience: {args["audience"]}. -The UiPath runtime provides your DeepAgents filesystem through the tagged graph -contract. Write these workspace files before producing the final answer: +The UiPath runtime detects this DeepAgents graph and provides its filesystem. +Write these workspace files before producing the final answer: - /launch/brief.md with the final launch brief - /launch/risks.md with the risk review @@ -87,12 +87,12 @@ def build_user_prompt(args: dict) -> str: """ -graph = create_uipath_deep_agent_graph( +graph = create_advanced_agent_graph( model=MODEL, input_schema=BriefInput, output_schema=BriefOutput, system_prompt=build_system_prompt, - user_prompt=build_user_prompt, + build_user_message=build_user_prompt, tools=[score_launch_readiness], subagents=[RISK_REVIEWER], ) diff --git a/src/uipath_langchain/agent/advanced/agent.py b/src/uipath_langchain/agent/advanced/agent.py index daa666177..e7ad02202 100644 --- a/src/uipath_langchain/agent/advanced/agent.py +++ b/src/uipath_langchain/agent/advanced/agent.py @@ -7,7 +7,7 @@ from deepagents import create_deep_agent as _create_deep_agent from deepagents.backends import BackendProtocol from deepagents.backends.protocol import BackendFactory -from langchain.agents.structured_output import ResponseFormat +from langchain.agents.structured_output import ResponseFormat, ToolStrategy from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.runnables.config import RunnableConfig @@ -18,6 +18,7 @@ from uipath.core.chat import UiPathConversationMessageData from uipath_langchain.agent.react.job_attachments import get_job_attachment_paths +from uipath_langchain.deepagents.backend import create_workspace_backend_factory from .types import AdvancedAgentGraphState, ConversationalAdvancedAgentGraphState from .utils import ( @@ -58,8 +59,6 @@ def create_advanced_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], @@ -68,12 +67,14 @@ def create_advanced_agent_graph( ) -> StateGraph[Any, Any, Any, Any]: """Wrap the advanced agent in a parent graph that maps typed I/O to/from messages. - With a filesystem workspace backend, attachment-shaped inputs are downloaded - into the workspace and given a ``FilePath`` before the user message is built. - Filesystem workspaces also enable workspace memory: deepagents' - ``MemoryMiddleware`` reads ``/memory/MEMORY.md`` from the backend each turn. - Memory stays disabled for backends which carry no workspace. + The UiPath workspace backend and structured-output strategy are selected by + the integration. Attachment-shaped inputs are downloaded into the workspace + and given a ``FilePath`` before the user message is built. DeepAgents' + ``MemoryMiddleware`` reads ``/memory/MEMORY.md`` from that workspace each + turn. """ + backend = create_workspace_backend_factory() + response_format = ToolStrategy(output_schema) memory_sources = ( [MEMORY_INDEX_VIRTUAL_PATH] if is_workspace_filesystem_backend(backend) else [] ) diff --git a/src/uipath_langchain/deepagents/__init__.py b/src/uipath_langchain/deepagents/__init__.py index c50cfdf61..3bdb1138c 100644 --- a/src/uipath_langchain/deepagents/__init__.py +++ b/src/uipath_langchain/deepagents/__init__.py @@ -1,15 +1 @@ -"""UiPath DeepAgents integration helpers.""" - -from .agent import ( - UiPathDeepAgent, - UiPathDeepAgentPrompt, - create_uipath_deep_agent, - create_uipath_deep_agent_graph, -) - -__all__ = [ - "UiPathDeepAgent", - "UiPathDeepAgentPrompt", - "create_uipath_deep_agent", - "create_uipath_deep_agent_graph", -] +"""Internal DeepAgents runtime support for UiPath.""" diff --git a/src/uipath_langchain/deepagents/agent.py b/src/uipath_langchain/deepagents/agent.py deleted file mode 100644 index 2577334c8..000000000 --- a/src/uipath_langchain/deepagents/agent.py +++ /dev/null @@ -1,123 +0,0 @@ -"""UiPath DeepAgents authoring helpers.""" - -from __future__ import annotations - -from collections.abc import Callable, Sequence -from dataclasses import dataclass -from typing import Any - -from deepagents import CompiledSubAgent, SubAgent -from langchain.agents.structured_output import ToolStrategy -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool -from langgraph.graph.state import StateGraph -from pydantic import BaseModel - -from uipath_langchain.agent.advanced import create_advanced_agent_graph - -from .backend import create_workspace_backend_factory -from .metadata import ( - UiPathDeepAgentRuntimeSpec, - set_uipath_deep_agent_runtime_spec, -) - -UiPathDeepAgentPrompt = str | Callable[[dict[str, Any]], str] - - -@dataclass(frozen=True) -class UiPathDeepAgent: - """Author-facing result for a UiPath DeepAgents graph.""" - - graph: StateGraph[Any, Any, Any, Any] - - -def create_uipath_deep_agent( - *, - model: BaseChatModel, - input_schema: type[BaseModel] | None = None, - output_schema: type[BaseModel], - system_prompt: UiPathDeepAgentPrompt = "", - user_prompt: UiPathDeepAgentPrompt | None = None, - tools: Sequence[BaseTool] = (), - subagents: Sequence[SubAgent | CompiledSubAgent] = (), - build_user_message: Callable[[dict[str, Any]], str] | None = None, -) -> UiPathDeepAgent: - """Create a UiPath-runtime-aware DeepAgents graph.""" - graph = create_uipath_deep_agent_graph( - model=model, - input_schema=input_schema, - output_schema=output_schema, - system_prompt=system_prompt, - user_prompt=user_prompt, - tools=tools, - subagents=subagents, - build_user_message=build_user_message, - ) - spec = UiPathDeepAgentRuntimeSpec() - set_uipath_deep_agent_runtime_spec(graph, spec) - return UiPathDeepAgent(graph=graph) - - -def create_uipath_deep_agent_graph( - *, - model: BaseChatModel, - input_schema: type[BaseModel] | None = None, - output_schema: type[BaseModel], - system_prompt: UiPathDeepAgentPrompt = "", - user_prompt: UiPathDeepAgentPrompt | None = None, - tools: Sequence[BaseTool] = (), - subagents: Sequence[SubAgent | CompiledSubAgent] = (), - build_user_message: Callable[[dict[str, Any]], str] | None = None, -) -> StateGraph[Any, Any, Any, Any]: - """Create a tagged UiPath DeepAgents graph for coded agents.""" - backend = create_workspace_backend_factory() - response_format = ToolStrategy(output_schema) - if user_prompt is not None and build_user_message is not None: - raise ValueError("Use either user_prompt or build_user_message, not both.") - - static_system_prompt, build_system_message = _split_system_prompt(system_prompt) - build_user_message = ( - _coerce_prompt_renderer(user_prompt) - if user_prompt is not None - else build_user_message or _default_user_message - ) - - graph = create_advanced_agent_graph( - model=model, - tools=tools, - subagents=subagents, - system_prompt=static_system_prompt, - backend=backend, - response_format=response_format, - input_schema=input_schema, - output_schema=output_schema, - build_user_message=build_user_message, - build_system_message=build_system_message, - ) - set_uipath_deep_agent_runtime_spec( - graph, - UiPathDeepAgentRuntimeSpec(), - ) - return graph - - -def _split_system_prompt( - system_prompt: UiPathDeepAgentPrompt, -) -> tuple[str, Callable[[dict[str, Any]], str] | None]: - if callable(system_prompt): - return "", system_prompt - return system_prompt, None - - -def _coerce_prompt_renderer( - prompt: UiPathDeepAgentPrompt, -) -> Callable[[dict[str, Any]], str]: - if callable(prompt): - return prompt - return lambda _args: prompt - - -def _default_user_message(args: dict[str, Any]) -> str: - if not args: - return "" - return "\n".join(f"{key}: {value}" for key, value in args.items()) diff --git a/src/uipath_langchain/deepagents/metadata.py b/src/uipath_langchain/deepagents/metadata.py index c95030b7d..c9eb2adc0 100644 --- a/src/uipath_langchain/deepagents/metadata.py +++ b/src/uipath_langchain/deepagents/metadata.py @@ -1,67 +1,11 @@ -"""Runtime metadata for UiPath DeepAgents graphs.""" +"""DeepAgents graph detection for the UiPath runtime.""" from __future__ import annotations -from typing import Any, Literal +from typing import Any -from pydantic import BaseModel, ConfigDict - -_RUNTIME_SPEC_ATTR = "__uipath_deep_agent_runtime_spec__" _DEEPAGENTS_INTEGRATION_NAME = "deepagents" -UiPathDeepAgentHydrationPolicy = Literal[ - "suspend_only", - "suspend_or_success", - "always", -] - - -class UiPathDeepAgentRuntimeSpec(BaseModel): - """UiPath runtime contract for a DeepAgents-backed graph.""" - - model_config = ConfigDict(extra="forbid") - - kind: Literal["uipath.deepagents.v1"] = "uipath.deepagents.v1" - interaction_mode: Literal["task", "conversation"] = "task" - workspace_scope: Literal["runtime", "conversation"] = "runtime" - workspace_config_key: str = "uipath_workspace_path" - persistence: Literal["attachments"] = "attachments" - hydration_policy: UiPathDeepAgentHydrationPolicy = "suspend_or_success" - attachment_prefix: str = ".uipath-workspace" - - -def set_uipath_deep_agent_runtime_spec( - graph: Any, - spec: UiPathDeepAgentRuntimeSpec, -) -> Any: - """Attach UiPath DeepAgents runtime metadata to a graph-like object.""" - setattr(graph, _RUNTIME_SPEC_ATTR, spec) - builder = getattr(graph, "builder", None) - if builder is not None: - setattr(builder, _RUNTIME_SPEC_ATTR, spec) - return graph - - -def get_uipath_deep_agent_runtime_spec( - graph: Any, -) -> UiPathDeepAgentRuntimeSpec | None: - """Return UiPath DeepAgents runtime metadata from a graph-like object.""" - raw = getattr(graph, _RUNTIME_SPEC_ATTR, None) - if raw is None: - builder = getattr(graph, "builder", None) - raw = ( - getattr(builder, _RUNTIME_SPEC_ATTR, None) if builder is not None else None - ) - if raw is None: - return UiPathDeepAgentRuntimeSpec() if is_deep_agent_graph(graph) else None - if isinstance(raw, UiPathDeepAgentRuntimeSpec): - return raw - if isinstance(raw, dict): - return UiPathDeepAgentRuntimeSpec.model_validate(raw) - raise TypeError( - "UiPath DeepAgents runtime spec must be a UiPathDeepAgentRuntimeSpec or dict." - ) - def is_deep_agent_graph(graph: Any) -> bool: """Return whether a graph-like object carries DeepAgents LangSmith metadata.""" diff --git a/src/uipath_langchain/runtime/factory.py b/src/uipath_langchain/runtime/factory.py index afc84d20b..3195c77b9 100644 --- a/src/uipath_langchain/runtime/factory.py +++ b/src/uipath_langchain/runtime/factory.py @@ -32,11 +32,7 @@ from uipath.runtime.errors import UiPathErrorCategory from uipath_langchain._tracing import _instrument_traceable_attributes -from uipath_langchain.deepagents.metadata import ( - UiPathDeepAgentRuntimeSpec, - get_uipath_deep_agent_runtime_spec, - set_uipath_deep_agent_runtime_spec, -) +from uipath_langchain.deepagents.metadata import is_deep_agent_graph from uipath_langchain.governance import GovernanceCallbackHandler from uipath_langchain.runtime.config import LangGraphConfig from uipath_langchain.runtime.errors import LangGraphErrorCode, LangGraphRuntimeError @@ -46,6 +42,9 @@ _AGENT_TYPE_CODED = "uipath_coded" _AGENT_FRAMEWORK = "langchain" +_DEEPAGENT_WORKSPACE_CONFIG_KEY = "uipath_workspace_path" +_DEEPAGENT_ATTACHMENT_PREFIX = ".uipath-workspace" +_DEEPAGENT_HYDRATION_POLICY = HydrationPolicy.SUSPEND_OR_SUCCESS class UiPathLangGraphRuntimeFactory: @@ -234,9 +233,6 @@ async def _resolve_and_compile_graph( loaded_graph = await self._load_graph(entrypoint, **kwargs) compiled_graph = await self._compile_graph(loaded_graph, memory) - spec = get_uipath_deep_agent_runtime_spec(loaded_graph) - if spec is not None: - set_uipath_deep_agent_runtime_spec(compiled_graph, spec) self._graph_cache[entrypoint] = compiled_graph @@ -315,11 +311,11 @@ async def _create_runtime_instance( else None ) - spec = get_uipath_deep_agent_runtime_spec(compiled_graph) - workspace = self._create_deep_agent_workspace(runtime_id, spec) + is_deep_agent = is_deep_agent_graph(compiled_graph) + workspace = self._create_deep_agent_workspace(runtime_id, is_deep_agent) configurable = ( - {spec.workspace_config_key: str(workspace.path)} - if spec is not None and workspace is not None + {_DEEPAGENT_WORKSPACE_CONFIG_KEY: str(workspace.path)} + if workspace is not None else None ) @@ -339,11 +335,10 @@ async def _create_runtime_instance( runtime_id=runtime_id, ) - if spec is None or workspace is None: + if workspace is None: return resumable_runtime sdk = UiPath() - registry_runtime_id = self._deep_agent_registry_runtime_id(runtime_id, spec) return HydrationRuntime( delegate=resumable_runtime, workspace=workspace, @@ -353,40 +348,26 @@ async def _create_runtime_instance( jobs=getattr(sdk, "jobs", None), current_job_key=self.context.job_id, folder_key=self.context.folder_key, - attachment_prefix=spec.attachment_prefix, + attachment_prefix=_DEEPAGENT_ATTACHMENT_PREFIX, ), registry_store=WorkspaceRegistryStore( storage, - registry_runtime_id, + runtime_id, ), - policy=HydrationPolicy(spec.hydration_policy), + policy=_DEEPAGENT_HYDRATION_POLICY, ) def _create_deep_agent_workspace( self, runtime_id: str, - spec: UiPathDeepAgentRuntimeSpec | None, + is_deep_agent: bool, ) -> Workspace | None: - if spec is None: + if not is_deep_agent: return None - if spec.interaction_mode == "conversation": - raise NotImplementedError( - "Conversational UiPath DeepAgents require a chat-runtime adapter. " - "Task-mode DeepAgents are supported by this PoC." - ) base_dir = Path(self.context.runtime_dir or "__uipath") / "workspaces" return Workspace.create(base_dir / runtime_id, cleanup=True) - def _deep_agent_registry_runtime_id( - self, - runtime_id: str, - spec: UiPathDeepAgentRuntimeSpec, - ) -> str: - if spec.workspace_scope == "conversation" and self.context.conversation_id: - return self.context.conversation_id - return runtime_id - async def new_runtime( self, entrypoint: str, diff --git a/tests/agent/advanced/test_create_advanced_agent_graph.py b/tests/agent/advanced/test_create_advanced_agent_graph.py index 9b9dce9bc..8604275ab 100644 --- a/tests/agent/advanced/test_create_advanced_agent_graph.py +++ b/tests/agent/advanced/test_create_advanced_agent_graph.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from langchain.agents.structured_output import ToolStrategy from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel, ConfigDict, Field @@ -11,6 +12,8 @@ from uipath_langchain.agent.advanced.agent import create_advanced_agent_graph from uipath_langchain.agent.advanced.types import AdvancedAgentGraphState from uipath_langchain.agent.advanced.utils import create_state_with_input +from uipath_langchain.deepagents.backend import UiPathWorkspaceBackendFactory +from uipath_langchain.deepagents.metadata import is_deep_agent_graph class _Output(BaseModel): @@ -40,8 +43,6 @@ def _build(**overrides: Any) -> Any: model=_mock_model(), tools=[], system_prompt="sys", - backend=None, - response_format=None, input_schema=None, output_schema=_Output, build_user_message=lambda args: "hello", @@ -56,6 +57,10 @@ def test_wrapper_graph_has_io_nodes() -> None: assert {"transform_input", "advanced_agent", "transform_output"} <= set(graph.nodes) +def test_wrapper_is_detected_from_nested_deepagents_metadata() -> None: + assert is_deep_agent_graph(_build()) + + def test_subagents_are_passed_to_inner_advanced_agent() -> None: """The typed I/O wrapper preserves DeepAgents sub-agent delegation.""" subagent = { @@ -73,6 +78,18 @@ def test_subagents_are_passed_to_inner_advanced_agent() -> None: assert mock_create.call_args.kwargs["subagents"] == [subagent] +def test_wrapper_owns_workspace_backend_and_response_format() -> None: + with patch( + "uipath_langchain.agent.advanced.agent.create_advanced_agent", + return_value=MagicMock(), + ) as mock_create: + _build() + + kwargs = mock_create.call_args.kwargs + assert isinstance(kwargs["backend"], UiPathWorkspaceBackendFactory) + assert isinstance(kwargs["response_format"], ToolStrategy) + + @pytest.mark.asyncio async def test_transform_input_without_schema_builds_single_user_message() -> None: """With no input schema, the built message comes straight from build_user_message.""" diff --git a/tests/agent/advanced/test_memory_injection.py b/tests/agent/advanced/test_memory_injection.py index cb31b2103..4c0296469 100644 --- a/tests/agent/advanced/test_memory_injection.py +++ b/tests/agent/advanced/test_memory_injection.py @@ -1,18 +1,16 @@ -"""Wiring test: the wrapper enables deepagents memory based on the backend. +"""Wiring test: the wrapper enables DeepAgents workspace memory. Phase 1 workspace memory is delegated to deepagents' ``MemoryMiddleware``, which ``create_deep_agent`` builds when a ``memory=`` source list is supplied. The wrapper's responsibility is therefore to pass ``memory=["/memory/MEMORY.md"]`` -through to ``_create_deep_agent`` when (and only when) the backend is a -``FilesystemBackend`` that carries a durable workspace. The actual loading and -system-prompt injection are deepagents' concern and covered by their own tests. +through to ``_create_deep_agent``. The actual loading and system-prompt +injection are DeepAgents' concern and covered by their own tests. """ from typing import Any from unittest.mock import MagicMock, patch import pytest -from deepagents.backends.filesystem import FilesystemBackend from langchain_core.language_models import BaseChatModel from pydantic import BaseModel @@ -40,9 +38,7 @@ def _build_user_message(args: dict[str, Any]) -> str: return args.get("task", "") -def _memory_kwarg( - backend: Any, -) -> Any: +def _memory_kwarg() -> Any: """Build the wrapper graph and return the ``memory`` kwarg handed to deepagents.""" with patch( "uipath_langchain.agent.advanced.agent._create_deep_agent", @@ -52,8 +48,6 @@ def _memory_kwarg( model=MagicMock(spec=BaseChatModel), tools=[], system_prompt="", - backend=backend, - response_format=None, input_schema=_Input, output_schema=_Output, build_user_message=_build_user_message, @@ -62,26 +56,10 @@ def _memory_kwarg( class TestWorkspaceMemoryWiring: - """The wrapper turns deepagents memory on for filesystem-backed workspaces.""" + """The wrapper turns DeepAgents memory on for the UiPath workspace.""" - def test_enables_memory_for_filesystem_backend(self, tmp_path: Any) -> None: - backend = FilesystemBackend(root_dir=tmp_path, virtual_mode=True) - assert _memory_kwarg(backend) == [MEMORY_INDEX_VIRTUAL_PATH] - - def test_enables_memory_for_marked_runtime_workspace_factory( - self, tmp_path: Any - ) -> None: - def backend_factory(runtime: Any) -> FilesystemBackend: - return FilesystemBackend(root_dir=tmp_path, virtual_mode=True) - - backend_factory.is_uipath_workspace_filesystem_backend = True # type: ignore[attr-defined] - - assert _memory_kwarg(backend_factory) == [MEMORY_INDEX_VIRTUAL_PATH] - - def test_disables_memory_for_non_filesystem_backend(self) -> None: - # The default in-state backend (None) carries no durable workspace, so - # passing memory=None leaves MemoryMiddleware out of the stack entirely. - assert _memory_kwarg(None) is None + def test_enables_memory_for_runtime_workspace(self) -> None: + assert _memory_kwarg() == [MEMORY_INDEX_VIRTUAL_PATH] @pytest.mark.asyncio @@ -93,47 +71,28 @@ async def test_transform_input_emits_only_user_message(self, tmp_path: Any) -> N # message; memory injection is now MemoryMiddleware's job inside the inner # agent, not the wrapper's. from langchain_core.messages import HumanMessage - from langgraph.graph import END, START, StateGraph - - from uipath_langchain.agent.advanced.types import ( - AdvancedAgentGraphState, - ) memory_dir = tmp_path / "memory" memory_dir.mkdir() (memory_dir / "MEMORY.md").write_text("- entry: x", encoding="utf-8") - captured: list[list[Any]] = [] - - def _make_inner() -> Any: - def _capture(state: AdvancedAgentGraphState) -> dict[str, Any]: - captured.append(list(state.messages)) - return {"structured_response": {"result": "ok"}} - - inner = StateGraph(AdvancedAgentGraphState) - inner.add_node("capture", _capture) - inner.add_edge(START, "capture") - inner.add_edge("capture", END) - return inner.compile() - with patch( "uipath_langchain.agent.advanced.agent._create_deep_agent", - return_value=_make_inner(), + return_value=MagicMock(), ): wrapper = create_advanced_agent_graph( model=MagicMock(spec=BaseChatModel), tools=[], system_prompt="", - backend=FilesystemBackend(root_dir=tmp_path, virtual_mode=True), - response_format=None, input_schema=_Input, output_schema=_Output, build_user_message=_build_user_message, - ).compile() - await wrapper.ainvoke({"task": "do something"}) + ) + out = await wrapper.nodes["transform_input"].runnable.ainvoke( + _Input(task="do something") + ) - assert len(captured) == 1 - messages = captured[0] + messages = out["messages"] assert len(messages) == 1 assert isinstance(messages[0], HumanMessage) assert messages[0].content == "do something" diff --git a/tests/deepagents/test_metadata.py b/tests/deepagents/test_metadata.py index 59ec02581..90244eb52 100644 --- a/tests/deepagents/test_metadata.py +++ b/tests/deepagents/test_metadata.py @@ -1,140 +1,14 @@ from types import SimpleNamespace -from unittest.mock import patch -import pytest -from langchain.agents.structured_output import ToolStrategy -from langgraph.graph import END, START, StateGraph -from pydantic import BaseModel - -from uipath_langchain.deepagents.agent import create_uipath_deep_agent_graph -from uipath_langchain.deepagents.backend import UiPathWorkspaceBackendFactory from uipath_langchain.deepagents.metadata import ( - UiPathDeepAgentRuntimeSpec, - get_uipath_deep_agent_runtime_spec, is_deep_agent_graph, - set_uipath_deep_agent_runtime_spec, ) -class State(BaseModel): - value: str = "" - - -class Output(BaseModel): - result: str - - -def _graph() -> StateGraph: - async def node(state: State) -> State: - return state - - builder = StateGraph(State) - builder.add_node("node", node) - builder.add_edge(START, "node") - builder.add_edge("node", END) - return builder - - -def test_runtime_spec_round_trips_on_builder_and_compiled_graph() -> None: - graph = _graph() - spec = UiPathDeepAgentRuntimeSpec(workspace_config_key="workspace") - - set_uipath_deep_agent_runtime_spec(graph, spec) - compiled = graph.compile() - - assert get_uipath_deep_agent_runtime_spec(graph) == spec - assert get_uipath_deep_agent_runtime_spec(compiled) == spec - - -def test_runtime_spec_defaults_to_success_persistence() -> None: - spec = UiPathDeepAgentRuntimeSpec() - - assert spec.hydration_policy == "suspend_or_success" - - -def test_uipath_deep_agent_graph_uses_internal_runtime_defaults() -> None: - with patch( - "uipath_langchain.deepagents.agent.create_advanced_agent_graph", - return_value=_graph(), - ) as mock_create: - create_uipath_deep_agent_graph( - model=object(), # type: ignore[arg-type] - output_schema=Output, - ) - - kwargs = mock_create.call_args.kwargs - assert isinstance(kwargs["backend"], UiPathWorkspaceBackendFactory) - assert isinstance(kwargs["response_format"], ToolStrategy) - - -def test_subagents_are_forwarded_to_advanced_agent_graph() -> None: - subagent = { - "name": "researcher", - "description": "Researches one part of the task", - "system_prompt": "Research carefully.", - } - - with patch( - "uipath_langchain.deepagents.agent.create_advanced_agent_graph", - return_value=_graph(), - ) as mock_create: - create_uipath_deep_agent_graph( - model=object(), # type: ignore[arg-type] - output_schema=Output, - subagents=[subagent], - ) - - assert mock_create.call_args.kwargs["subagents"] == [subagent] - - -def test_dynamic_system_prompt_is_forwarded_as_runtime_renderer() -> None: - def build_system_prompt(args: dict[str, str]) -> str: - return f"System for {args['topic']}" - - with patch( - "uipath_langchain.deepagents.agent.create_advanced_agent_graph", - return_value=_graph(), - ) as mock_create: - create_uipath_deep_agent_graph( - model=object(), # type: ignore[arg-type] - output_schema=Output, - system_prompt=build_system_prompt, - ) - - assert mock_create.call_args.kwargs["system_prompt"] == "" - assert mock_create.call_args.kwargs["build_system_message"] is build_system_prompt - - -def test_static_user_prompt_is_forwarded_as_user_renderer() -> None: - with patch( - "uipath_langchain.deepagents.agent.create_advanced_agent_graph", - return_value=_graph(), - ) as mock_create: - create_uipath_deep_agent_graph( - model=object(), # type: ignore[arg-type] - output_schema=Output, - user_prompt="Create the brief.", - ) - - build_user_message = mock_create.call_args.kwargs["build_user_message"] - assert build_user_message({}) == "Create the brief." - - -def test_user_prompt_and_build_user_message_are_mutually_exclusive() -> None: - with pytest.raises(ValueError, match="either user_prompt or build_user_message"): - create_uipath_deep_agent_graph( - model=object(), # type: ignore[arg-type] - output_schema=Output, - user_prompt="Create the brief.", - build_user_message=lambda _args: "Create the brief.", - ) - - def test_deepagents_langsmith_metadata_is_detected() -> None: graph = SimpleNamespace(config={"metadata": {"ls_integration": "deepagents"}}) assert is_deep_agent_graph(graph) - assert get_uipath_deep_agent_runtime_spec(graph) == UiPathDeepAgentRuntimeSpec() def test_deepagents_langsmith_metadata_is_detected_on_subgraph_node() -> None: @@ -144,4 +18,3 @@ def test_deepagents_langsmith_metadata_is_detected_on_subgraph_node() -> None: ) assert is_deep_agent_graph(graph) - assert get_uipath_deep_agent_runtime_spec(graph) == UiPathDeepAgentRuntimeSpec() diff --git a/tests/deepagents/test_runtime_factory.py b/tests/deepagents/test_runtime_factory.py index cc1c6048c..e7c89802f 100644 --- a/tests/deepagents/test_runtime_factory.py +++ b/tests/deepagents/test_runtime_factory.py @@ -1,13 +1,9 @@ from typing import Any, TypedDict -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from langgraph.graph import END, START, StateGraph from uipath.runtime import HydrationPolicy, HydrationRuntime, UiPathRuntimeContext -from uipath_langchain.deepagents.metadata import ( - UiPathDeepAgentRuntimeSpec, - set_uipath_deep_agent_runtime_spec, -) from uipath_langchain.runtime.factory import UiPathLangGraphRuntimeFactory @@ -23,7 +19,7 @@ def _build_graph() -> StateGraph[Any, Any, Any]: return graph -async def test_tagged_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> None: +async def test_detected_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> None: context = UiPathRuntimeContext( runtime_dir=str(tmp_path), state_file="state.db", @@ -32,15 +28,13 @@ async def test_tagged_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> No ) factory = UiPathLangGraphRuntimeFactory(context) compiled = _build_graph().compile() - spec = UiPathDeepAgentRuntimeSpec( - workspace_config_key="workspace", - hydration_policy="always", - attachment_prefix=".deepagent-workspace", - ) - set_uipath_deep_agent_runtime_spec(compiled, spec) + compiled.config = {"metadata": {"ls_integration": "deepagents"}} sdk = MagicMock() - with patch("uipath_langchain.runtime.factory.UiPath", return_value=sdk): + with ( + patch("uipath_langchain.runtime.factory.UiPath", return_value=sdk), + patch.object(factory, "_get_memory", AsyncMock(return_value=MagicMock())), + ): runtime = await factory._create_runtime_instance( compiled_graph=compiled, runtime_id="runtime-1", @@ -48,13 +42,13 @@ async def test_tagged_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> No ) assert isinstance(runtime, HydrationRuntime) - assert runtime.policy == HydrationPolicy.ALWAYS + assert runtime.policy == HydrationPolicy.SUSPEND_OR_SUCCESS assert runtime.workspace.path.parent == tmp_path / "workspaces" resumable_runtime = runtime.delegate langgraph_runtime = resumable_runtime.delegate assert langgraph_runtime.configurable == { - "workspace": str(runtime.workspace.path), + "uipath_workspace_path": str(runtime.workspace.path), } assert runtime.hydrator.workspace_path == runtime.workspace.path @@ -62,7 +56,5 @@ async def test_tagged_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> No assert runtime.hydrator.jobs is sdk.jobs assert runtime.hydrator.current_job_key == "job-1" assert runtime.hydrator.folder_key == "folder-1" - assert runtime.hydrator.attachment_prefix == ".deepagent-workspace" + assert runtime.hydrator.attachment_prefix == ".uipath-workspace" assert runtime.registry_store.runtime_id == "runtime-1" - - await factory.dispose() From 0f62f9b6879aca9c15f31e51b3e15c003b8bbe19 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Wed, 15 Jul 2026 12:48:21 +0300 Subject: [PATCH 5/6] Detect standard DeepAgents entrypoints --- samples/coded-deepagent/README.md | 22 ++++---- samples/coded-deepagent/graph.py | 52 ++++--------------- samples/coded-deepagent/input.json | 12 ++--- src/uipath_langchain/agent/advanced/agent.py | 17 +++--- src/uipath_langchain/runtime/factory.py | 9 +++- .../test_create_advanced_agent_graph.py | 16 +----- tests/agent/advanced/test_memory_injection.py | 34 +++++++++--- tests/deepagents/test_metadata.py | 13 +++++ tests/deepagents/test_runtime_factory.py | 17 ++++++ 9 files changed, 99 insertions(+), 93 deletions(-) diff --git a/samples/coded-deepagent/README.md b/samples/coded-deepagent/README.md index 2c3a7a72e..9e51a994c 100644 --- a/samples/coded-deepagent/README.md +++ b/samples/coded-deepagent/README.md @@ -1,22 +1,18 @@ # Coded DeepAgent -This sample demonstrates a task-mode coded agent built with the standard -UiPath advanced-agent graph builder. +This sample demonstrates a task-mode coded agent built directly with the +standard `deepagents.create_deep_agent` API. -The graph itself is a DeepAgent. At runtime, UiPath detects the DeepAgents -metadata already present on the graph, creates a workspace, injects its path -into LangGraph config, and hydrates that workspace through job attachments. -Task-mode DeepAgents persist workspace changes when the run successfully -completes or suspends. The sample does not configure a backend or runtime -policy. +The sample does not use a UiPath-specific graph factory or attach UiPath +metadata. At runtime, UiPath detects the `ls_integration: deepagents` metadata +already present on the graph and applies the UiPath-owned runtime policy. ## What It Shows -- Typed coded-agent input and output with Pydantic models. -- System and user prompts rendered from typed input. +- Standard DeepAgents message input and structured output. - A standard LangChain tool used by the main DeepAgent. - A DeepAgents subagent used for risk review. -- Runtime-provided workspace persistence through the UiPath contract. +- Automatic runtime detection through native DeepAgents metadata. ## Files @@ -39,5 +35,5 @@ uv sync uipath run agent "$(cat input.json)" ``` -The agent writes `/launch/brief.md` and `/launch/risks.md` in the DeepAgents -workspace and returns those paths in `workspace_files`. +The agent uses the filesystem tools supplied by the DeepAgents harness for its +working files and returns a structured launch brief. diff --git a/samples/coded-deepagent/graph.py b/samples/coded-deepagent/graph.py index 197df2bb5..ca23f1852 100644 --- a/samples/coded-deepagent/graph.py +++ b/samples/coded-deepagent/graph.py @@ -1,31 +1,18 @@ -"""Task-mode coded DeepAgent using the standard advanced-agent graph builder.""" +"""Task-mode coded DeepAgent using the standard DeepAgents API.""" +from deepagents import create_deep_agent from langchain_core.tools import tool -from pydantic import BaseModel, Field +from pydantic import BaseModel -from uipath_langchain.agent.advanced import create_advanced_agent_graph from uipath_langchain.chat import UiPathChat -class BriefInput(BaseModel): - """Input for the product launch brief.""" - - product_name: str = Field(description="Name of the product or feature.") - audience: str = Field(description="Primary customer or user audience.") - objective: str = Field(description="Main launch objective.") - constraints: list[str] = Field( - default_factory=list, - description="Important limits, requirements, or risks to account for.", - ) - - class BriefOutput(BaseModel): """Structured launch brief returned by the agent.""" executive_summary: str launch_plan: list[str] risk_review: list[str] - workspace_files: list[str] @tool @@ -56,43 +43,24 @@ def score_launch_readiness(audience: str, constraints: list[str]) -> str: } -def build_system_prompt(args: dict) -> str: - return f"""You are a product launch planning agent for {args["product_name"]}. +SYSTEM_PROMPT = """You are a product launch planning agent. Use the planning tools provided by DeepAgents. Use score_launch_readiness to get a deterministic readiness signal. Delegate a plan review to risk_reviewer before finalizing the answer. -Tailor all planning to this audience: {args["audience"]}. - -The UiPath runtime detects this DeepAgents graph and provides its filesystem. -Write these workspace files before producing the final answer: +Use the DeepAgents filesystem to write these working files before producing the +final answer: - /launch/brief.md with the final launch brief - /launch/risks.md with the risk review -Return structured output matching the schema. Include the workspace file paths -you wrote in workspace_files.""" - - -def build_user_prompt(args: dict) -> str: - constraints = args.get("constraints") or [] - constraint_lines = "\n".join(f"- {item}" for item in constraints) or "- None" - return f"""Create a launch brief. - -Product: {args["product_name"]} -Audience: {args["audience"]} -Objective: {args["objective"]} -Constraints: -{constraint_lines} -""" +Return structured output matching the schema.""" -graph = create_advanced_agent_graph( +graph = create_deep_agent( model=MODEL, - input_schema=BriefInput, - output_schema=BriefOutput, - system_prompt=build_system_prompt, - build_user_message=build_user_prompt, + system_prompt=SYSTEM_PROMPT, + response_format=BriefOutput, tools=[score_launch_readiness], subagents=[RISK_REVIEWER], ) diff --git a/samples/coded-deepagent/input.json b/samples/coded-deepagent/input.json index 5543a9daf..4fcfc4a95 100644 --- a/samples/coded-deepagent/input.json +++ b/samples/coded-deepagent/input.json @@ -1,10 +1,8 @@ { - "product_name": "Invoice Exception Copilot", - "audience": "accounts payable operations managers", - "objective": "prepare a pilot launch plan for three enterprise customers", - "constraints": [ - "security review must complete before external rollout", - "deadline is six weeks away", - "support team needs enablement material" + "messages": [ + { + "type": "human", + "content": "Create a pilot launch plan for Invoice Exception Copilot for accounts payable operations managers at three enterprise customers. The security review must complete before external rollout, the deadline is six weeks away, and the support team needs enablement material." + } ] } diff --git a/src/uipath_langchain/agent/advanced/agent.py b/src/uipath_langchain/agent/advanced/agent.py index e7ad02202..cfcfc2e65 100644 --- a/src/uipath_langchain/agent/advanced/agent.py +++ b/src/uipath_langchain/agent/advanced/agent.py @@ -7,7 +7,7 @@ from deepagents import create_deep_agent as _create_deep_agent from deepagents.backends import BackendProtocol from deepagents.backends.protocol import BackendFactory -from langchain.agents.structured_output import ResponseFormat, ToolStrategy +from langchain.agents.structured_output import ResponseFormat from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.runnables.config import RunnableConfig @@ -18,7 +18,6 @@ from uipath.core.chat import UiPathConversationMessageData from uipath_langchain.agent.react.job_attachments import get_job_attachment_paths -from uipath_langchain.deepagents.backend import create_workspace_backend_factory from .types import AdvancedAgentGraphState, ConversationalAdvancedAgentGraphState from .utils import ( @@ -59,6 +58,8 @@ def create_advanced_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], @@ -67,14 +68,12 @@ def create_advanced_agent_graph( ) -> StateGraph[Any, Any, Any, Any]: """Wrap the advanced agent in a parent graph that maps typed I/O to/from messages. - The UiPath workspace backend and structured-output strategy are selected by - the integration. Attachment-shaped inputs are downloaded into the workspace - and given a ``FilePath`` before the user message is built. DeepAgents' - ``MemoryMiddleware`` reads ``/memory/MEMORY.md`` from that workspace each - turn. + With a filesystem workspace backend, attachment-shaped inputs are downloaded + into the workspace and given a ``FilePath`` before the user message is built. + Filesystem workspaces also enable workspace memory: DeepAgents' + ``MemoryMiddleware`` reads ``/memory/MEMORY.md`` from the backend each turn. + Memory stays disabled for backends which carry no workspace. """ - backend = create_workspace_backend_factory() - response_format = ToolStrategy(output_schema) memory_sources = ( [MEMORY_INDEX_VIRTUAL_PATH] if is_workspace_filesystem_backend(backend) else [] ) diff --git a/src/uipath_langchain/runtime/factory.py b/src/uipath_langchain/runtime/factory.py index 3195c77b9..83938586d 100644 --- a/src/uipath_langchain/runtime/factory.py +++ b/src/uipath_langchain/runtime/factory.py @@ -67,6 +67,7 @@ def __init__( self._memory_lock = asyncio.Lock() self._graph_cache: dict[str, CompiledStateGraph[Any, Any, Any, Any]] = {} + self._deep_agent_entrypoints: set[str] = set() self._graph_loaders: dict[str, LangGraphLoader] = {} self._graph_lock = asyncio.Lock() @@ -231,6 +232,8 @@ async def _resolve_and_compile_graph( return self._graph_cache[entrypoint] loaded_graph = await self._load_graph(entrypoint, **kwargs) + if is_deep_agent_graph(loaded_graph): + self._deep_agent_entrypoints.add(entrypoint) compiled_graph = await self._compile_graph(loaded_graph, memory) @@ -311,7 +314,10 @@ async def _create_runtime_instance( else None ) - is_deep_agent = is_deep_agent_graph(compiled_graph) + is_deep_agent = ( + entrypoint in self._deep_agent_entrypoints + or is_deep_agent_graph(compiled_graph) + ) workspace = self._create_deep_agent_workspace(runtime_id, is_deep_agent) configurable = ( {_DEEPAGENT_WORKSPACE_CONFIG_KEY: str(workspace.path)} @@ -409,6 +415,7 @@ async def dispose(self) -> None: self._graph_loaders.clear() self._graph_cache.clear() + self._deep_agent_entrypoints.clear() if self._memory_cm is not None: await self._memory_cm.__aexit__(None, None, None) diff --git a/tests/agent/advanced/test_create_advanced_agent_graph.py b/tests/agent/advanced/test_create_advanced_agent_graph.py index 8604275ab..2a69cdc2f 100644 --- a/tests/agent/advanced/test_create_advanced_agent_graph.py +++ b/tests/agent/advanced/test_create_advanced_agent_graph.py @@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from langchain.agents.structured_output import ToolStrategy from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel, ConfigDict, Field @@ -12,7 +11,6 @@ from uipath_langchain.agent.advanced.agent import create_advanced_agent_graph from uipath_langchain.agent.advanced.types import AdvancedAgentGraphState from uipath_langchain.agent.advanced.utils import create_state_with_input -from uipath_langchain.deepagents.backend import UiPathWorkspaceBackendFactory from uipath_langchain.deepagents.metadata import is_deep_agent_graph @@ -43,6 +41,8 @@ def _build(**overrides: Any) -> Any: model=_mock_model(), tools=[], system_prompt="sys", + backend=None, + response_format=None, input_schema=None, output_schema=_Output, build_user_message=lambda args: "hello", @@ -78,18 +78,6 @@ def test_subagents_are_passed_to_inner_advanced_agent() -> None: assert mock_create.call_args.kwargs["subagents"] == [subagent] -def test_wrapper_owns_workspace_backend_and_response_format() -> None: - with patch( - "uipath_langchain.agent.advanced.agent.create_advanced_agent", - return_value=MagicMock(), - ) as mock_create: - _build() - - kwargs = mock_create.call_args.kwargs - assert isinstance(kwargs["backend"], UiPathWorkspaceBackendFactory) - assert isinstance(kwargs["response_format"], ToolStrategy) - - @pytest.mark.asyncio async def test_transform_input_without_schema_builds_single_user_message() -> None: """With no input schema, the built message comes straight from build_user_message.""" diff --git a/tests/agent/advanced/test_memory_injection.py b/tests/agent/advanced/test_memory_injection.py index 4c0296469..e90051c42 100644 --- a/tests/agent/advanced/test_memory_injection.py +++ b/tests/agent/advanced/test_memory_injection.py @@ -1,16 +1,18 @@ -"""Wiring test: the wrapper enables DeepAgents workspace memory. +"""Wiring test: the wrapper enables DeepAgents memory based on the backend. Phase 1 workspace memory is delegated to deepagents' ``MemoryMiddleware``, which ``create_deep_agent`` builds when a ``memory=`` source list is supplied. The wrapper's responsibility is therefore to pass ``memory=["/memory/MEMORY.md"]`` -through to ``_create_deep_agent``. The actual loading and system-prompt -injection are DeepAgents' concern and covered by their own tests. +through to ``_create_deep_agent`` when (and only when) the backend is a +``FilesystemBackend`` that carries a durable workspace. The actual loading and +system-prompt injection are DeepAgents' concern and covered by their own tests. """ from typing import Any from unittest.mock import MagicMock, patch import pytest +from deepagents.backends.filesystem import FilesystemBackend from langchain_core.language_models import BaseChatModel from pydantic import BaseModel @@ -38,7 +40,7 @@ def _build_user_message(args: dict[str, Any]) -> str: return args.get("task", "") -def _memory_kwarg() -> Any: +def _memory_kwarg(backend: Any) -> Any: """Build the wrapper graph and return the ``memory`` kwarg handed to deepagents.""" with patch( "uipath_langchain.agent.advanced.agent._create_deep_agent", @@ -48,6 +50,8 @@ def _memory_kwarg() -> Any: model=MagicMock(spec=BaseChatModel), tools=[], system_prompt="", + backend=backend, + response_format=None, input_schema=_Input, output_schema=_Output, build_user_message=_build_user_message, @@ -56,10 +60,24 @@ def _memory_kwarg() -> Any: class TestWorkspaceMemoryWiring: - """The wrapper turns DeepAgents memory on for the UiPath workspace.""" + """The wrapper turns DeepAgents memory on for filesystem-backed workspaces.""" - def test_enables_memory_for_runtime_workspace(self) -> None: - assert _memory_kwarg() == [MEMORY_INDEX_VIRTUAL_PATH] + def test_enables_memory_for_filesystem_backend(self, tmp_path: Any) -> None: + backend = FilesystemBackend(root_dir=tmp_path, virtual_mode=True) + assert _memory_kwarg(backend) == [MEMORY_INDEX_VIRTUAL_PATH] + + def test_enables_memory_for_marked_runtime_workspace_factory( + self, tmp_path: Any + ) -> None: + def backend_factory(runtime: Any) -> FilesystemBackend: + return FilesystemBackend(root_dir=tmp_path, virtual_mode=True) + + backend_factory.is_uipath_workspace_filesystem_backend = True # type: ignore[attr-defined] + + assert _memory_kwarg(backend_factory) == [MEMORY_INDEX_VIRTUAL_PATH] + + def test_disables_memory_for_non_filesystem_backend(self) -> None: + assert _memory_kwarg(None) is None @pytest.mark.asyncio @@ -84,6 +102,8 @@ async def test_transform_input_emits_only_user_message(self, tmp_path: Any) -> N model=MagicMock(spec=BaseChatModel), tools=[], system_prompt="", + backend=FilesystemBackend(root_dir=tmp_path, virtual_mode=True), + response_format=None, input_schema=_Input, output_schema=_Output, build_user_message=_build_user_message, diff --git a/tests/deepagents/test_metadata.py b/tests/deepagents/test_metadata.py index 90244eb52..cbbfa1b42 100644 --- a/tests/deepagents/test_metadata.py +++ b/tests/deepagents/test_metadata.py @@ -1,10 +1,23 @@ from types import SimpleNamespace +from unittest.mock import MagicMock + +from deepagents import create_deep_agent +from langchain_core.language_models import BaseChatModel from uipath_langchain.deepagents.metadata import ( is_deep_agent_graph, ) +def test_standard_deepagent_is_detected_from_native_metadata() -> None: + model = MagicMock(spec=BaseChatModel) + model.profile = None + + graph = create_deep_agent(model=model) + + assert is_deep_agent_graph(graph) + + def test_deepagents_langsmith_metadata_is_detected() -> None: graph = SimpleNamespace(config={"metadata": {"ls_integration": "deepagents"}}) diff --git a/tests/deepagents/test_runtime_factory.py b/tests/deepagents/test_runtime_factory.py index e7c89802f..c748325d4 100644 --- a/tests/deepagents/test_runtime_factory.py +++ b/tests/deepagents/test_runtime_factory.py @@ -1,3 +1,4 @@ +from types import SimpleNamespace from typing import Any, TypedDict from unittest.mock import AsyncMock, MagicMock, patch @@ -19,6 +20,22 @@ def _build_graph() -> StateGraph[Any, Any, Any]: return graph +async def test_native_detection_survives_runtime_recompilation(tmp_path) -> None: + context = UiPathRuntimeContext(runtime_dir=str(tmp_path), state_file="state.db") + factory = UiPathLangGraphRuntimeFactory(context) + loaded = SimpleNamespace(config={"metadata": {"ls_integration": "deepagents"}}) + recompiled = _build_graph().compile() + + with ( + patch.object(factory, "_load_graph", AsyncMock(return_value=loaded)), + patch.object(factory, "_compile_graph", AsyncMock(return_value=recompiled)), + ): + result = await factory._resolve_and_compile_graph("agent", MagicMock()) + + assert result is recompiled + assert "agent" in factory._deep_agent_entrypoints + + async def test_detected_deep_agent_runtime_uses_hydrated_workspace(tmp_path) -> None: context = UiPathRuntimeContext( runtime_dir=str(tmp_path), From bd50e54ad50e093997dc2b9d83c81eacbe4bf6d8 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Wed, 15 Jul 2026 14:19:20 +0300 Subject: [PATCH 6/6] Resolve DeepAgents backend from active config --- src/uipath_langchain/deepagents/backend.py | 8 +++++++- tests/deepagents/test_backend.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/uipath_langchain/deepagents/backend.py b/src/uipath_langchain/deepagents/backend.py index ce090e6db..77b6c824c 100644 --- a/src/uipath_langchain/deepagents/backend.py +++ b/src/uipath_langchain/deepagents/backend.py @@ -8,6 +8,7 @@ from deepagents.backends.filesystem import FilesystemBackend from deepagents.backends.protocol import BackendFactory from langchain.tools import ToolRuntime +from langgraph.config import get_config WORKSPACE_FILESYSTEM_BACKEND_ATTR = "is_uipath_workspace_filesystem_backend" @@ -21,7 +22,12 @@ class UiPathWorkspaceBackendFactory: is_uipath_workspace_filesystem_backend: bool = field(default=True, init=False) def __call__(self, runtime: ToolRuntime) -> FilesystemBackend: - config = getattr(runtime, "config", None) or {} + config = getattr(runtime, "config", None) + if config is None: + try: + config = get_config() + except RuntimeError: + config = {} configurable = ( config.get("configurable", {}) if isinstance(config, dict) else {} ) diff --git a/tests/deepagents/test_backend.py b/tests/deepagents/test_backend.py index 455249b72..30c63d6e5 100644 --- a/tests/deepagents/test_backend.py +++ b/tests/deepagents/test_backend.py @@ -1,4 +1,6 @@ +from types import SimpleNamespace from typing import Any +from unittest.mock import patch import pytest from deepagents.backends.filesystem import FilesystemBackend @@ -38,3 +40,17 @@ def test_workspace_backend_factory_raises_when_config_missing() -> None: with pytest.raises(RuntimeError, match="workspace path missing"): factory(_tool_runtime({"configurable": {}})) + + +def test_workspace_backend_factory_uses_active_config_for_current_runtime( + tmp_path, +) -> None: + factory = create_workspace_backend_factory(workspace_config_key="workspace") + + with patch( + "uipath_langchain.deepagents.backend.get_config", + return_value={"configurable": {"workspace": str(tmp_path)}}, + ): + backend = factory(SimpleNamespace()) # type: ignore[arg-type] + + assert backend.cwd == tmp_path.resolve()