uipath-langchain 0.0.133__py3-none-any.whl → 0.1.24__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 (83) hide show
  1. uipath_langchain/_cli/cli_init.py +130 -191
  2. uipath_langchain/_cli/cli_new.py +2 -3
  3. uipath_langchain/_resources/AGENTS.md +21 -0
  4. uipath_langchain/_resources/REQUIRED_STRUCTURE.md +92 -0
  5. uipath_langchain/_tracing/__init__.py +3 -2
  6. uipath_langchain/_tracing/_instrument_traceable.py +11 -12
  7. uipath_langchain/_utils/_request_mixin.py +327 -51
  8. uipath_langchain/_utils/_settings.py +2 -2
  9. uipath_langchain/agent/exceptions/__init__.py +6 -0
  10. uipath_langchain/agent/exceptions/exceptions.py +11 -0
  11. uipath_langchain/agent/guardrails/__init__.py +21 -0
  12. uipath_langchain/agent/guardrails/actions/__init__.py +11 -0
  13. uipath_langchain/agent/guardrails/actions/base_action.py +23 -0
  14. uipath_langchain/agent/guardrails/actions/block_action.py +41 -0
  15. uipath_langchain/agent/guardrails/actions/escalate_action.py +274 -0
  16. uipath_langchain/agent/guardrails/actions/log_action.py +57 -0
  17. uipath_langchain/agent/guardrails/guardrail_nodes.py +125 -0
  18. uipath_langchain/agent/guardrails/guardrails_factory.py +70 -0
  19. uipath_langchain/agent/guardrails/guardrails_subgraph.py +247 -0
  20. uipath_langchain/agent/guardrails/types.py +20 -0
  21. uipath_langchain/agent/react/__init__.py +14 -0
  22. uipath_langchain/agent/react/agent.py +113 -0
  23. uipath_langchain/agent/react/constants.py +2 -0
  24. uipath_langchain/agent/react/init_node.py +20 -0
  25. uipath_langchain/agent/react/llm_node.py +43 -0
  26. uipath_langchain/agent/react/router.py +97 -0
  27. uipath_langchain/agent/react/terminate_node.py +82 -0
  28. uipath_langchain/agent/react/tools/__init__.py +7 -0
  29. uipath_langchain/agent/react/tools/tools.py +50 -0
  30. uipath_langchain/agent/react/types.py +39 -0
  31. uipath_langchain/agent/react/utils.py +49 -0
  32. uipath_langchain/agent/tools/__init__.py +17 -0
  33. uipath_langchain/agent/tools/context_tool.py +53 -0
  34. uipath_langchain/agent/tools/escalation_tool.py +111 -0
  35. uipath_langchain/agent/tools/integration_tool.py +181 -0
  36. uipath_langchain/agent/tools/process_tool.py +49 -0
  37. uipath_langchain/agent/tools/static_args.py +138 -0
  38. uipath_langchain/agent/tools/structured_tool_with_output_type.py +14 -0
  39. uipath_langchain/agent/tools/tool_factory.py +45 -0
  40. uipath_langchain/agent/tools/tool_node.py +22 -0
  41. uipath_langchain/agent/tools/utils.py +11 -0
  42. uipath_langchain/chat/__init__.py +4 -0
  43. uipath_langchain/chat/bedrock.py +187 -0
  44. uipath_langchain/chat/gemini.py +330 -0
  45. uipath_langchain/chat/mapper.py +309 -0
  46. uipath_langchain/chat/models.py +248 -35
  47. uipath_langchain/chat/openai.py +132 -0
  48. uipath_langchain/chat/supported_models.py +42 -0
  49. uipath_langchain/embeddings/embeddings.py +131 -34
  50. uipath_langchain/middlewares.py +0 -6
  51. uipath_langchain/retrievers/context_grounding_retriever.py +7 -9
  52. uipath_langchain/runtime/__init__.py +36 -0
  53. uipath_langchain/runtime/_serialize.py +46 -0
  54. uipath_langchain/runtime/config.py +61 -0
  55. uipath_langchain/runtime/errors.py +43 -0
  56. uipath_langchain/runtime/factory.py +315 -0
  57. uipath_langchain/runtime/graph.py +159 -0
  58. uipath_langchain/runtime/runtime.py +453 -0
  59. uipath_langchain/runtime/schema.py +349 -0
  60. uipath_langchain/runtime/storage.py +115 -0
  61. uipath_langchain/vectorstores/context_grounding_vectorstore.py +90 -110
  62. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.24.dist-info}/METADATA +42 -22
  63. uipath_langchain-0.1.24.dist-info/RECORD +76 -0
  64. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.24.dist-info}/WHEEL +1 -1
  65. uipath_langchain-0.1.24.dist-info/entry_points.txt +5 -0
  66. uipath_langchain/_cli/_runtime/_context.py +0 -21
  67. uipath_langchain/_cli/_runtime/_conversation.py +0 -298
  68. uipath_langchain/_cli/_runtime/_exception.py +0 -17
  69. uipath_langchain/_cli/_runtime/_input.py +0 -139
  70. uipath_langchain/_cli/_runtime/_output.py +0 -234
  71. uipath_langchain/_cli/_runtime/_runtime.py +0 -379
  72. uipath_langchain/_cli/_utils/_graph.py +0 -199
  73. uipath_langchain/_cli/cli_dev.py +0 -44
  74. uipath_langchain/_cli/cli_eval.py +0 -78
  75. uipath_langchain/_cli/cli_run.py +0 -82
  76. uipath_langchain/_tracing/_oteladapter.py +0 -222
  77. uipath_langchain/_tracing/_utils.py +0 -28
  78. uipath_langchain/builder/agent_config.py +0 -191
  79. uipath_langchain/tools/preconfigured.py +0 -191
  80. uipath_langchain-0.0.133.dist-info/RECORD +0 -41
  81. uipath_langchain-0.0.133.dist-info/entry_points.txt +0 -2
  82. /uipath_langchain/{tools/__init__.py → py.typed} +0 -0
  83. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.24.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,247 @@
1
+ from typing import Any, Callable, Sequence
2
+
3
+ from langgraph.constants import END, START
4
+ from langgraph.graph import StateGraph
5
+ from uipath.platform.guardrails import (
6
+ BaseGuardrail,
7
+ BuiltInValidatorGuardrail,
8
+ GuardrailScope,
9
+ )
10
+
11
+ from uipath_langchain.agent.guardrails.types import ExecutionStage
12
+
13
+ from .actions.base_action import GuardrailAction, GuardrailActionNode
14
+ from .guardrail_nodes import (
15
+ create_agent_guardrail_node,
16
+ create_llm_guardrail_node,
17
+ create_tool_guardrail_node,
18
+ )
19
+ from .types import AgentGuardrailsGraphState
20
+
21
+ _VALIDATOR_ALLOWED_STAGES = {
22
+ "prompt_injection": {ExecutionStage.PRE_EXECUTION},
23
+ "pii_detection": {ExecutionStage.PRE_EXECUTION, ExecutionStage.POST_EXECUTION},
24
+ }
25
+
26
+
27
+ def _filter_guardrails_by_stage(
28
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
29
+ stage: ExecutionStage,
30
+ ) -> list[tuple[BaseGuardrail, GuardrailAction]]:
31
+ """Filter guardrails that apply to a specific execution stage."""
32
+ filtered_guardrails = []
33
+ for guardrail, action in guardrails or []:
34
+ # Internal knowledge: Check against configured allowed stages
35
+ if (
36
+ isinstance(guardrail, BuiltInValidatorGuardrail)
37
+ and guardrail.validator_type in _VALIDATOR_ALLOWED_STAGES
38
+ and stage not in _VALIDATOR_ALLOWED_STAGES[guardrail.validator_type]
39
+ ):
40
+ continue
41
+ filtered_guardrails.append((guardrail, action))
42
+ return filtered_guardrails
43
+
44
+
45
+ def _create_guardrails_subgraph(
46
+ main_inner_node: tuple[str, Any],
47
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
48
+ scope: GuardrailScope,
49
+ execution_stages: Sequence[ExecutionStage],
50
+ node_factory: Callable[
51
+ [
52
+ BaseGuardrail,
53
+ ExecutionStage,
54
+ str, # success node name
55
+ str, # fail node name
56
+ ],
57
+ GuardrailActionNode,
58
+ ] = create_llm_guardrail_node,
59
+ ):
60
+ """Build a subgraph that enforces guardrails around an inner node.
61
+
62
+ The constructed graph conditionally includes pre- and/or post-execution guardrail
63
+ chains based on ``execution_stages``:
64
+ - If ``ExecutionStage.PRE_EXECUTION`` is included, the graph links
65
+ START -> first pre-guardrail node -> ... -> inner.
66
+ Otherwise, it directly links START -> inner.
67
+ - If ``ExecutionStage.POST_EXECUTION`` is included, the graph links
68
+ inner -> first post-guardrail node -> ... -> END.
69
+ Otherwise, it directly links inner -> END.
70
+
71
+ No static edges are added between guardrail nodes; each evaluation node routes
72
+ dynamically to its configured success/failure targets. Failure nodes are added
73
+ but not chained; they are expected to route via Command to the provided next node.
74
+ """
75
+ inner_name, inner_node = main_inner_node
76
+
77
+ subgraph = StateGraph(AgentGuardrailsGraphState)
78
+
79
+ subgraph.add_node(inner_name, inner_node)
80
+
81
+ # Add pre execution guardrail nodes
82
+ if ExecutionStage.PRE_EXECUTION in execution_stages:
83
+ pre_guardrails = _filter_guardrails_by_stage(
84
+ guardrails, ExecutionStage.PRE_EXECUTION
85
+ )
86
+ first_pre_exec_guardrail_node = _build_guardrail_node_chain(
87
+ subgraph,
88
+ pre_guardrails,
89
+ scope,
90
+ ExecutionStage.PRE_EXECUTION,
91
+ node_factory,
92
+ inner_name,
93
+ )
94
+ subgraph.add_edge(START, first_pre_exec_guardrail_node)
95
+ else:
96
+ subgraph.add_edge(START, inner_name)
97
+
98
+ # Add post execution guardrail nodes
99
+ if ExecutionStage.POST_EXECUTION in execution_stages:
100
+ post_guardrails = _filter_guardrails_by_stage(
101
+ guardrails, ExecutionStage.POST_EXECUTION
102
+ )
103
+ first_post_exec_guardrail_node = _build_guardrail_node_chain(
104
+ subgraph,
105
+ post_guardrails,
106
+ scope,
107
+ ExecutionStage.POST_EXECUTION,
108
+ node_factory,
109
+ END,
110
+ )
111
+ subgraph.add_edge(inner_name, first_post_exec_guardrail_node)
112
+ else:
113
+ subgraph.add_edge(inner_name, END)
114
+
115
+ return subgraph.compile()
116
+
117
+
118
+ def _build_guardrail_node_chain(
119
+ subgraph: StateGraph[AgentGuardrailsGraphState],
120
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
121
+ scope: GuardrailScope,
122
+ execution_stage: ExecutionStage,
123
+ node_factory: Callable[
124
+ [
125
+ BaseGuardrail,
126
+ ExecutionStage,
127
+ str, # success node name
128
+ str, # fail node name
129
+ ],
130
+ GuardrailActionNode,
131
+ ],
132
+ next_node: str,
133
+ ) -> str:
134
+ """Recursively build a chain of guardrail nodes in reverse order.
135
+
136
+ This function processes guardrails from last to first, creating a chain where:
137
+ - Each guardrail node evaluates the guardrail condition
138
+ - On success, it routes to the next guardrail node (or the final next_node)
139
+ - On failure, it routes to a failure node that either throws an error or continues to next_node
140
+
141
+ Args:
142
+ subgraph: The StateGraph to add nodes and edges to.
143
+ guardrails: Sequence of (guardrail, action) tuples to process. Processed in reverse.
144
+ scope: The scope of the guardrails (LLM, AGENT, or TOOL).
145
+ execution_stage: Whether this is "PreExecution" or "PostExecution" guardrails.
146
+ node_factory: Factory function to create guardrail evaluation nodes.
147
+ next_node: The node name to route to after all guardrails pass.
148
+
149
+ Returns:
150
+ The name of the first guardrail node in the chain (or next_node if no guardrails).
151
+ """
152
+ # Base case: no guardrails to process, return the next node directly
153
+ if not guardrails:
154
+ return next_node
155
+
156
+ guardrail, action = guardrails[-1]
157
+ remaining_guardrails = guardrails[:-1]
158
+
159
+ fail_node_name, fail_node = action.action_node(
160
+ guardrail=guardrail, scope=scope, execution_stage=execution_stage
161
+ )
162
+
163
+ # Create the guardrail evaluation node.
164
+ guardrail_node_name, guardrail_node = node_factory(
165
+ guardrail, execution_stage, next_node, fail_node_name
166
+ )
167
+
168
+ # Add both nodes to the subgraph
169
+ subgraph.add_node(guardrail_node_name, guardrail_node)
170
+ subgraph.add_node(fail_node_name, fail_node)
171
+
172
+ # Failure path route to the next node
173
+ subgraph.add_edge(fail_node_name, next_node)
174
+
175
+ previous_node_name = _build_guardrail_node_chain(
176
+ subgraph,
177
+ remaining_guardrails,
178
+ scope,
179
+ execution_stage,
180
+ node_factory,
181
+ guardrail_node_name,
182
+ )
183
+
184
+ return previous_node_name
185
+
186
+
187
+ def create_llm_guardrails_subgraph(
188
+ llm_node: tuple[str, Any],
189
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
190
+ ):
191
+ applicable_guardrails = [
192
+ (guardrail, _)
193
+ for (guardrail, _) in (guardrails or [])
194
+ if GuardrailScope.LLM in guardrail.selector.scopes
195
+ ]
196
+ return _create_guardrails_subgraph(
197
+ main_inner_node=llm_node,
198
+ guardrails=applicable_guardrails,
199
+ scope=GuardrailScope.LLM,
200
+ execution_stages=[ExecutionStage.PRE_EXECUTION, ExecutionStage.POST_EXECUTION],
201
+ node_factory=create_llm_guardrail_node,
202
+ )
203
+
204
+
205
+ def create_agent_guardrails_subgraph(
206
+ agent_node: tuple[str, Any],
207
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
208
+ execution_stage: ExecutionStage,
209
+ ):
210
+ """Create a subgraph for AGENT-scoped guardrails that applies checks at the specified stage.
211
+
212
+ This is intended for wrapping nodes like INIT or TERMINATE, where guardrails should run
213
+ either before (pre-execution) or after (post-execution) the node logic.
214
+ """
215
+ applicable_guardrails = [
216
+ (guardrail, _)
217
+ for (guardrail, _) in (guardrails or [])
218
+ if GuardrailScope.AGENT in guardrail.selector.scopes
219
+ ]
220
+ return _create_guardrails_subgraph(
221
+ main_inner_node=agent_node,
222
+ guardrails=applicable_guardrails,
223
+ scope=GuardrailScope.AGENT,
224
+ execution_stages=[execution_stage],
225
+ node_factory=create_agent_guardrail_node,
226
+ )
227
+
228
+
229
+ def create_tool_guardrails_subgraph(
230
+ tool_node: tuple[str, Any],
231
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
232
+ ):
233
+ tool_name, _ = tool_node
234
+ applicable_guardrails = [
235
+ (guardrail, action)
236
+ for (guardrail, action) in (guardrails or [])
237
+ if GuardrailScope.TOOL in guardrail.selector.scopes
238
+ and guardrail.selector.match_names is not None
239
+ and tool_name in guardrail.selector.match_names
240
+ ]
241
+ return _create_guardrails_subgraph(
242
+ main_inner_node=tool_node,
243
+ guardrails=applicable_guardrails,
244
+ scope=GuardrailScope.TOOL,
245
+ execution_stages=[ExecutionStage.PRE_EXECUTION, ExecutionStage.POST_EXECUTION],
246
+ node_factory=create_tool_guardrail_node,
247
+ )
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+ from typing import Annotated, Optional
3
+
4
+ from langchain_core.messages import AnyMessage
5
+ from langgraph.graph.message import add_messages
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class AgentGuardrailsGraphState(BaseModel):
10
+ """Agent Guardrails Graph state for guardrail subgraph."""
11
+
12
+ messages: Annotated[list[AnyMessage], add_messages] = []
13
+ guardrail_validation_result: Optional[str] = None
14
+
15
+
16
+ class ExecutionStage(str, Enum):
17
+ """Execution stage enumeration."""
18
+
19
+ PRE_EXECUTION = "preExecution"
20
+ POST_EXECUTION = "postExecution"
@@ -0,0 +1,14 @@
1
+ """UiPath ReAct Agent implementation"""
2
+
3
+ from .agent import create_agent
4
+ from .types import AgentGraphConfig, AgentGraphNode, AgentGraphState
5
+ from .utils import resolve_input_model, resolve_output_model
6
+
7
+ __all__ = [
8
+ "create_agent",
9
+ "resolve_output_model",
10
+ "resolve_input_model",
11
+ "AgentGraphNode",
12
+ "AgentGraphState",
13
+ "AgentGraphConfig",
14
+ ]
@@ -0,0 +1,113 @@
1
+ import os
2
+ from typing import Callable, Sequence, Type, TypeVar, cast
3
+
4
+ from langchain_core.language_models import BaseChatModel
5
+ from langchain_core.messages import HumanMessage, SystemMessage
6
+ from langchain_core.tools import BaseTool
7
+ from langgraph.constants import END, START
8
+ from langgraph.graph import StateGraph
9
+ from pydantic import BaseModel
10
+ from uipath.platform.guardrails import BaseGuardrail
11
+
12
+ from ..guardrails import create_llm_guardrails_subgraph
13
+ from ..guardrails.actions import GuardrailAction
14
+ from ..tools import create_tool_node
15
+ from .init_node import (
16
+ create_init_node,
17
+ )
18
+ from .llm_node import (
19
+ create_llm_node,
20
+ )
21
+ from .router import (
22
+ route_agent,
23
+ )
24
+ from .terminate_node import (
25
+ create_terminate_node,
26
+ )
27
+ from .tools import create_flow_control_tools
28
+ from .types import AgentGraphConfig, AgentGraphNode, AgentGraphState
29
+
30
+ InputT = TypeVar("InputT", bound=BaseModel)
31
+ OutputT = TypeVar("OutputT", bound=BaseModel)
32
+
33
+
34
+ def create_state_with_input(input_schema: Type[InputT]):
35
+ InnerAgentGraphState = type(
36
+ "InnerAgentGraphState",
37
+ (AgentGraphState, input_schema),
38
+ {},
39
+ )
40
+
41
+ cast(type[BaseModel], InnerAgentGraphState).model_rebuild()
42
+ return InnerAgentGraphState
43
+
44
+
45
+ def create_agent(
46
+ model: BaseChatModel,
47
+ tools: Sequence[BaseTool],
48
+ messages: Sequence[SystemMessage | HumanMessage]
49
+ | Callable[[InputT], Sequence[SystemMessage | HumanMessage]],
50
+ *,
51
+ input_schema: Type[InputT] | None = None,
52
+ output_schema: Type[OutputT] | None = None,
53
+ config: AgentGraphConfig | None = None,
54
+ guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None = None,
55
+ ) -> StateGraph[AgentGraphState, None, InputT, OutputT]:
56
+ """Build agent graph with INIT -> AGENT(subgraph) <-> TOOLS loop, terminated by control flow tools.
57
+
58
+ The AGENT node is a subgraph that runs:
59
+ - before-agent guardrail middlewares
60
+ - the LLM tool-executing node
61
+ - after-agent guardrail middlewares
62
+
63
+ Control flow tools (end_execution, raise_error) are auto-injected alongside regular tools.
64
+ """
65
+ if config is None:
66
+ config = AgentGraphConfig()
67
+
68
+ os.environ["LANGCHAIN_RECURSION_LIMIT"] = str(config.recursion_limit)
69
+
70
+ agent_tools = list(tools)
71
+ flow_control_tools: list[BaseTool] = create_flow_control_tools(output_schema)
72
+ llm_tools: list[BaseTool] = [*agent_tools, *flow_control_tools]
73
+
74
+ init_node = create_init_node(messages)
75
+ tool_nodes = create_tool_node(agent_tools)
76
+ terminate_node = create_terminate_node(output_schema)
77
+
78
+ InnerAgentGraphState = create_state_with_input(
79
+ input_schema if input_schema is not None else BaseModel
80
+ )
81
+
82
+ builder: StateGraph[AgentGraphState, None, InputT, OutputT] = StateGraph(
83
+ InnerAgentGraphState, input_schema=input_schema, output_schema=output_schema
84
+ )
85
+ builder.add_node(AgentGraphNode.INIT, init_node)
86
+
87
+ for tool_name, tool_node in tool_nodes.items():
88
+ builder.add_node(tool_name, tool_node)
89
+
90
+ builder.add_node(AgentGraphNode.TERMINATE, terminate_node)
91
+
92
+ builder.add_edge(START, AgentGraphNode.INIT)
93
+
94
+ llm_node = create_llm_node(model, llm_tools)
95
+ llm_with_guardrails_subgraph = create_llm_guardrails_subgraph(
96
+ (AgentGraphNode.LLM, llm_node), guardrails
97
+ )
98
+ builder.add_node(AgentGraphNode.AGENT, llm_with_guardrails_subgraph)
99
+ builder.add_edge(AgentGraphNode.INIT, AgentGraphNode.AGENT)
100
+
101
+ tool_node_names = list(tool_nodes.keys())
102
+ builder.add_conditional_edges(
103
+ AgentGraphNode.AGENT,
104
+ route_agent,
105
+ [AgentGraphNode.AGENT, *tool_node_names, AgentGraphNode.TERMINATE],
106
+ )
107
+
108
+ for tool_name in tool_node_names:
109
+ builder.add_edge(tool_name, AgentGraphNode.AGENT)
110
+
111
+ builder.add_edge(AgentGraphNode.TERMINATE, END)
112
+
113
+ return builder
@@ -0,0 +1,2 @@
1
+ # Agent routing configuration
2
+ MAX_SUCCESSIVE_COMPLETIONS = 1
@@ -0,0 +1,20 @@
1
+ """State initialization node for the ReAct Agent graph."""
2
+
3
+ from typing import Any, Callable, Sequence
4
+
5
+ from langchain_core.messages import HumanMessage, SystemMessage
6
+
7
+
8
+ def create_init_node(
9
+ messages: Sequence[SystemMessage | HumanMessage]
10
+ | Callable[[Any], Sequence[SystemMessage | HumanMessage]],
11
+ ):
12
+ def graph_state_init(state: Any):
13
+ if callable(messages):
14
+ resolved_messages = messages(state)
15
+ else:
16
+ resolved_messages = messages
17
+
18
+ return {"messages": list(resolved_messages)}
19
+
20
+ return graph_state_init
@@ -0,0 +1,43 @@
1
+ """LLM node implementation for LangGraph."""
2
+
3
+ from typing import Sequence
4
+
5
+ from langchain_core.language_models import BaseChatModel
6
+ from langchain_core.messages import AIMessage, AnyMessage
7
+ from langchain_core.tools import BaseTool
8
+
9
+ from .constants import MAX_SUCCESSIVE_COMPLETIONS
10
+ from .types import AgentGraphState
11
+ from .utils import count_successive_completions
12
+
13
+
14
+ def create_llm_node(
15
+ model: BaseChatModel,
16
+ tools: Sequence[BaseTool] | None = None,
17
+ ):
18
+ """Invoke LLM with tools and dynamically control tool_choice based on successive completions.
19
+
20
+ When successive completions reach the limit, tool_choice is set to "required" to force
21
+ the LLM to use a tool and prevent infinite reasoning loops.
22
+ """
23
+ bindable_tools = list(tools) if tools else []
24
+ base_llm = model.bind_tools(bindable_tools) if bindable_tools else model
25
+
26
+ async def llm_node(state: AgentGraphState):
27
+ messages: list[AnyMessage] = state.messages
28
+
29
+ successive_completions = count_successive_completions(messages)
30
+ if successive_completions >= MAX_SUCCESSIVE_COMPLETIONS:
31
+ llm = base_llm.bind(tool_choice="required")
32
+ else:
33
+ llm = base_llm
34
+
35
+ response = await llm.ainvoke(messages)
36
+ if not isinstance(response, AIMessage):
37
+ raise TypeError(
38
+ f"LLM returned {type(response).__name__} instead of AIMessage"
39
+ )
40
+
41
+ return {"messages": [response]}
42
+
43
+ return llm_node
@@ -0,0 +1,97 @@
1
+ """Routing functions for conditional edges in the agent graph."""
2
+
3
+ from typing import Literal
4
+
5
+ from langchain_core.messages import AIMessage, AnyMessage, ToolCall
6
+ from uipath.agent.react import END_EXECUTION_TOOL, RAISE_ERROR_TOOL
7
+
8
+ from ..exceptions import AgentNodeRoutingException
9
+ from .constants import MAX_SUCCESSIVE_COMPLETIONS
10
+ from .types import AgentGraphNode, AgentGraphState
11
+ from .utils import count_successive_completions
12
+
13
+ FLOW_CONTROL_TOOLS = [END_EXECUTION_TOOL.name, RAISE_ERROR_TOOL.name]
14
+
15
+
16
+ def __filter_control_flow_tool_calls(
17
+ tool_calls: list[ToolCall],
18
+ ) -> list[ToolCall]:
19
+ """Remove control flow tools when multiple tool calls exist."""
20
+ if len(tool_calls) <= 1:
21
+ return tool_calls
22
+
23
+ return [tc for tc in tool_calls if tc.get("name") not in FLOW_CONTROL_TOOLS]
24
+
25
+
26
+ def __has_control_flow_tool(tool_calls: list[ToolCall]) -> bool:
27
+ """Check if any tool call is of a control flow tool."""
28
+ return any(tc.get("name") in FLOW_CONTROL_TOOLS for tc in tool_calls)
29
+
30
+
31
+ def __validate_last_message_is_AI(messages: list[AnyMessage]) -> AIMessage:
32
+ """Validate and return last message from state.
33
+
34
+ Raises:
35
+ AgentNodeRoutingException: If messages are empty or last message is not AIMessage
36
+ """
37
+ if not messages:
38
+ raise AgentNodeRoutingException(
39
+ "No messages in state - cannot route after agent"
40
+ )
41
+
42
+ last_message = messages[-1]
43
+ if not isinstance(last_message, AIMessage):
44
+ raise AgentNodeRoutingException(
45
+ f"Last message is not AIMessage (type: {type(last_message).__name__}) - cannot route after agent"
46
+ )
47
+
48
+ return last_message
49
+
50
+
51
+ def route_agent(
52
+ state: AgentGraphState,
53
+ ) -> list[str] | Literal[AgentGraphNode.AGENT, AgentGraphNode.TERMINATE]:
54
+ """Route after agent: handles all routing logic including control flow detection.
55
+
56
+ Routing logic:
57
+ 1. If multiple tool calls exist, filter out control flow tools (EndExecution, RaiseError)
58
+ 2. If control flow tool(s) remain, route to TERMINATE
59
+ 3. If regular tool calls remain, route to specific tool nodes (return list of tool names)
60
+ 4. If no tool calls, handle successive completions
61
+
62
+ Returns:
63
+ - list[str]: Tool node names for parallel execution
64
+ - AgentGraphNode.AGENT: For successive completions
65
+ - AgentGraphNode.TERMINATE: For control flow termination
66
+
67
+ Raises:
68
+ AgentNodeRoutingException: When encountering unexpected state (empty messages, non-AIMessage, or excessive completions)
69
+ """
70
+ messages = state.messages
71
+ last_message = __validate_last_message_is_AI(messages)
72
+
73
+ tool_calls = list(last_message.tool_calls) if last_message.tool_calls else []
74
+ tool_calls = __filter_control_flow_tool_calls(tool_calls)
75
+
76
+ if tool_calls and __has_control_flow_tool(tool_calls):
77
+ return AgentGraphNode.TERMINATE
78
+
79
+ if tool_calls:
80
+ return [tc["name"] for tc in tool_calls]
81
+
82
+ successive_completions = count_successive_completions(messages)
83
+
84
+ if successive_completions > MAX_SUCCESSIVE_COMPLETIONS:
85
+ raise AgentNodeRoutingException(
86
+ f"Agent exceeded successive completions limit without producing tool calls "
87
+ f"(completions: {successive_completions}, max: {MAX_SUCCESSIVE_COMPLETIONS}). "
88
+ f"This should not happen as tool_choice='required' is enforced at the limit."
89
+ )
90
+
91
+ if last_message.content:
92
+ return AgentGraphNode.AGENT
93
+
94
+ raise AgentNodeRoutingException(
95
+ f"Agent produced empty response without tool calls "
96
+ f"(completions: {successive_completions}, has_content: False)"
97
+ )
@@ -0,0 +1,82 @@
1
+ """Termination node for the Agent graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, NoReturn
6
+
7
+ from langchain_core.messages import AIMessage
8
+ from pydantic import BaseModel
9
+ from uipath.agent.react import END_EXECUTION_TOOL, RAISE_ERROR_TOOL
10
+ from uipath.runtime.errors import UiPathErrorCode
11
+
12
+ from ..exceptions import (
13
+ AgentNodeRoutingException,
14
+ AgentTerminationException,
15
+ )
16
+ from .types import AgentGraphState, AgentTermination
17
+
18
+
19
+ def _handle_end_execution(
20
+ args: dict[str, Any], response_schema: type[BaseModel] | None
21
+ ) -> dict[str, Any]:
22
+ """Handle LLM-initiated termination via END_EXECUTION_TOOL."""
23
+ output_schema = response_schema or END_EXECUTION_TOOL.args_schema
24
+ validated = output_schema.model_validate(args)
25
+ return validated.model_dump()
26
+
27
+
28
+ def _handle_raise_error(args: dict[str, Any]) -> NoReturn:
29
+ """Handle LLM-initiated error via RAISE_ERROR_TOOL."""
30
+ error_message = args.get("message", "The LLM did not set the error message")
31
+ detail = args.get("details", "")
32
+ raise AgentTerminationException(
33
+ code=UiPathErrorCode.EXECUTION_ERROR,
34
+ title=error_message,
35
+ detail=detail,
36
+ )
37
+
38
+
39
+ def _handle_agent_termination(termination: AgentTermination) -> NoReturn:
40
+ """Handle Command-based termination."""
41
+ raise AgentTerminationException(
42
+ code=UiPathErrorCode.EXECUTION_ERROR,
43
+ title=termination.title,
44
+ detail=termination.detail,
45
+ )
46
+
47
+
48
+ def create_terminate_node(
49
+ response_schema: type[BaseModel] | None = None,
50
+ ):
51
+ """Handles Agent Graph termination for multiple sources and output or error propagation to Orchestrator.
52
+
53
+ Termination scenarios:
54
+ 1. Command based termination with information in state (e.g: escalation)
55
+ 2. LLM-initiated termination (END_EXECUTION_TOOL)
56
+ 3. LLM-initiated error (RAISE_ERROR_TOOL)
57
+ """
58
+
59
+ def terminate_node(state: AgentGraphState):
60
+ if state.termination:
61
+ _handle_agent_termination(state.termination)
62
+
63
+ last_message = state.messages[-1]
64
+ if not isinstance(last_message, AIMessage):
65
+ raise AgentNodeRoutingException(
66
+ f"Expected last message to be AIMessage, got {type(last_message).__name__}"
67
+ )
68
+
69
+ for tool_call in last_message.tool_calls:
70
+ tool_name = tool_call["name"]
71
+
72
+ if tool_name == END_EXECUTION_TOOL.name:
73
+ return _handle_end_execution(tool_call["args"], response_schema)
74
+
75
+ if tool_name == RAISE_ERROR_TOOL.name:
76
+ _handle_raise_error(tool_call["args"])
77
+
78
+ raise AgentNodeRoutingException(
79
+ "No control flow tool call found in terminate node. Unexpected state."
80
+ )
81
+
82
+ return terminate_node
@@ -0,0 +1,7 @@
1
+ from .tools import (
2
+ create_flow_control_tools,
3
+ )
4
+
5
+ __all__ = [
6
+ "create_flow_control_tools",
7
+ ]