Skip to content

Commit 189d2dd

Browse files
Support input-rendered coded DeepAgent prompts
1 parent 71b218c commit 189d2dd

7 files changed

Lines changed: 133 additions & 11 deletions

File tree

samples/coded-deepagent/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ bucket backend.
1313
## What It Shows
1414

1515
- Typed coded-agent input and output with Pydantic models.
16+
- System and user prompts rendered from typed input.
1617
- A standard LangChain tool used by the main DeepAgent.
1718
- A DeepAgents subagent used for risk review.
1819
- Runtime-provided workspace persistence through the UiPath contract.

samples/coded-deepagent/graph.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,16 @@ def score_launch_readiness(audience: str, constraints: list[str]) -> str:
5555
"model": MODEL,
5656
}
5757

58-
SYSTEM_PROMPT = """You are a product launch planning agent.
58+
59+
def build_system_prompt(args: dict) -> str:
60+
return f"""You are a product launch planning agent for {args["product_name"]}.
5961
6062
Use the planning tools provided by DeepAgents. Use score_launch_readiness to get
6163
a deterministic readiness signal. Delegate a plan review to risk_reviewer before
6264
finalizing the answer.
6365
66+
Tailor all planning to this audience: {args["audience"]}.
67+
6468
The UiPath runtime provides your DeepAgents filesystem through the tagged graph
6569
contract. Write these workspace files before producing the final answer:
6670
- /launch/brief.md with the final launch brief
@@ -70,7 +74,7 @@ def score_launch_readiness(audience: str, constraints: list[str]) -> str:
7074
you wrote in workspace_files."""
7175

7276

73-
def build_user_message(args: dict) -> str:
77+
def build_user_prompt(args: dict) -> str:
7478
constraints = args.get("constraints") or []
7579
constraint_lines = "\n".join(f"- {item}" for item in constraints) or "- None"
7680
return f"""Create a launch brief.
@@ -87,8 +91,8 @@ def build_user_message(args: dict) -> str:
8791
model=MODEL,
8892
input_schema=BriefInput,
8993
output_schema=BriefOutput,
90-
system_prompt=SYSTEM_PROMPT,
94+
system_prompt=build_system_prompt,
95+
user_prompt=build_user_prompt,
9196
tools=[score_launch_readiness],
9297
subagents=[RISK_REVIEWER],
93-
build_user_message=build_user_message,
9498
)

src/uipath_langchain/agent/advanced/agent.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from deepagents.backends.protocol import BackendFactory
1010
from langchain.agents.structured_output import ResponseFormat
1111
from langchain_core.language_models import BaseChatModel
12-
from langchain_core.messages import HumanMessage
12+
from langchain_core.messages import HumanMessage, SystemMessage
1313
from langchain_core.runnables.config import RunnableConfig
1414
from langchain_core.tools import BaseTool
1515
from langgraph.graph import END, START
@@ -63,6 +63,7 @@ def create_advanced_agent_graph(
6363
input_schema: type[BaseModel] | None,
6464
output_schema: type[BaseModel],
6565
build_user_message: Callable[[dict[str, Any]], str],
66+
build_system_message: Callable[[dict[str, Any]], str] | None = None,
6667
subagents: Sequence[SubAgent | CompiledSubAgent] = (),
6768
) -> StateGraph[Any, Any, Any, Any]:
6869
"""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(
112113
state=state,
113114
config=config,
114115
)
116+
messages = []
117+
if build_system_message is not None:
118+
system_text = build_system_message(input_args)
119+
if system_text:
120+
messages.append(SystemMessage(content=system_text, id="system-input"))
115121
user_text = build_user_message(input_args)
116-
return {"messages": [HumanMessage(content=user_text, id="user-input")]}
122+
messages.append(HumanMessage(content=user_text, id="user-input"))
123+
return {"messages": messages}
117124

118125
def transform_output(state: BaseModel) -> dict[str, Any]:
119126
structured = getattr(state, "structured_response", {})

src/uipath_langchain/deepagents/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .agent import (
44
UiPathDeepAgent,
5+
UiPathDeepAgentPrompt,
56
create_uipath_deep_agent,
67
create_uipath_deep_agent_graph,
78
)
@@ -16,6 +17,7 @@
1617
__all__ = [
1718
"UiPathDeepAgent",
1819
"UiPathDeepAgentHydrationPolicy",
20+
"UiPathDeepAgentPrompt",
1921
"UiPathDeepAgentRuntimeSpec",
2022
"create_uipath_deep_agent",
2123
"create_uipath_deep_agent_graph",

src/uipath_langchain/deepagents/agent.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
set_uipath_deep_agent_runtime_spec,
2525
)
2626

27+
UiPathDeepAgentPrompt = str | Callable[[dict[str, Any]], str]
28+
2729

2830
@dataclass(frozen=True)
2931
class UiPathDeepAgent:
@@ -38,7 +40,8 @@ def create_uipath_deep_agent(
3840
model: BaseChatModel,
3941
input_schema: type[BaseModel] | None = None,
4042
output_schema: type[BaseModel],
41-
system_prompt: str = "",
43+
system_prompt: UiPathDeepAgentPrompt = "",
44+
user_prompt: UiPathDeepAgentPrompt | None = None,
4245
tools: Sequence[BaseTool] = (),
4346
subagents: Sequence[SubAgent | CompiledSubAgent] = (),
4447
backend: BackendProtocol | BackendFactory | None = None,
@@ -60,6 +63,7 @@ def create_uipath_deep_agent(
6063
input_schema=input_schema,
6164
output_schema=output_schema,
6265
system_prompt=system_prompt,
66+
user_prompt=user_prompt,
6367
tools=tools,
6468
subagents=subagents,
6569
backend=backend,
@@ -85,7 +89,8 @@ def create_uipath_deep_agent_graph(
8589
model: BaseChatModel,
8690
input_schema: type[BaseModel] | None = None,
8791
output_schema: type[BaseModel],
88-
system_prompt: str = "",
92+
system_prompt: UiPathDeepAgentPrompt = "",
93+
user_prompt: UiPathDeepAgentPrompt | None = None,
8994
tools: Sequence[BaseTool] = (),
9095
subagents: Sequence[SubAgent | CompiledSubAgent] = (),
9196
backend: BackendProtocol | BackendFactory | None = None,
@@ -112,18 +117,27 @@ def create_uipath_deep_agent_graph(
112117
workspace_config_key=workspace_config_key,
113118
)
114119
response_format = response_format or ToolStrategy(output_schema)
115-
build_user_message = build_user_message or _default_user_message
120+
if user_prompt is not None and build_user_message is not None:
121+
raise ValueError("Use either user_prompt or build_user_message, not both.")
122+
123+
static_system_prompt, build_system_message = _split_system_prompt(system_prompt)
124+
build_user_message = (
125+
_coerce_prompt_renderer(user_prompt)
126+
if user_prompt is not None
127+
else build_user_message or _default_user_message
128+
)
116129

117130
graph = create_advanced_agent_graph(
118131
model=model,
119132
tools=tools,
120133
subagents=subagents,
121-
system_prompt=system_prompt,
134+
system_prompt=static_system_prompt,
122135
backend=backend,
123136
response_format=response_format,
124137
input_schema=input_schema,
125138
output_schema=output_schema,
126139
build_user_message=build_user_message,
140+
build_system_message=build_system_message,
127141
)
128142
set_uipath_deep_agent_runtime_spec(
129143
graph,
@@ -137,6 +151,22 @@ def create_uipath_deep_agent_graph(
137151
return graph
138152

139153

154+
def _split_system_prompt(
155+
system_prompt: UiPathDeepAgentPrompt,
156+
) -> tuple[str, Callable[[dict[str, Any]], str] | None]:
157+
if callable(system_prompt):
158+
return "", system_prompt
159+
return system_prompt, None
160+
161+
162+
def _coerce_prompt_renderer(
163+
prompt: UiPathDeepAgentPrompt,
164+
) -> Callable[[dict[str, Any]], str]:
165+
if callable(prompt):
166+
return prompt
167+
return lambda _args: prompt
168+
169+
140170
def _default_user_message(args: dict[str, Any]) -> str:
141171
if not args:
142172
return ""

tests/agent/advanced/test_create_advanced_agent_graph.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77
from langchain_core.language_models import BaseChatModel
8-
from langchain_core.messages import HumanMessage
8+
from langchain_core.messages import HumanMessage, SystemMessage
99
from pydantic import BaseModel, ConfigDict, Field
1010

1111
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
8686
assert message.id == "user-input"
8787

8888

89+
@pytest.mark.asyncio
90+
async def test_transform_input_includes_dynamic_system_message() -> None:
91+
"""Runtime input can render a system message before the user message."""
92+
graph = _build(
93+
input_schema=_Input,
94+
build_system_message=lambda args: f"system:{args['question']}",
95+
build_user_message=lambda args: f"user:{args['question']}",
96+
)
97+
state_cls = create_state_with_input(_Input)
98+
state = state_cls(question="q")
99+
100+
out = await graph.nodes["transform_input"].runnable.ainvoke(state)
101+
102+
system_message, user_message = out["messages"]
103+
assert isinstance(system_message, SystemMessage)
104+
assert system_message.content == "system:q"
105+
assert system_message.id == "system-input"
106+
assert isinstance(user_message, HumanMessage)
107+
assert user_message.content == "user:q"
108+
assert user_message.id == "user-input"
109+
110+
111+
@pytest.mark.asyncio
112+
async def test_transform_input_omits_empty_dynamic_system_message() -> None:
113+
"""An empty rendered system prompt does not create a blank message."""
114+
graph = _build(build_system_message=lambda _args: "")
115+
116+
out = await graph.nodes["transform_input"].runnable.ainvoke(
117+
AdvancedAgentGraphState()
118+
)
119+
120+
assert len(out["messages"]) == 1
121+
assert isinstance(out["messages"][0], HumanMessage)
122+
123+
89124
@pytest.mark.asyncio
90125
async def test_transform_input_resolves_attachments_when_present() -> None:
91126
"""With an input schema and attachment paths, input attachments are resolved first."""

tests/deepagents/test_metadata.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,46 @@ def test_subagents_are_forwarded_to_advanced_agent_graph() -> None:
9191
)
9292

9393
assert mock_create.call_args.kwargs["subagents"] == [subagent]
94+
95+
96+
def test_dynamic_system_prompt_is_forwarded_as_runtime_renderer() -> None:
97+
def build_system_prompt(args: dict[str, str]) -> str:
98+
return f"System for {args['topic']}"
99+
100+
with patch(
101+
"uipath_langchain.deepagents.agent.create_advanced_agent_graph",
102+
return_value=_graph(),
103+
) as mock_create:
104+
create_uipath_deep_agent_graph(
105+
model=object(), # type: ignore[arg-type]
106+
output_schema=Output,
107+
system_prompt=build_system_prompt,
108+
)
109+
110+
assert mock_create.call_args.kwargs["system_prompt"] == ""
111+
assert mock_create.call_args.kwargs["build_system_message"] is build_system_prompt
112+
113+
114+
def test_static_user_prompt_is_forwarded_as_user_renderer() -> None:
115+
with patch(
116+
"uipath_langchain.deepagents.agent.create_advanced_agent_graph",
117+
return_value=_graph(),
118+
) as mock_create:
119+
create_uipath_deep_agent_graph(
120+
model=object(), # type: ignore[arg-type]
121+
output_schema=Output,
122+
user_prompt="Create the brief.",
123+
)
124+
125+
build_user_message = mock_create.call_args.kwargs["build_user_message"]
126+
assert build_user_message({}) == "Create the brief."
127+
128+
129+
def test_user_prompt_and_build_user_message_are_mutually_exclusive() -> None:
130+
with pytest.raises(ValueError, match="either user_prompt or build_user_message"):
131+
create_uipath_deep_agent_graph(
132+
model=object(), # type: ignore[arg-type]
133+
output_schema=Output,
134+
user_prompt="Create the brief.",
135+
build_user_message=lambda _args: "Create the brief.",
136+
)

0 commit comments

Comments
 (0)