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.
Files changed (41) hide show
  1. uipath_langchain/_cli/_templates/langgraph.json.template +2 -4
  2. uipath_langchain/_cli/cli_new.py +1 -2
  3. uipath_langchain/agent/guardrails/actions/escalate_action.py +252 -108
  4. uipath_langchain/agent/guardrails/actions/filter_action.py +247 -12
  5. uipath_langchain/agent/guardrails/guardrail_nodes.py +47 -12
  6. uipath_langchain/agent/guardrails/guardrails_factory.py +40 -15
  7. uipath_langchain/agent/guardrails/utils.py +64 -33
  8. uipath_langchain/agent/react/agent.py +4 -2
  9. uipath_langchain/agent/react/file_type_handler.py +123 -0
  10. uipath_langchain/agent/react/guardrails/guardrails_subgraph.py +67 -12
  11. uipath_langchain/agent/react/init_node.py +16 -1
  12. uipath_langchain/agent/react/job_attachments.py +125 -0
  13. uipath_langchain/agent/react/json_utils.py +183 -0
  14. uipath_langchain/agent/react/jsonschema_pydantic_converter.py +76 -0
  15. uipath_langchain/agent/react/llm_with_files.py +76 -0
  16. uipath_langchain/agent/react/types.py +4 -0
  17. uipath_langchain/agent/react/utils.py +29 -3
  18. uipath_langchain/agent/tools/__init__.py +5 -1
  19. uipath_langchain/agent/tools/context_tool.py +151 -1
  20. uipath_langchain/agent/tools/escalation_tool.py +46 -15
  21. uipath_langchain/agent/tools/integration_tool.py +20 -16
  22. uipath_langchain/agent/tools/internal_tools/__init__.py +5 -0
  23. uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py +113 -0
  24. uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py +54 -0
  25. uipath_langchain/agent/tools/process_tool.py +8 -1
  26. uipath_langchain/agent/tools/static_args.py +18 -40
  27. uipath_langchain/agent/tools/tool_factory.py +13 -5
  28. uipath_langchain/agent/tools/tool_node.py +133 -4
  29. uipath_langchain/agent/tools/utils.py +31 -0
  30. uipath_langchain/agent/wrappers/__init__.py +6 -0
  31. uipath_langchain/agent/wrappers/job_attachment_wrapper.py +62 -0
  32. uipath_langchain/agent/wrappers/static_args_wrapper.py +34 -0
  33. uipath_langchain/chat/mapper.py +60 -42
  34. uipath_langchain/runtime/factory.py +10 -5
  35. uipath_langchain/runtime/runtime.py +38 -35
  36. uipath_langchain/runtime/storage.py +178 -71
  37. {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/METADATA +5 -4
  38. {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/RECORD +41 -30
  39. {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/WHEEL +0 -0
  40. {uipath_langchain-0.1.34.dist-info → uipath_langchain-0.3.1.dist-info}/entry_points.txt +0 -0
  41. {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 now, filtering is only supported for non-AGENT and non-LLM scopes.
18
- If invoked for ``GuardrailScope.AGENT`` or ``GuardrailScope.LLM``, this action
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(_state: AgentGuardrailsGraphState) -> dict[str, Any]:
45
- if scope in (GuardrailScope.AGENT, GuardrailScope.LLM):
46
- raise AgentTerminationException(
47
- code=UiPathErrorCode.EXECUTION_ERROR,
48
- title="Guardrail filter action not supported",
49
- detail=f"FilterAction is not supported for scope [{scope.name}] at this time.",
50
- category=UiPathErrorCategory.USER,
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
- # No-op for other scopes for now.
53
- return {}
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
- _extract_tool_input_data,
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 that evaluates the guardrail and routes via Command:
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
- raise AgentTerminationException(
153
- code=UiPathErrorCode.EXECUTION_ERROR,
154
- title="Unsupported guardrail type",
155
- detail=f"Guardrail type '{type(guardrail).__name__}' is not supported. "
156
- f"Expected DeterministicGuardrail or BuiltInValidatorGuardrail.",
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
- return get_message_content(state.messages[-1])
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
- # Extract tool args as dict and convert to JSON string
267
- args_dict = _extract_tool_input_data(state, tool_name, execution_stage)
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
- return _extract_tool_input_data(state, tool_name, execution_stage)
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=guardrail.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
- # Convert AgentCustomGuardrail to DeterministicGuardrail
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
- result.append(
256
- (
257
- converted_guardrail,
258
- EscalateAction(
259
- app_name=action.app.name,
260
- app_folder_path=action.app.folder_name,
261
- version=action.app.version,
262
- assignee=action.recipient.value,
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': %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 _extract_tool_input_data(
73
- state: AgentGuardrailsGraphState, tool_name: str, execution_stage: ExecutionStage
74
- ) -> dict[str, Any]:
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
- Returns:
83
- Dict containing tool call arguments, or empty dict if not found.
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
- # For PRE_EXECUTION, look at last message
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
- return _extract_tool_args_from_message(message, tool_name)
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
- logger.warning("Tool output is not valid JSON")
135
- return {"output": content}
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