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.
- uipath_langchain/_cli/cli_init.py +130 -191
- uipath_langchain/_cli/cli_new.py +2 -3
- uipath_langchain/_resources/AGENTS.md +21 -0
- uipath_langchain/_resources/REQUIRED_STRUCTURE.md +92 -0
- uipath_langchain/_tracing/__init__.py +3 -2
- uipath_langchain/_tracing/_instrument_traceable.py +11 -12
- uipath_langchain/_utils/_request_mixin.py +327 -51
- uipath_langchain/_utils/_settings.py +2 -2
- uipath_langchain/agent/exceptions/__init__.py +6 -0
- uipath_langchain/agent/exceptions/exceptions.py +11 -0
- uipath_langchain/agent/guardrails/__init__.py +21 -0
- uipath_langchain/agent/guardrails/actions/__init__.py +11 -0
- uipath_langchain/agent/guardrails/actions/base_action.py +23 -0
- uipath_langchain/agent/guardrails/actions/block_action.py +41 -0
- uipath_langchain/agent/guardrails/actions/escalate_action.py +274 -0
- uipath_langchain/agent/guardrails/actions/log_action.py +57 -0
- uipath_langchain/agent/guardrails/guardrail_nodes.py +125 -0
- uipath_langchain/agent/guardrails/guardrails_factory.py +70 -0
- uipath_langchain/agent/guardrails/guardrails_subgraph.py +247 -0
- uipath_langchain/agent/guardrails/types.py +20 -0
- uipath_langchain/agent/react/__init__.py +14 -0
- uipath_langchain/agent/react/agent.py +113 -0
- uipath_langchain/agent/react/constants.py +2 -0
- uipath_langchain/agent/react/init_node.py +20 -0
- uipath_langchain/agent/react/llm_node.py +43 -0
- uipath_langchain/agent/react/router.py +97 -0
- uipath_langchain/agent/react/terminate_node.py +82 -0
- uipath_langchain/agent/react/tools/__init__.py +7 -0
- uipath_langchain/agent/react/tools/tools.py +50 -0
- uipath_langchain/agent/react/types.py +39 -0
- uipath_langchain/agent/react/utils.py +49 -0
- uipath_langchain/agent/tools/__init__.py +17 -0
- uipath_langchain/agent/tools/context_tool.py +53 -0
- uipath_langchain/agent/tools/escalation_tool.py +111 -0
- uipath_langchain/agent/tools/integration_tool.py +181 -0
- uipath_langchain/agent/tools/process_tool.py +49 -0
- uipath_langchain/agent/tools/static_args.py +138 -0
- uipath_langchain/agent/tools/structured_tool_with_output_type.py +14 -0
- uipath_langchain/agent/tools/tool_factory.py +45 -0
- uipath_langchain/agent/tools/tool_node.py +22 -0
- uipath_langchain/agent/tools/utils.py +11 -0
- uipath_langchain/chat/__init__.py +4 -0
- uipath_langchain/chat/bedrock.py +187 -0
- uipath_langchain/chat/gemini.py +330 -0
- uipath_langchain/chat/mapper.py +309 -0
- uipath_langchain/chat/models.py +248 -35
- uipath_langchain/chat/openai.py +132 -0
- uipath_langchain/chat/supported_models.py +42 -0
- uipath_langchain/embeddings/embeddings.py +131 -34
- uipath_langchain/middlewares.py +0 -6
- uipath_langchain/retrievers/context_grounding_retriever.py +7 -9
- uipath_langchain/runtime/__init__.py +36 -0
- uipath_langchain/runtime/_serialize.py +46 -0
- uipath_langchain/runtime/config.py +61 -0
- uipath_langchain/runtime/errors.py +43 -0
- uipath_langchain/runtime/factory.py +315 -0
- uipath_langchain/runtime/graph.py +159 -0
- uipath_langchain/runtime/runtime.py +453 -0
- uipath_langchain/runtime/schema.py +349 -0
- uipath_langchain/runtime/storage.py +115 -0
- uipath_langchain/vectorstores/context_grounding_vectorstore.py +90 -110
- {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.24.dist-info}/METADATA +42 -22
- uipath_langchain-0.1.24.dist-info/RECORD +76 -0
- {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.24.dist-info}/WHEEL +1 -1
- uipath_langchain-0.1.24.dist-info/entry_points.txt +5 -0
- uipath_langchain/_cli/_runtime/_context.py +0 -21
- uipath_langchain/_cli/_runtime/_conversation.py +0 -298
- uipath_langchain/_cli/_runtime/_exception.py +0 -17
- uipath_langchain/_cli/_runtime/_input.py +0 -139
- uipath_langchain/_cli/_runtime/_output.py +0 -234
- uipath_langchain/_cli/_runtime/_runtime.py +0 -379
- uipath_langchain/_cli/_utils/_graph.py +0 -199
- uipath_langchain/_cli/cli_dev.py +0 -44
- uipath_langchain/_cli/cli_eval.py +0 -78
- uipath_langchain/_cli/cli_run.py +0 -82
- uipath_langchain/_tracing/_oteladapter.py +0 -222
- uipath_langchain/_tracing/_utils.py +0 -28
- uipath_langchain/builder/agent_config.py +0 -191
- uipath_langchain/tools/preconfigured.py +0 -191
- uipath_langchain-0.0.133.dist-info/RECORD +0 -41
- uipath_langchain-0.0.133.dist-info/entry_points.txt +0 -2
- /uipath_langchain/{tools/__init__.py → py.typed} +0 -0
- {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,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
|