Skip to content

Commit 8e5f36d

Browse files
feat: add static args support (#431)
1 parent db2e07b commit 8e5f36d

16 files changed

Lines changed: 1268 additions & 90 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""JSONPath utilities."""
2+
3+
from jsonpath_ng import ( # type: ignore[import-untyped]
4+
Child,
5+
Fields,
6+
JSONPath,
7+
Root,
8+
Slice,
9+
parse,
10+
)
11+
12+
13+
def parse_jsonpath_segments(json_path: str) -> list[str]:
14+
"""Parse JSON path $['a']['b'] into list of segments ['a', 'b']."""
15+
jsonpath_expr = parse(json_path)
16+
return _extract_segments(jsonpath_expr)
17+
18+
19+
def _extract_segments(expr: JSONPath) -> list[str]:
20+
"""Recursively extract segments from a JSONPath expression tree.
21+
22+
Args:
23+
expr: The JSONPath expression node (Root, Child, Fields, etc.)
24+
"""
25+
parts: list[str] = []
26+
27+
match expr:
28+
case Root():
29+
pass
30+
case Fields():
31+
# Leaf node, add it to the path
32+
parts.extend(expr.fields)
33+
case Child():
34+
# Child node, walk left then right to maintain order
35+
parts.extend(_extract_segments(expr.left))
36+
parts.extend(_extract_segments(expr.right))
37+
case Slice(start=None, end=None, step=None):
38+
parts.append("*")
39+
40+
return parts

src/uipath_langchain/agent/react/agent.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ def create_agent(
108108
llm_node = create_llm_node(
109109
model,
110110
llm_tools,
111-
config.is_conversational,
112-
config.llm_messages_limit,
113-
config.thinking_messages_limit,
111+
input_schema=input_schema,
112+
is_conversational=config.is_conversational,
113+
llm_messages_limit=config.llm_messages_limit,
114+
thinking_messages_limit=config.thinking_messages_limit,
114115
)
115116
llm_with_guardrails_subgraph = create_llm_guardrails_subgraph(
116117
(AgentGraphNode.LLM, llm_node), guardrails, input_schema=input_schema

src/uipath_langchain/agent/react/llm_node.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
"""LLM node for ReAct Agent graph."""
22

3-
from typing import Literal, Sequence
3+
from typing import Literal, Sequence, TypeVar
44

55
from langchain_core.language_models import BaseChatModel
66
from langchain_core.messages import AIMessage, AnyMessage, ToolCall
77
from langchain_core.tools import BaseTool
8+
from pydantic import BaseModel
89
from uipath.runtime.errors import UiPathErrorCategory, UiPathErrorCode
910

11+
from uipath_langchain.agent.tools.static_args import (
12+
apply_static_argument_properties_to_schema,
13+
)
14+
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
15+
StructuredToolWithArgumentProperties,
16+
)
17+
1018
from ..exceptions import AgentTerminationException
1119
from .constants import (
1220
DEFAULT_MAX_CONSECUTIVE_THINKING_MESSAGES,
1321
DEFAULT_MAX_LLM_MESSAGES,
1422
)
1523
from .types import FLOW_CONTROL_TOOLS, AgentGraphState
16-
from .utils import count_consecutive_thinking_messages
24+
from .utils import count_consecutive_thinking_messages, extract_input_data_from_state
1725

1826
OPENAI_COMPATIBLE_CHAT_MODELS = (
1927
"UiPathChatOpenAI",
@@ -48,9 +56,14 @@ def _filter_control_flow_tool_calls(
4856
return [tc for tc in tool_calls if tc.get("name") not in FLOW_CONTROL_TOOLS]
4957

5058

59+
StateT = TypeVar("StateT", bound=AgentGraphState)
60+
InputT = TypeVar("InputT", bound=BaseModel)
61+
62+
5163
def create_llm_node(
5264
model: BaseChatModel,
53-
tools: Sequence[BaseTool] | None = None,
65+
tools: Sequence[BaseTool],
66+
input_schema: type[InputT] | None = None,
5467
is_conversational: bool = False,
5568
llm_messages_limit: int = DEFAULT_MAX_LLM_MESSAGES,
5669
thinking_messages_limit: int = DEFAULT_MAX_CONSECUTIVE_THINKING_MESSAGES,
@@ -69,12 +82,10 @@ def create_llm_node(
6982
before enforcing tool usage. 0 = force tools every time.
7083
"""
7184
bindable_tools = list(tools) if tools else []
72-
base_llm = model.bind_tools(bindable_tools) if bindable_tools else model
7385
tool_choice_required_value = _get_required_tool_choice_by_model(model)
7486

75-
async def llm_node(state: AgentGraphState):
87+
async def llm_node(state: StateT):
7688
messages: list[AnyMessage] = state.messages
77-
7889
agent_ai_messages = sum(1 for msg in messages if isinstance(msg, AIMessage))
7990
if agent_ai_messages >= llm_messages_limit:
8091
raise AgentTerminationException(
@@ -86,6 +97,11 @@ async def llm_node(state: AgentGraphState):
8697

8798
consecutive_thinking_messages = count_consecutive_thinking_messages(messages)
8899

100+
static_schema_tools = _apply_tool_argument_properties(
101+
bindable_tools, state, input_schema
102+
)
103+
base_llm = model.bind_tools(static_schema_tools)
104+
89105
if (
90106
not is_conversational
91107
and bindable_tools
@@ -111,3 +127,19 @@ async def llm_node(state: AgentGraphState):
111127
return {"messages": [response]}
112128

113129
return llm_node
130+
131+
132+
def _apply_tool_argument_properties(
133+
tools: list[BaseTool],
134+
state: StateT,
135+
input_schema: type[InputT] | None = None,
136+
) -> list[BaseTool]:
137+
"""Apply dynamic schema modifications to tools based on their argument_properties."""
138+
139+
agent_input = extract_input_data_from_state(state, input_schema or type(state))
140+
return [
141+
apply_static_argument_properties_to_schema(tool, agent_input)
142+
if isinstance(tool, StructuredToolWithArgumentProperties)
143+
else tool
144+
for tool in tools
145+
]

src/uipath_langchain/agent/react/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@ def resolve_output_model(
3434
return END_EXECUTION_TOOL.args_schema
3535

3636

37+
def extract_input_data_from_state(
38+
state: BaseModel | dict[str, Any],
39+
input_model: type[BaseModel],
40+
) -> dict[str, Any]:
41+
"""Extract only input schema fields from graph state, filtering out internal fields.
42+
43+
This prevents internal LangGraph state fields (messages, termination, agent_outcome, etc.)
44+
from leaking into template interpolation.
45+
46+
Args:
47+
state: The combined agent graph state (InnerAgentGraphState = AgentGraphState + input_schema).
48+
At runtime, this is a dynamically created class that inherits from both.
49+
input_model: The input schema model defining allowed fields
50+
51+
Returns:
52+
Dictionary containing only graph input arguments defined in the agent's input_schema
53+
"""
54+
if isinstance(state, BaseModel):
55+
graph_state = state.model_dump()
56+
else:
57+
graph_state = state
58+
return input_model.model_validate(graph_state, from_attributes=True).model_dump()
59+
60+
3761
def count_consecutive_thinking_messages(messages: Sequence[BaseMessage]) -> int:
3862
"""Count consecutive AIMessages without tool calls at end of message history."""
3963
if not messages:

src/uipath_langchain/agent/tools/integration_tool.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@
33
import copy
44
from typing import Any
55

6+
from jsonschema_pydantic_converter import transform as create_model
7+
from langchain.tools import BaseTool
8+
from langchain_core.messages import ToolCall
69
from langchain_core.tools import StructuredTool
710
from uipath.agent.models.agent import AgentIntegrationToolResourceConfig
811
from uipath.eval.mocks import mockable
912
from uipath.platform import UiPath
1013
from uipath.platform.connections import ActivityMetadata, ActivityParameterLocationInfo
1114

12-
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
13-
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
15+
from uipath_langchain.agent.react.types import AgentGraphState
16+
from uipath_langchain.agent.tools.static_args import handle_static_args
17+
from uipath_langchain.agent.tools.tool_node import (
18+
ToolWrapperMixin,
19+
ToolWrapperReturnType,
20+
)
1421

1522
from .structured_tool_with_output_type import StructuredToolWithOutputType
1623
from .utils import sanitize_dict_for_serialization, sanitize_tool_name
1724

1825

19-
class StructuredToolWithStaticArgs(StructuredToolWithOutputType, ToolWrapperMixin):
26+
class StructuredToolWithWrapper(StructuredToolWithOutputType, ToolWrapperMixin):
2027
pass
2128

2229

@@ -167,13 +174,15 @@ async def integration_tool_fn(**kwargs: Any):
167174

168175
return result
169176

170-
from uipath_langchain.agent.wrappers.static_args_wrapper import (
171-
get_static_args_wrapper,
172-
)
173-
174-
wrapper = get_static_args_wrapper(resource)
177+
async def integration_tool_wrapper(
178+
tool: BaseTool,
179+
call: ToolCall,
180+
state: AgentGraphState,
181+
) -> ToolWrapperReturnType:
182+
modified_args = handle_static_args(resource, state, call["args"])
183+
return await tool.ainvoke(modified_args)
175184

176-
tool = StructuredToolWithStaticArgs(
185+
tool = StructuredToolWithWrapper(
177186
name=tool_name,
178187
description=resource.description,
179188
args_schema=input_model,
@@ -184,6 +193,6 @@ async def integration_tool_fn(**kwargs: Any):
184193
"display_name": resource.name,
185194
},
186195
)
187-
tool.set_tool_wrappers(awrapper=wrapper)
196+
tool.set_tool_wrappers(awrapper=integration_tool_wrapper)
188197

189198
return tool

src/uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
from langchain_core.language_models import BaseChatModel
55
from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
6-
from langchain_core.tools import StructuredTool
6+
from langchain_core.messages.tool import ToolCall
7+
from langchain_core.tools import BaseTool, StructuredTool
78
from uipath.agent.models.agent import (
89
AgentInternalToolResourceConfig,
910
)
@@ -12,11 +13,16 @@
1213

1314
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
1415
from uipath_langchain.agent.react.llm_with_files import FileInfo, llm_call_with_files
15-
from uipath_langchain.agent.tools.structured_tool_with_output_type import (
16-
StructuredToolWithOutputType,
16+
from uipath_langchain.agent.react.types import AgentGraphState
17+
from uipath_langchain.agent.tools.static_args import handle_static_args
18+
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
19+
StructuredToolWithArgumentProperties,
20+
)
21+
from uipath_langchain.agent.tools.tool_node import (
22+
ToolWrapperReturnType,
1723
)
18-
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
1924
from uipath_langchain.agent.tools.utils import sanitize_tool_name
25+
from uipath_langchain.agent.wrappers import get_job_attachment_wrapper
2026

2127
ANALYZE_FILES_SYSTEM_MESSAGE = (
2228
"Process the provided files to complete the given task. "
@@ -25,17 +31,9 @@
2531
)
2632

2733

28-
class AnalyzeFileTool(StructuredToolWithOutputType, ToolWrapperMixin):
29-
pass
30-
31-
3234
def create_analyze_file_tool(
3335
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
3436
) -> StructuredTool:
35-
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
36-
get_job_attachment_wrapper,
37-
)
38-
3937
tool_name = sanitize_tool_name(resource.name)
4038
input_model = create_model(resource.input_schema)
4139
output_model = create_model(resource.output_schema)
@@ -69,15 +67,25 @@ async def tool_fn(**kwargs: Any):
6967
result = await llm_call_with_files(messages, files, llm)
7068
return result
7169

72-
wrapper = get_job_attachment_wrapper(output_type=output_model)
73-
tool = AnalyzeFileTool(
70+
job_attachment_wrapper = get_job_attachment_wrapper(output_type=output_model)
71+
72+
async def analyze_file_tool_wrapper(
73+
tool: BaseTool,
74+
call: ToolCall,
75+
state: AgentGraphState,
76+
) -> ToolWrapperReturnType:
77+
call["args"] = handle_static_args(resource, state, call["args"])
78+
return await job_attachment_wrapper(tool, call, state)
79+
80+
tool = StructuredToolWithArgumentProperties(
7481
name=tool_name,
7582
description=resource.description,
7683
args_schema=input_model,
7784
coroutine=tool_fn,
7885
output_type=output_model,
86+
argument_properties=resource.argument_properties,
7987
)
80-
tool.set_tool_wrappers(awrapper=wrapper)
88+
tool.set_tool_wrappers(awrapper=analyze_file_tool_wrapper)
8189
return tool
8290

8391

src/uipath_langchain/agent/tools/process_tool.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,28 @@
22

33
from typing import Any
44

5+
from jsonschema_pydantic_converter import transform as create_model
6+
from langchain.tools import BaseTool
7+
from langchain_core.messages import ToolCall
58
from langchain_core.tools import StructuredTool
69
from langgraph.types import interrupt
710
from uipath.agent.models.agent import AgentProcessToolResourceConfig
811
from uipath.eval.mocks import mockable
912
from uipath.platform.common import InvokeProcess
1013

11-
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
12-
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
13-
get_job_attachment_wrapper,
14+
from uipath_langchain.agent.react.types import AgentGraphState
15+
from uipath_langchain.agent.tools.static_args import handle_static_args
16+
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
17+
StructuredToolWithArgumentProperties,
1418
)
19+
from uipath_langchain.agent.tools.tool_node import (
20+
ToolWrapperReturnType,
21+
)
22+
from uipath_langchain.agent.wrappers import get_job_attachment_wrapper
1523

16-
from .structured_tool_with_output_type import StructuredToolWithOutputType
17-
from .tool_node import ToolWrapperMixin
1824
from .utils import sanitize_tool_name
1925

2026

21-
class ProcessTool(StructuredToolWithOutputType, ToolWrapperMixin):
22-
pass
23-
24-
2527
def create_process_tool(resource: AgentProcessToolResourceConfig) -> StructuredTool:
2628
"""Uses interrupt() to suspend graph execution until process completes (handled by runtime)."""
2729
tool_name: str = sanitize_tool_name(resource.name)
@@ -48,8 +50,17 @@ async def process_tool_fn(**kwargs: Any):
4850
)
4951
)
5052

51-
wrapper = get_job_attachment_wrapper(output_type=output_model)
52-
tool = ProcessTool(
53+
job_attachment_wrapper = get_job_attachment_wrapper(output_type=output_model)
54+
55+
async def process_tool_wrapper(
56+
tool: BaseTool,
57+
call: ToolCall,
58+
state: AgentGraphState,
59+
) -> ToolWrapperReturnType:
60+
call["args"] = handle_static_args(resource, state, call["args"])
61+
return await job_attachment_wrapper(tool, call, state)
62+
63+
tool = StructuredToolWithArgumentProperties(
5364
name=tool_name,
5465
description=resource.description,
5566
args_schema=input_model,
@@ -60,6 +71,8 @@ async def process_tool_fn(**kwargs: Any):
6071
"display_name": process_name,
6172
"folder_path": folder_path,
6273
},
74+
argument_properties=resource.argument_properties,
6375
)
64-
tool.set_tool_wrappers(awrapper=wrapper)
76+
tool.set_tool_wrappers(awrapper=process_tool_wrapper)
77+
6578
return tool

0 commit comments

Comments
 (0)