uipath-langchain 0.1.34__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uipath_langchain/_cli/_templates/langgraph.json.template +2 -4
- uipath_langchain/_cli/cli_new.py +1 -2
- uipath_langchain/agent/guardrails/actions/escalate_action.py +252 -108
- uipath_langchain/agent/guardrails/actions/filter_action.py +247 -12
- uipath_langchain/agent/guardrails/guardrail_nodes.py +47 -12
- uipath_langchain/agent/guardrails/guardrails_factory.py +40 -15
- uipath_langchain/agent/guardrails/utils.py +64 -33
- uipath_langchain/agent/react/agent.py +4 -2
- uipath_langchain/agent/react/file_type_handler.py +123 -0
- uipath_langchain/agent/react/guardrails/guardrails_subgraph.py +67 -12
- uipath_langchain/agent/react/init_node.py +16 -1
- uipath_langchain/agent/react/job_attachments.py +125 -0
- uipath_langchain/agent/react/json_utils.py +183 -0
- uipath_langchain/agent/react/jsonschema_pydantic_converter.py +76 -0
- uipath_langchain/agent/react/llm_with_files.py +76 -0
- uipath_langchain/agent/react/types.py +4 -0
- uipath_langchain/agent/react/utils.py +29 -3
- uipath_langchain/agent/tools/__init__.py +5 -1
- uipath_langchain/agent/tools/context_tool.py +151 -1
- uipath_langchain/agent/tools/escalation_tool.py +46 -15
- uipath_langchain/agent/tools/integration_tool.py +20 -16
- uipath_langchain/agent/tools/internal_tools/__init__.py +5 -0
- uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py +113 -0
- uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py +54 -0
- uipath_langchain/agent/tools/process_tool.py +8 -1
- uipath_langchain/agent/tools/static_args.py +18 -40
- uipath_langchain/agent/tools/tool_factory.py +13 -5
- uipath_langchain/agent/tools/tool_node.py +133 -4
- uipath_langchain/agent/tools/utils.py +31 -0
- uipath_langchain/agent/wrappers/__init__.py +6 -0
- uipath_langchain/agent/wrappers/job_attachment_wrapper.py +62 -0
- uipath_langchain/agent/wrappers/static_args_wrapper.py +34 -0
- uipath_langchain/chat/mapper.py +60 -42
- uipath_langchain/runtime/factory.py +10 -5
- uipath_langchain/runtime/runtime.py +38 -35
- uipath_langchain/runtime/storage.py +178 -71
- {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/METADATA +5 -4
- {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/RECORD +41 -30
- {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/WHEEL +0 -0
- {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/entry_points.txt +0 -0
- {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
+
from langchain_core.messages import AIMessage, ToolMessage
|
|
5
|
+
from langgraph.types import Command
|
|
6
|
+
from uipath.core.guardrails.guardrails import FieldReference, FieldSource
|
|
4
7
|
from uipath.platform.guardrails import BaseGuardrail, GuardrailScope
|
|
5
8
|
from uipath.runtime.errors import UiPathErrorCategory, UiPathErrorCode
|
|
6
9
|
|
|
@@ -14,11 +17,18 @@ from .base_action import GuardrailAction, GuardrailActionNode
|
|
|
14
17
|
class FilterAction(GuardrailAction):
|
|
15
18
|
"""Action that filters inputs/outputs on guardrail failure.
|
|
16
19
|
|
|
17
|
-
For
|
|
18
|
-
|
|
19
|
-
raises an exception to indicate the operation is not supported yet.
|
|
20
|
+
For Tool scope, this action removes specified fields from tool call arguments.
|
|
21
|
+
For AGENT and LLM scopes, this action raises an exception as it's not supported yet.
|
|
20
22
|
"""
|
|
21
23
|
|
|
24
|
+
def __init__(self, fields: list[FieldReference] | None = None):
|
|
25
|
+
"""Initialize FilterAction with fields to filter.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
fields: List of FieldReference objects specifying which fields to filter.
|
|
29
|
+
"""
|
|
30
|
+
self.fields = fields or []
|
|
31
|
+
|
|
22
32
|
def action_node(
|
|
23
33
|
self,
|
|
24
34
|
*,
|
|
@@ -41,15 +51,240 @@ class FilterAction(GuardrailAction):
|
|
|
41
51
|
raw_node_name = f"{scope.name}_{execution_stage.name}_{guardrail.name}_filter"
|
|
42
52
|
node_name = re.sub(r"\W+", "_", raw_node_name.lower()).strip("_")
|
|
43
53
|
|
|
44
|
-
async def _node(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
async def _node(
|
|
55
|
+
_state: AgentGuardrailsGraphState,
|
|
56
|
+
) -> dict[str, Any] | Command[Any]:
|
|
57
|
+
if scope == GuardrailScope.TOOL:
|
|
58
|
+
return _filter_tool_fields(
|
|
59
|
+
_state,
|
|
60
|
+
self.fields,
|
|
61
|
+
execution_stage,
|
|
62
|
+
guarded_component_name,
|
|
63
|
+
guardrail.name,
|
|
51
64
|
)
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
|
|
66
|
+
raise AgentTerminationException(
|
|
67
|
+
code=UiPathErrorCode.EXECUTION_ERROR,
|
|
68
|
+
title="Guardrail filter action not supported",
|
|
69
|
+
detail=f"FilterAction is not supported for scope [{scope.name}] at this time.",
|
|
70
|
+
category=UiPathErrorCategory.USER,
|
|
71
|
+
)
|
|
54
72
|
|
|
55
73
|
return node_name, _node
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _filter_tool_fields(
|
|
77
|
+
state: AgentGuardrailsGraphState,
|
|
78
|
+
fields_to_filter: list[FieldReference],
|
|
79
|
+
execution_stage: ExecutionStage,
|
|
80
|
+
tool_name: str,
|
|
81
|
+
guardrail_name: str,
|
|
82
|
+
) -> dict[str, Any] | Command[Any]:
|
|
83
|
+
"""Filter specified fields from tool call arguments or tool output.
|
|
84
|
+
|
|
85
|
+
The filter action filters fields based on the execution stage:
|
|
86
|
+
- PRE_EXECUTION: Only input fields are filtered
|
|
87
|
+
- POST_EXECUTION: Only output fields are filtered
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
state: The current agent graph state.
|
|
91
|
+
fields_to_filter: List of FieldReference objects specifying which fields to filter.
|
|
92
|
+
execution_stage: The execution stage (PRE_EXECUTION or POST_EXECUTION).
|
|
93
|
+
tool_name: Name of the tool to filter.
|
|
94
|
+
guardrail_name: Name of the guardrail for logging purposes.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Command to update messages with filtered tool call args or output.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
AgentTerminationException: If filtering fails.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
if not fields_to_filter:
|
|
104
|
+
return {}
|
|
105
|
+
|
|
106
|
+
if execution_stage == ExecutionStage.PRE_EXECUTION:
|
|
107
|
+
return _filter_tool_input_fields(state, fields_to_filter, tool_name)
|
|
108
|
+
else:
|
|
109
|
+
return _filter_tool_output_fields(state, fields_to_filter)
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise AgentTerminationException(
|
|
113
|
+
code=UiPathErrorCode.EXECUTION_ERROR,
|
|
114
|
+
title="Filter action failed",
|
|
115
|
+
detail=f"Failed to filter tool fields: {str(e)}",
|
|
116
|
+
category=UiPathErrorCategory.USER,
|
|
117
|
+
) from e
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _filter_tool_input_fields(
|
|
121
|
+
state: AgentGuardrailsGraphState,
|
|
122
|
+
fields_to_filter: list[FieldReference],
|
|
123
|
+
tool_name: str,
|
|
124
|
+
) -> dict[str, Any] | Command[Any]:
|
|
125
|
+
"""Filter specified input fields from tool call arguments (PRE_EXECUTION only).
|
|
126
|
+
|
|
127
|
+
This function is called at PRE_EXECUTION to filter input fields from tool call arguments
|
|
128
|
+
before the tool is executed.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
state: The current agent graph state.
|
|
132
|
+
fields_to_filter: List of FieldReference objects specifying which fields to filter.
|
|
133
|
+
tool_name: Name of the tool to filter.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Command to update messages with filtered tool call args, or empty dict if no input fields to filter.
|
|
137
|
+
"""
|
|
138
|
+
# Check if there are any input fields to filter
|
|
139
|
+
has_input_fields = any(
|
|
140
|
+
field_ref.source == FieldSource.INPUT for field_ref in fields_to_filter
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not has_input_fields:
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
msgs = state.messages.copy()
|
|
147
|
+
if not msgs:
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
# Find the AIMessage with tool calls
|
|
151
|
+
# At PRE_EXECUTION, this is always the last message
|
|
152
|
+
ai_message = None
|
|
153
|
+
for i in range(len(msgs) - 1, -1, -1):
|
|
154
|
+
msg = msgs[i]
|
|
155
|
+
if isinstance(msg, AIMessage) and msg.tool_calls:
|
|
156
|
+
ai_message = msg
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
if ai_message is None:
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
# Find and filter the tool call with matching name
|
|
163
|
+
# Type assertion: we know ai_message is AIMessage from the check above
|
|
164
|
+
assert isinstance(ai_message, AIMessage)
|
|
165
|
+
tool_calls = list(ai_message.tool_calls)
|
|
166
|
+
modified = False
|
|
167
|
+
|
|
168
|
+
for tool_call in tool_calls:
|
|
169
|
+
call_name = (
|
|
170
|
+
tool_call.get("name")
|
|
171
|
+
if isinstance(tool_call, dict)
|
|
172
|
+
else getattr(tool_call, "name", None)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if call_name == tool_name:
|
|
176
|
+
# Get the current args
|
|
177
|
+
args = (
|
|
178
|
+
tool_call.get("args")
|
|
179
|
+
if isinstance(tool_call, dict)
|
|
180
|
+
else getattr(tool_call, "args", None)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if args and isinstance(args, dict):
|
|
184
|
+
# Filter out the specified input fields
|
|
185
|
+
filtered_args = args.copy()
|
|
186
|
+
for field_ref in fields_to_filter:
|
|
187
|
+
# Only filter input fields
|
|
188
|
+
if (
|
|
189
|
+
field_ref.source == FieldSource.INPUT
|
|
190
|
+
and field_ref.path in filtered_args
|
|
191
|
+
):
|
|
192
|
+
del filtered_args[field_ref.path]
|
|
193
|
+
modified = True
|
|
194
|
+
|
|
195
|
+
# Update the tool call with filtered args
|
|
196
|
+
if isinstance(tool_call, dict):
|
|
197
|
+
tool_call["args"] = filtered_args
|
|
198
|
+
else:
|
|
199
|
+
tool_call.args = filtered_args
|
|
200
|
+
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if modified:
|
|
204
|
+
ai_message.tool_calls = tool_calls
|
|
205
|
+
return Command(update={"messages": msgs})
|
|
206
|
+
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _filter_tool_output_fields(
|
|
211
|
+
state: AgentGuardrailsGraphState,
|
|
212
|
+
fields_to_filter: list[FieldReference],
|
|
213
|
+
) -> dict[str, Any] | Command[Any]:
|
|
214
|
+
"""Filter specified output fields from tool output (POST_EXECUTION only).
|
|
215
|
+
|
|
216
|
+
This function is called at POST_EXECUTION to filter output fields from tool results
|
|
217
|
+
after the tool has been executed.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
state: The current agent graph state.
|
|
221
|
+
fields_to_filter: List of FieldReference objects specifying which fields to filter.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Command to update messages with filtered tool output, or empty dict if no output fields to filter.
|
|
225
|
+
"""
|
|
226
|
+
# Check if there are any output fields to filter
|
|
227
|
+
has_output_fields = any(
|
|
228
|
+
field_ref.source == FieldSource.OUTPUT for field_ref in fields_to_filter
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if not has_output_fields:
|
|
232
|
+
return {}
|
|
233
|
+
|
|
234
|
+
msgs = state.messages.copy()
|
|
235
|
+
if not msgs:
|
|
236
|
+
return {}
|
|
237
|
+
|
|
238
|
+
last_message = msgs[-1]
|
|
239
|
+
if not isinstance(last_message, ToolMessage):
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
# Parse the tool output content
|
|
243
|
+
import json
|
|
244
|
+
|
|
245
|
+
content = last_message.content
|
|
246
|
+
if not content:
|
|
247
|
+
return {}
|
|
248
|
+
|
|
249
|
+
# Try to parse the content as JSON or dict
|
|
250
|
+
try:
|
|
251
|
+
if isinstance(content, dict):
|
|
252
|
+
output_data = content
|
|
253
|
+
elif isinstance(content, str):
|
|
254
|
+
try:
|
|
255
|
+
output_data = json.loads(content)
|
|
256
|
+
except json.JSONDecodeError:
|
|
257
|
+
# Try to parse as Python literal (dict representation)
|
|
258
|
+
import ast
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
output_data = ast.literal_eval(content)
|
|
262
|
+
if not isinstance(output_data, dict):
|
|
263
|
+
return {}
|
|
264
|
+
except (ValueError, SyntaxError):
|
|
265
|
+
return {}
|
|
266
|
+
else:
|
|
267
|
+
# Content is not JSON-parseable, can't filter specific fields
|
|
268
|
+
return {}
|
|
269
|
+
except Exception:
|
|
270
|
+
return {}
|
|
271
|
+
|
|
272
|
+
if not isinstance(output_data, dict):
|
|
273
|
+
return {}
|
|
274
|
+
|
|
275
|
+
# Filter out the specified fields
|
|
276
|
+
filtered_output = output_data.copy()
|
|
277
|
+
modified = False
|
|
278
|
+
|
|
279
|
+
for field_ref in fields_to_filter:
|
|
280
|
+
# Only filter output fields
|
|
281
|
+
if field_ref.source == FieldSource.OUTPUT and field_ref.path in filtered_output:
|
|
282
|
+
del filtered_output[field_ref.path]
|
|
283
|
+
modified = True
|
|
284
|
+
|
|
285
|
+
if modified:
|
|
286
|
+
# Update the tool message content with filtered output
|
|
287
|
+
last_message.content = json.dumps(filtered_output)
|
|
288
|
+
return Command(update={"messages": msgs})
|
|
289
|
+
|
|
290
|
+
return {}
|
|
@@ -18,8 +18,9 @@ from uipath.runtime.errors import UiPathErrorCode
|
|
|
18
18
|
|
|
19
19
|
from uipath_langchain.agent.guardrails.types import ExecutionStage
|
|
20
20
|
from uipath_langchain.agent.guardrails.utils import (
|
|
21
|
-
|
|
21
|
+
_extract_tool_args_from_message,
|
|
22
22
|
_extract_tool_output_data,
|
|
23
|
+
_extract_tools_args_from_message,
|
|
23
24
|
get_message_content,
|
|
24
25
|
)
|
|
25
26
|
from uipath_langchain.agent.react.types import AgentGuardrailsGraphState
|
|
@@ -117,10 +118,11 @@ def _create_guardrail_node(
|
|
|
117
118
|
| None = None,
|
|
118
119
|
output_data_extractor: Callable[[AgentGuardrailsGraphState], dict[str, Any]]
|
|
119
120
|
| None = None,
|
|
121
|
+
tool_name: str | None = None,
|
|
120
122
|
) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]:
|
|
121
123
|
"""Private factory for guardrail evaluation nodes.
|
|
122
124
|
|
|
123
|
-
Returns a node
|
|
125
|
+
Returns a node with observability metadata attached as __metadata__ attribute:
|
|
124
126
|
- goto success_node on validation pass
|
|
125
127
|
- goto failure_node on validation fail
|
|
126
128
|
"""
|
|
@@ -149,12 +151,22 @@ def _create_guardrail_node(
|
|
|
149
151
|
state, guardrail, payload_generator
|
|
150
152
|
)
|
|
151
153
|
else:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
# Provide specific error message for DeterministicGuardrails with wrong scope
|
|
155
|
+
if isinstance(guardrail, DeterministicGuardrail):
|
|
156
|
+
raise AgentTerminationException(
|
|
157
|
+
code=UiPathErrorCode.EXECUTION_ERROR,
|
|
158
|
+
title="Invalid guardrail scope",
|
|
159
|
+
detail=f"DeterministicGuardrail '{guardrail.name}' can only be used with TOOL scope. "
|
|
160
|
+
f"Current scope: {scope.name}. "
|
|
161
|
+
f"Please configure this guardrail to use only TOOL scope.",
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
raise AgentTerminationException(
|
|
165
|
+
code=UiPathErrorCode.EXECUTION_ERROR,
|
|
166
|
+
title="Unsupported guardrail type",
|
|
167
|
+
detail=f"Guardrail type '{type(guardrail).__name__}' is not supported. "
|
|
168
|
+
f"Expected DeterministicGuardrail (TOOL scope only) or BuiltInValidatorGuardrail.",
|
|
169
|
+
)
|
|
158
170
|
|
|
159
171
|
return _create_validation_command(result, success_node, failure_node)
|
|
160
172
|
|
|
@@ -166,6 +178,15 @@ def _create_guardrail_node(
|
|
|
166
178
|
)
|
|
167
179
|
raise
|
|
168
180
|
|
|
181
|
+
# Attach observability metadata as function attribute
|
|
182
|
+
node.__metadata__ = { # type: ignore[attr-defined]
|
|
183
|
+
"guardrail_name": guardrail.name,
|
|
184
|
+
"guardrail_description": getattr(guardrail, "description", None),
|
|
185
|
+
"guardrail_scope": scope.value,
|
|
186
|
+
"guardrail_stage": execution_stage.value,
|
|
187
|
+
"tool_name": tool_name,
|
|
188
|
+
}
|
|
189
|
+
|
|
169
190
|
return node_name, node
|
|
170
191
|
|
|
171
192
|
|
|
@@ -178,7 +199,11 @@ def create_llm_guardrail_node(
|
|
|
178
199
|
def _payload_generator(state: AgentGuardrailsGraphState) -> str:
|
|
179
200
|
if not state.messages:
|
|
180
201
|
return ""
|
|
181
|
-
|
|
202
|
+
match execution_stage:
|
|
203
|
+
case ExecutionStage.PRE_EXECUTION:
|
|
204
|
+
return get_message_content(state.messages[-1])
|
|
205
|
+
case ExecutionStage.POST_EXECUTION:
|
|
206
|
+
return json.dumps(_extract_tools_args_from_message(state.messages[-1]))
|
|
182
207
|
|
|
183
208
|
return _create_guardrail_node(
|
|
184
209
|
guardrail,
|
|
@@ -263,8 +288,8 @@ def create_tool_guardrail_node(
|
|
|
263
288
|
return ""
|
|
264
289
|
|
|
265
290
|
if execution_stage == ExecutionStage.PRE_EXECUTION:
|
|
266
|
-
|
|
267
|
-
args_dict =
|
|
291
|
+
last_message = state.messages[-1]
|
|
292
|
+
args_dict = _extract_tool_args_from_message(last_message, tool_name)
|
|
268
293
|
if args_dict:
|
|
269
294
|
return json.dumps(args_dict)
|
|
270
295
|
|
|
@@ -272,7 +297,16 @@ def create_tool_guardrail_node(
|
|
|
272
297
|
|
|
273
298
|
# Create closures for input/output data extraction (for deterministic guardrails)
|
|
274
299
|
def _input_data_extractor(state: AgentGuardrailsGraphState) -> dict[str, Any]:
|
|
275
|
-
|
|
300
|
+
if execution_stage == ExecutionStage.PRE_EXECUTION:
|
|
301
|
+
if len(state.messages) < 1:
|
|
302
|
+
return {}
|
|
303
|
+
message = state.messages[-1]
|
|
304
|
+
else: # POST_EXECUTION
|
|
305
|
+
if len(state.messages) < 2:
|
|
306
|
+
return {}
|
|
307
|
+
message = state.messages[-2]
|
|
308
|
+
|
|
309
|
+
return _extract_tool_args_from_message(message, tool_name)
|
|
276
310
|
|
|
277
311
|
def _output_data_extractor(state: AgentGuardrailsGraphState) -> dict[str, Any]:
|
|
278
312
|
return _extract_tool_output_data(state)
|
|
@@ -286,4 +320,5 @@ def create_tool_guardrail_node(
|
|
|
286
320
|
failure_node,
|
|
287
321
|
_input_data_extractor,
|
|
288
322
|
_output_data_extractor,
|
|
323
|
+
tool_name,
|
|
289
324
|
)
|
|
@@ -9,6 +9,7 @@ from uipath.agent.models.agent import (
|
|
|
9
9
|
AgentGuardrail,
|
|
10
10
|
AgentGuardrailBlockAction,
|
|
11
11
|
AgentGuardrailEscalateAction,
|
|
12
|
+
AgentGuardrailFilterAction,
|
|
12
13
|
AgentGuardrailLogAction,
|
|
13
14
|
AgentGuardrailSeverityLevel,
|
|
14
15
|
AgentNumberOperator,
|
|
@@ -16,6 +17,7 @@ from uipath.agent.models.agent import (
|
|
|
16
17
|
AgentUnknownGuardrail,
|
|
17
18
|
AgentWordOperator,
|
|
18
19
|
AgentWordRule,
|
|
20
|
+
StandardRecipient,
|
|
19
21
|
)
|
|
20
22
|
from uipath.core.guardrails import (
|
|
21
23
|
BooleanRule,
|
|
@@ -24,14 +26,16 @@ from uipath.core.guardrails import (
|
|
|
24
26
|
UniversalRule,
|
|
25
27
|
WordRule,
|
|
26
28
|
)
|
|
27
|
-
from uipath.platform.guardrails import BaseGuardrail
|
|
29
|
+
from uipath.platform.guardrails import BaseGuardrail, GuardrailScope
|
|
28
30
|
|
|
29
31
|
from uipath_langchain.agent.guardrails.actions import (
|
|
30
32
|
BlockAction,
|
|
31
33
|
EscalateAction,
|
|
34
|
+
FilterAction,
|
|
32
35
|
GuardrailAction,
|
|
33
36
|
LogAction,
|
|
34
37
|
)
|
|
38
|
+
from uipath_langchain.agent.guardrails.utils import _sanitize_selector_tool_names
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def _assert_value_not_none(value: str | None, operator: AgentWordOperator) -> str:
|
|
@@ -191,18 +195,21 @@ def _convert_agent_custom_guardrail_to_deterministic(
|
|
|
191
195
|
guardrail: The agent custom guardrail to convert.
|
|
192
196
|
|
|
193
197
|
Returns:
|
|
194
|
-
A DeterministicGuardrail with converted rules.
|
|
198
|
+
A DeterministicGuardrail with converted rules and sanitized selector.
|
|
195
199
|
"""
|
|
196
200
|
converted_rules = [
|
|
197
201
|
_convert_agent_rule_to_deterministic(rule) for rule in guardrail.rules
|
|
198
202
|
]
|
|
199
203
|
|
|
204
|
+
# Sanitize tool names in selector for Tool scope guardrails
|
|
205
|
+
sanitized_selector = _sanitize_selector_tool_names(guardrail.selector)
|
|
206
|
+
|
|
200
207
|
return DeterministicGuardrail(
|
|
201
208
|
id=guardrail.id,
|
|
202
209
|
name=guardrail.name,
|
|
203
210
|
description=guardrail.description,
|
|
204
211
|
enabled_for_evals=guardrail.enabled_for_evals,
|
|
205
|
-
selector=
|
|
212
|
+
selector=sanitized_selector,
|
|
206
213
|
guardrail_type="custom",
|
|
207
214
|
rules=converted_rules,
|
|
208
215
|
)
|
|
@@ -227,12 +234,27 @@ def build_guardrails_with_actions(
|
|
|
227
234
|
if isinstance(guardrail, AgentUnknownGuardrail):
|
|
228
235
|
continue
|
|
229
236
|
|
|
230
|
-
|
|
231
|
-
converted_guardrail: BaseGuardrail = guardrail
|
|
237
|
+
converted_guardrail: BaseGuardrail
|
|
232
238
|
if isinstance(guardrail, AgentCustomGuardrail):
|
|
233
239
|
converted_guardrail = _convert_agent_custom_guardrail_to_deterministic(
|
|
234
240
|
guardrail
|
|
235
241
|
)
|
|
242
|
+
# Validate that DeterministicGuardrails only have TOOL scope
|
|
243
|
+
non_tool_scopes = [
|
|
244
|
+
scope
|
|
245
|
+
for scope in converted_guardrail.selector.scopes
|
|
246
|
+
if scope != GuardrailScope.TOOL
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
if non_tool_scopes:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"Deterministic guardrail '{converted_guardrail.name}' can only be used with TOOL scope. "
|
|
252
|
+
f"Found invalid scopes: {[scope.name for scope in non_tool_scopes]}. "
|
|
253
|
+
f"Please configure this guardrail to use only TOOL scope."
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
converted_guardrail = guardrail
|
|
257
|
+
_sanitize_selector_tool_names(converted_guardrail.selector)
|
|
236
258
|
|
|
237
259
|
action = guardrail.action
|
|
238
260
|
|
|
@@ -252,15 +274,18 @@ def build_guardrails_with_actions(
|
|
|
252
274
|
)
|
|
253
275
|
)
|
|
254
276
|
elif isinstance(action, AgentGuardrailEscalateAction):
|
|
255
|
-
|
|
256
|
-
(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
277
|
+
if isinstance(action.recipient, StandardRecipient):
|
|
278
|
+
result.append(
|
|
279
|
+
(
|
|
280
|
+
converted_guardrail,
|
|
281
|
+
EscalateAction(
|
|
282
|
+
app_name=action.app.name,
|
|
283
|
+
app_folder_path=action.app.folder_name,
|
|
284
|
+
version=action.app.version,
|
|
285
|
+
assignee=action.recipient.value,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
264
288
|
)
|
|
265
|
-
|
|
289
|
+
elif isinstance(action, AgentGuardrailFilterAction):
|
|
290
|
+
result.append((converted_guardrail, FilterAction(fields=action.fields)))
|
|
266
291
|
return result
|
|
@@ -10,8 +10,8 @@ from langchain_core.messages import (
|
|
|
10
10
|
ToolMessage,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
from uipath_langchain.agent.guardrails.types import ExecutionStage
|
|
14
13
|
from uipath_langchain.agent.react.types import AgentGuardrailsGraphState
|
|
14
|
+
from uipath_langchain.agent.tools.utils import sanitize_tool_name
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -60,45 +60,42 @@ def _extract_tool_args_from_message(
|
|
|
60
60
|
return parsed
|
|
61
61
|
except json.JSONDecodeError:
|
|
62
62
|
logger.warning(
|
|
63
|
-
"Failed to parse tool args as JSON for tool '%s'
|
|
64
|
-
tool_name,
|
|
65
|
-
args[:100] if len(args) > 100 else args,
|
|
63
|
+
"Failed to parse tool args as JSON for tool '%s'", tool_name
|
|
66
64
|
)
|
|
67
65
|
return {}
|
|
68
66
|
|
|
69
67
|
return {}
|
|
70
68
|
|
|
71
69
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""Extract tool call arguments as dict for deterministic guardrails.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
state: The current agent graph state.
|
|
79
|
-
tool_name: Name of the tool to extract arguments from.
|
|
80
|
-
execution_stage: PRE_EXECUTION or POST_EXECUTION.
|
|
70
|
+
def _extract_tools_args_from_message(message: AnyMessage) -> list[dict[str, Any]]:
|
|
71
|
+
if not isinstance(message, AIMessage):
|
|
72
|
+
return []
|
|
81
73
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- For PRE_EXECUTION: extracts from last message
|
|
85
|
-
- For POST_EXECUTION: extracts from second-to-last message
|
|
86
|
-
"""
|
|
87
|
-
if not state.messages:
|
|
88
|
-
return {}
|
|
74
|
+
if not message.tool_calls:
|
|
75
|
+
return []
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
# For POST_EXECUTION, look at second-to-last message (before the ToolMessage)
|
|
92
|
-
if execution_stage == ExecutionStage.PRE_EXECUTION:
|
|
93
|
-
if len(state.messages) < 1:
|
|
94
|
-
return {}
|
|
95
|
-
message = state.messages[-1]
|
|
96
|
-
else: # POST_EXECUTION
|
|
97
|
-
if len(state.messages) < 2:
|
|
98
|
-
return {}
|
|
99
|
-
message = state.messages[-2]
|
|
77
|
+
result: list[dict[str, Any]] = []
|
|
100
78
|
|
|
101
|
-
|
|
79
|
+
for tool_call in message.tool_calls:
|
|
80
|
+
args = (
|
|
81
|
+
tool_call.get("args")
|
|
82
|
+
if isinstance(tool_call, dict)
|
|
83
|
+
else getattr(tool_call, "args", None)
|
|
84
|
+
)
|
|
85
|
+
if args is not None:
|
|
86
|
+
# Args should already be a dict
|
|
87
|
+
if isinstance(args, dict):
|
|
88
|
+
result.append(args)
|
|
89
|
+
# If it's a JSON string, parse it
|
|
90
|
+
elif isinstance(args, str):
|
|
91
|
+
try:
|
|
92
|
+
parsed = json.loads(args)
|
|
93
|
+
if isinstance(parsed, dict):
|
|
94
|
+
result.append(parsed)
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
logger.warning("Failed to parse tool args as JSON")
|
|
97
|
+
|
|
98
|
+
return result
|
|
102
99
|
|
|
103
100
|
|
|
104
101
|
def _extract_tool_output_data(state: AgentGuardrailsGraphState) -> dict[str, Any]:
|
|
@@ -131,8 +128,18 @@ def _extract_tool_output_data(state: AgentGuardrailsGraphState) -> dict[str, Any
|
|
|
131
128
|
# JSON array or primitive - wrap it
|
|
132
129
|
return {"output": parsed}
|
|
133
130
|
except json.JSONDecodeError:
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
# Try to parse as Python literal (dict/list representation)
|
|
132
|
+
try:
|
|
133
|
+
import ast
|
|
134
|
+
|
|
135
|
+
parsed = ast.literal_eval(content)
|
|
136
|
+
if isinstance(parsed, dict):
|
|
137
|
+
return parsed
|
|
138
|
+
else:
|
|
139
|
+
return {"output": parsed}
|
|
140
|
+
except (ValueError, SyntaxError):
|
|
141
|
+
logger.warning("Tool output is not valid JSON or Python literal")
|
|
142
|
+
return {"output": content}
|
|
136
143
|
elif isinstance(content, dict):
|
|
137
144
|
return content
|
|
138
145
|
else:
|
|
@@ -144,3 +151,27 @@ def get_message_content(msg: AnyMessage) -> str:
|
|
|
144
151
|
if isinstance(msg, (HumanMessage, SystemMessage)):
|
|
145
152
|
return msg.content if isinstance(msg.content, str) else str(msg.content)
|
|
146
153
|
return str(getattr(msg, "content", "")) if hasattr(msg, "content") else ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _sanitize_selector_tool_names(selector):
|
|
157
|
+
"""Sanitize tool names in the selector's match_names for Tool scope guardrails.
|
|
158
|
+
|
|
159
|
+
This ensures that the tool names in the selector match the sanitized tool names
|
|
160
|
+
used in the actual tool nodes.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
selector: The guardrail selector object.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The selector with sanitized match_names (if applicable).
|
|
167
|
+
"""
|
|
168
|
+
from uipath.platform.guardrails import GuardrailScope
|
|
169
|
+
|
|
170
|
+
# Only sanitize for Tool scope guardrails
|
|
171
|
+
if GuardrailScope.TOOL in selector.scopes and selector.match_names is not None:
|
|
172
|
+
# Sanitize each tool name in match_names
|
|
173
|
+
sanitized_names = [sanitize_tool_name(name) for name in selector.match_names]
|
|
174
|
+
# Update the selector with sanitized names
|
|
175
|
+
selector.match_names = sanitized_names
|
|
176
|
+
|
|
177
|
+
return selector
|
|
@@ -10,7 +10,6 @@ from pydantic import BaseModel
|
|
|
10
10
|
from uipath.platform.guardrails import BaseGuardrail
|
|
11
11
|
|
|
12
12
|
from ..guardrails.actions import GuardrailAction
|
|
13
|
-
from ..tools import create_tool_node
|
|
14
13
|
from .guardrails.guardrails_subgraph import (
|
|
15
14
|
create_agent_init_guardrails_subgraph,
|
|
16
15
|
create_agent_terminate_guardrails_subgraph,
|
|
@@ -67,6 +66,8 @@ def create_agent(
|
|
|
67
66
|
|
|
68
67
|
Control flow tools (end_execution, raise_error) are auto-injected alongside regular tools.
|
|
69
68
|
"""
|
|
69
|
+
from ..tools import create_tool_node
|
|
70
|
+
|
|
70
71
|
if config is None:
|
|
71
72
|
config = AgentGraphConfig()
|
|
72
73
|
|
|
@@ -76,7 +77,8 @@ def create_agent(
|
|
|
76
77
|
flow_control_tools: list[BaseTool] = create_flow_control_tools(output_schema)
|
|
77
78
|
llm_tools: list[BaseTool] = [*agent_tools, *flow_control_tools]
|
|
78
79
|
|
|
79
|
-
init_node = create_init_node(messages)
|
|
80
|
+
init_node = create_init_node(messages, input_schema)
|
|
81
|
+
|
|
80
82
|
tool_nodes = create_tool_node(agent_tools)
|
|
81
83
|
tool_nodes_with_guardrails = create_tools_guardrails_subgraph(
|
|
82
84
|
tool_nodes, guardrails
|