uipath-langchain 0.0.133__py3-none-any.whl → 0.1.28__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 +24 -0
- uipath_langchain/agent/guardrails/actions/block_action.py +42 -0
- uipath_langchain/agent/guardrails/actions/escalate_action.py +499 -0
- uipath_langchain/agent/guardrails/actions/log_action.py +58 -0
- uipath_langchain/agent/guardrails/guardrail_nodes.py +173 -0
- uipath_langchain/agent/guardrails/guardrails_factory.py +70 -0
- uipath_langchain/agent/guardrails/guardrails_subgraph.py +283 -0
- uipath_langchain/agent/guardrails/types.py +20 -0
- uipath_langchain/agent/react/__init__.py +14 -0
- uipath_langchain/agent/react/agent.py +117 -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/mapper.py +309 -0
- uipath_langchain/chat/models.py +248 -35
- uipath_langchain/chat/openai.py +133 -0
- uipath_langchain/chat/supported_models.py +42 -0
- uipath_langchain/chat/vertex.py +255 -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 +386 -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.28.dist-info}/METADATA +44 -23
- uipath_langchain-0.1.28.dist-info/RECORD +76 -0
- {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.28.dist-info}/WHEEL +1 -1
- uipath_langchain-0.1.28.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.28.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Graph loading utilities for LangGraph JSON configuration."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from langgraph.graph import StateGraph
|
|
12
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LangGraphLoader:
|
|
18
|
+
"""Loads a graph from a Python file path (e.g., 'agent.py:graph')."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str, file_path: str, variable_name: str):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the graph loader.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name: Human-readable name for the graph
|
|
26
|
+
file_path: Path to the Python file containing the graph
|
|
27
|
+
variable_name: Name of the variable/function in the file
|
|
28
|
+
"""
|
|
29
|
+
self.name = name
|
|
30
|
+
self.file_path = file_path
|
|
31
|
+
self.variable_name = variable_name
|
|
32
|
+
self._context_manager: Any = None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_path_string(cls, name: str, path: str) -> "LangGraphLoader":
|
|
36
|
+
"""
|
|
37
|
+
Create a GraphLoader from a path string like 'agent.py:graph'.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name: Human-readable name for the graph
|
|
41
|
+
path: Path string in format 'file_path:variable_name'
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
GraphLoader instance
|
|
45
|
+
"""
|
|
46
|
+
if ":" not in path:
|
|
47
|
+
raise ValueError(f"Invalid path format: {path}. Expected 'file:variable'")
|
|
48
|
+
|
|
49
|
+
file_path, variable_name = path.split(":", 1)
|
|
50
|
+
return cls(name=name, file_path=file_path, variable_name=variable_name)
|
|
51
|
+
|
|
52
|
+
async def load(
|
|
53
|
+
self,
|
|
54
|
+
) -> StateGraph[Any, Any, Any] | CompiledStateGraph[Any, Any, Any, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Load and return the graph.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
StateGraph or CompiledStateGraph instance
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If file path is outside current directory
|
|
63
|
+
FileNotFoundError: If file doesn't exist
|
|
64
|
+
ImportError: If module can't be loaded
|
|
65
|
+
TypeError: If loaded object isn't a valid graph
|
|
66
|
+
"""
|
|
67
|
+
# Validate and normalize paths
|
|
68
|
+
cwd = os.path.abspath(os.getcwd())
|
|
69
|
+
abs_file_path = os.path.abspath(os.path.normpath(self.file_path))
|
|
70
|
+
|
|
71
|
+
if not abs_file_path.startswith(cwd):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Graph file must be within current directory. Got: {self.file_path}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not os.path.exists(abs_file_path):
|
|
77
|
+
raise FileNotFoundError(f"Graph file not found: {abs_file_path}")
|
|
78
|
+
|
|
79
|
+
# Ensure current directory and src/ are in sys.path
|
|
80
|
+
self._setup_python_path(cwd)
|
|
81
|
+
|
|
82
|
+
# Import the module
|
|
83
|
+
module = self._import_module(abs_file_path)
|
|
84
|
+
|
|
85
|
+
# Get the graph object/function
|
|
86
|
+
graph_obj = getattr(module, self.variable_name, None)
|
|
87
|
+
if graph_obj is None:
|
|
88
|
+
raise AttributeError(
|
|
89
|
+
f"'{self.variable_name}' not found in {self.file_path}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Resolve the graph (handle functions, async functions, context managers)
|
|
93
|
+
graph = await self._resolve_graph(graph_obj)
|
|
94
|
+
|
|
95
|
+
# Validate it's a valid graph type
|
|
96
|
+
if not isinstance(graph, (StateGraph, CompiledStateGraph)):
|
|
97
|
+
raise TypeError(
|
|
98
|
+
f"Expected StateGraph or CompiledStateGraph, got {type(graph).__name__}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return graph
|
|
102
|
+
|
|
103
|
+
def _setup_python_path(self, cwd: str) -> None:
|
|
104
|
+
"""Add current directory and src/ to Python path if needed."""
|
|
105
|
+
if cwd not in sys.path:
|
|
106
|
+
sys.path.insert(0, cwd)
|
|
107
|
+
|
|
108
|
+
# Support src-layout projects (mimics editable install)
|
|
109
|
+
src_dir = os.path.join(cwd, "src")
|
|
110
|
+
if os.path.isdir(src_dir) and src_dir not in sys.path:
|
|
111
|
+
sys.path.insert(0, src_dir)
|
|
112
|
+
|
|
113
|
+
def _import_module(self, abs_file_path: str) -> Any:
|
|
114
|
+
"""Import a Python module from a file path."""
|
|
115
|
+
module_name = Path(abs_file_path).stem
|
|
116
|
+
spec = importlib.util.spec_from_file_location(module_name, abs_file_path)
|
|
117
|
+
|
|
118
|
+
if not spec or not spec.loader:
|
|
119
|
+
raise ImportError(f"Could not load module from: {abs_file_path}")
|
|
120
|
+
|
|
121
|
+
module = importlib.util.module_from_spec(spec)
|
|
122
|
+
sys.modules[module_name] = module
|
|
123
|
+
spec.loader.exec_module(module)
|
|
124
|
+
|
|
125
|
+
return module
|
|
126
|
+
|
|
127
|
+
async def _resolve_graph(
|
|
128
|
+
self, graph_obj: Any
|
|
129
|
+
) -> StateGraph[Any, Any, Any] | CompiledStateGraph[Any, Any, Any, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Resolve a graph object that might be:
|
|
132
|
+
- A direct StateGraph/CompiledStateGraph
|
|
133
|
+
- A function that returns a graph
|
|
134
|
+
- An async function that returns a graph
|
|
135
|
+
- An async context manager that yields a graph
|
|
136
|
+
"""
|
|
137
|
+
# Handle callable (function or async function)
|
|
138
|
+
if callable(graph_obj):
|
|
139
|
+
if inspect.iscoroutinefunction(graph_obj):
|
|
140
|
+
graph_obj = await graph_obj()
|
|
141
|
+
else:
|
|
142
|
+
graph_obj = graph_obj()
|
|
143
|
+
|
|
144
|
+
# Handle async context manager
|
|
145
|
+
if hasattr(graph_obj, "__aenter__") and callable(graph_obj.__aenter__):
|
|
146
|
+
self._context_manager = graph_obj
|
|
147
|
+
return await graph_obj.__aenter__()
|
|
148
|
+
|
|
149
|
+
return graph_obj
|
|
150
|
+
|
|
151
|
+
async def cleanup(self) -> None:
|
|
152
|
+
"""Clean up resources (e.g., exit async context managers)."""
|
|
153
|
+
if self._context_manager:
|
|
154
|
+
try:
|
|
155
|
+
await self._context_manager.__aexit__(None, None, None)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.warning(f"Error during graph cleanup: {e}")
|
|
158
|
+
finally:
|
|
159
|
+
self._context_manager = None
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, AsyncGenerator
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from langchain_core.runnables.config import RunnableConfig
|
|
7
|
+
from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError
|
|
8
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
9
|
+
from langgraph.types import Command, Interrupt, StateSnapshot
|
|
10
|
+
from uipath.runtime import (
|
|
11
|
+
UiPathBreakpointResult,
|
|
12
|
+
UiPathExecuteOptions,
|
|
13
|
+
UiPathRuntimeResult,
|
|
14
|
+
UiPathRuntimeStatus,
|
|
15
|
+
UiPathStreamOptions,
|
|
16
|
+
)
|
|
17
|
+
from uipath.runtime.errors import UiPathErrorCategory, UiPathErrorCode
|
|
18
|
+
from uipath.runtime.events import (
|
|
19
|
+
UiPathRuntimeEvent,
|
|
20
|
+
UiPathRuntimeMessageEvent,
|
|
21
|
+
UiPathRuntimeStateEvent,
|
|
22
|
+
)
|
|
23
|
+
from uipath.runtime.schema import UiPathRuntimeSchema
|
|
24
|
+
|
|
25
|
+
from uipath_langchain.chat import UiPathChatMessagesMapper
|
|
26
|
+
from uipath_langchain.runtime.errors import LangGraphErrorCode, LangGraphRuntimeError
|
|
27
|
+
from uipath_langchain.runtime.schema import get_entrypoints_schema, get_graph_schema
|
|
28
|
+
|
|
29
|
+
from ._serialize import serialize_output
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UiPathLangGraphRuntime:
|
|
35
|
+
"""
|
|
36
|
+
A runtime class for executing LangGraph graphs within the UiPath framework.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
graph: CompiledStateGraph[Any, Any, Any, Any],
|
|
42
|
+
runtime_id: str | None = None,
|
|
43
|
+
entrypoint: str | None = None,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the runtime.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
graph: The CompiledStateGraph to execute
|
|
50
|
+
runtime_id: Unique identifier for this runtime instance
|
|
51
|
+
entrypoint: Optional entrypoint name (for schema generation)
|
|
52
|
+
"""
|
|
53
|
+
self.graph: CompiledStateGraph[Any, Any, Any, Any] = graph
|
|
54
|
+
self.runtime_id: str = runtime_id or "default"
|
|
55
|
+
self.entrypoint: str | None = entrypoint
|
|
56
|
+
self.chat = UiPathChatMessagesMapper()
|
|
57
|
+
self._middleware_node_names: set[str] = self._detect_middleware_nodes()
|
|
58
|
+
|
|
59
|
+
async def execute(
|
|
60
|
+
self,
|
|
61
|
+
input: dict[str, Any] | None = None,
|
|
62
|
+
options: UiPathExecuteOptions | None = None,
|
|
63
|
+
) -> UiPathRuntimeResult:
|
|
64
|
+
"""Execute the graph with the provided input and configuration."""
|
|
65
|
+
try:
|
|
66
|
+
graph_input = await self._get_graph_input(input, options)
|
|
67
|
+
graph_config = self._get_graph_config()
|
|
68
|
+
|
|
69
|
+
# Execute without streaming
|
|
70
|
+
graph_output = await self.graph.ainvoke(
|
|
71
|
+
graph_input,
|
|
72
|
+
graph_config,
|
|
73
|
+
interrupt_before=options.breakpoints if options else None,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Get final state and create result
|
|
77
|
+
result = await self._create_runtime_result(graph_config, graph_output)
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise self._create_runtime_error(e) from e
|
|
83
|
+
|
|
84
|
+
async def stream(
|
|
85
|
+
self,
|
|
86
|
+
input: dict[str, Any] | None = None,
|
|
87
|
+
options: UiPathStreamOptions | None = None,
|
|
88
|
+
) -> AsyncGenerator[UiPathRuntimeEvent, None]:
|
|
89
|
+
"""
|
|
90
|
+
Stream graph execution events in real-time.
|
|
91
|
+
|
|
92
|
+
Yields UiPath UiPathRuntimeEvent instances (thin wrappers around framework data),
|
|
93
|
+
then yields the final UiPathRuntimeResult as the last item.
|
|
94
|
+
|
|
95
|
+
Yields:
|
|
96
|
+
- UiPathRuntimeMessageEvent: Wraps framework messages (BaseMessage, chunks, etc.)
|
|
97
|
+
- UiPathRuntimeStateEvent: Wraps framework state updates
|
|
98
|
+
- Final event: UiPathRuntimeResult or UiPathBreakpointResult
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
async for event in runtime.stream():
|
|
102
|
+
if isinstance(event, UiPathRuntimeResult):
|
|
103
|
+
# Last event is the result
|
|
104
|
+
print(f"Final result: {event}")
|
|
105
|
+
elif isinstance(event, UiPathRuntimeMessageEvent):
|
|
106
|
+
# Access framework-specific message
|
|
107
|
+
message = event.payload # BaseMessage or AIMessageChunk
|
|
108
|
+
print(f"Message: {message.content}")
|
|
109
|
+
elif isinstance(event, UiPathRuntimeStateEvent):
|
|
110
|
+
# Access framework-specific state
|
|
111
|
+
state = event.payload
|
|
112
|
+
print(f"Node {event.node_name} updated: {state}")
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
LangGraphRuntimeError: If execution fails
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
graph_input = await self._get_graph_input(input, options)
|
|
119
|
+
graph_config = self._get_graph_config()
|
|
120
|
+
|
|
121
|
+
# Track final chunk for result creation
|
|
122
|
+
final_chunk: dict[Any, Any] | None = None
|
|
123
|
+
|
|
124
|
+
# Stream events from graph
|
|
125
|
+
async for stream_chunk in self.graph.astream(
|
|
126
|
+
graph_input,
|
|
127
|
+
graph_config,
|
|
128
|
+
interrupt_before=options.breakpoints if options else None,
|
|
129
|
+
stream_mode=["messages", "updates"],
|
|
130
|
+
subgraphs=True,
|
|
131
|
+
):
|
|
132
|
+
_, chunk_type, data = stream_chunk
|
|
133
|
+
|
|
134
|
+
# Emit UiPathRuntimeMessageEvent for messages
|
|
135
|
+
if chunk_type == "messages":
|
|
136
|
+
if isinstance(data, tuple):
|
|
137
|
+
message, _ = data
|
|
138
|
+
event = UiPathRuntimeMessageEvent(
|
|
139
|
+
payload=self.chat.map_event(message),
|
|
140
|
+
)
|
|
141
|
+
yield event
|
|
142
|
+
|
|
143
|
+
# Emit UiPathRuntimeStateEvent for state updates
|
|
144
|
+
elif chunk_type == "updates":
|
|
145
|
+
if isinstance(data, dict):
|
|
146
|
+
filtered_data = {
|
|
147
|
+
node_name: agent_data
|
|
148
|
+
for node_name, agent_data in data.items()
|
|
149
|
+
if not self._is_middleware_node(node_name)
|
|
150
|
+
}
|
|
151
|
+
if filtered_data:
|
|
152
|
+
final_chunk = filtered_data
|
|
153
|
+
|
|
154
|
+
# Emit state update event for each node
|
|
155
|
+
for node_name, agent_data in data.items():
|
|
156
|
+
if isinstance(agent_data, dict):
|
|
157
|
+
state_event = UiPathRuntimeStateEvent(
|
|
158
|
+
payload=serialize_output(agent_data),
|
|
159
|
+
node_name=node_name,
|
|
160
|
+
)
|
|
161
|
+
yield state_event
|
|
162
|
+
|
|
163
|
+
# Extract output from final chunk
|
|
164
|
+
graph_output = self._extract_graph_result(final_chunk)
|
|
165
|
+
|
|
166
|
+
# Get final state and create result
|
|
167
|
+
result = await self._create_runtime_result(graph_config, graph_output)
|
|
168
|
+
|
|
169
|
+
# Yield the final result as last event
|
|
170
|
+
yield result
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
raise self._create_runtime_error(e) from e
|
|
174
|
+
|
|
175
|
+
async def get_schema(self) -> UiPathRuntimeSchema:
|
|
176
|
+
"""Get schema for this LangGraph runtime."""
|
|
177
|
+
schema_details = get_entrypoints_schema(self.graph)
|
|
178
|
+
|
|
179
|
+
return UiPathRuntimeSchema(
|
|
180
|
+
filePath=self.entrypoint,
|
|
181
|
+
uniqueId=str(uuid4()),
|
|
182
|
+
type="agent",
|
|
183
|
+
input=schema_details.schema["input"],
|
|
184
|
+
output=schema_details.schema["output"],
|
|
185
|
+
graph=get_graph_schema(self.graph, xray=1),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _get_graph_config(self) -> RunnableConfig:
|
|
189
|
+
"""Build graph execution configuration."""
|
|
190
|
+
graph_config: RunnableConfig = {
|
|
191
|
+
"configurable": {"thread_id": self.runtime_id},
|
|
192
|
+
"callbacks": [],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Add optional config from environment
|
|
196
|
+
recursion_limit = os.environ.get("LANGCHAIN_RECURSION_LIMIT")
|
|
197
|
+
max_concurrency = os.environ.get("LANGCHAIN_MAX_CONCURRENCY")
|
|
198
|
+
|
|
199
|
+
if recursion_limit is not None:
|
|
200
|
+
graph_config["recursion_limit"] = int(recursion_limit)
|
|
201
|
+
if max_concurrency is not None:
|
|
202
|
+
graph_config["max_concurrency"] = int(max_concurrency)
|
|
203
|
+
|
|
204
|
+
return graph_config
|
|
205
|
+
|
|
206
|
+
async def _get_graph_input(
|
|
207
|
+
self,
|
|
208
|
+
input: dict[str, Any] | None,
|
|
209
|
+
options: UiPathExecuteOptions | None,
|
|
210
|
+
) -> Any:
|
|
211
|
+
"""Process and return graph input."""
|
|
212
|
+
graph_input = input or {}
|
|
213
|
+
if isinstance(graph_input, dict):
|
|
214
|
+
messages = graph_input.get("messages", None)
|
|
215
|
+
if messages and isinstance(messages, list):
|
|
216
|
+
graph_input["messages"] = self.chat.map_messages(messages)
|
|
217
|
+
if options and options.resume:
|
|
218
|
+
return Command(resume=graph_input)
|
|
219
|
+
return graph_input
|
|
220
|
+
|
|
221
|
+
async def _get_graph_state(
|
|
222
|
+
self,
|
|
223
|
+
graph_config: RunnableConfig,
|
|
224
|
+
) -> StateSnapshot | None:
|
|
225
|
+
"""Get final graph state."""
|
|
226
|
+
try:
|
|
227
|
+
return await self.graph.aget_state(graph_config)
|
|
228
|
+
except Exception:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def _extract_graph_result(self, final_chunk: Any) -> Any:
|
|
232
|
+
"""
|
|
233
|
+
Extract the result from a LangGraph output chunk according to the graph's output channels.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
final_chunk: The final chunk from graph.astream()
|
|
237
|
+
output_channels: The graph's output channel configuration
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
The extracted result according to the graph's output_channels configuration
|
|
241
|
+
"""
|
|
242
|
+
# Unwrap from subgraph tuple format if needed
|
|
243
|
+
if isinstance(final_chunk, tuple) and len(final_chunk) == 2:
|
|
244
|
+
final_chunk = final_chunk[1]
|
|
245
|
+
|
|
246
|
+
# If the result isn't a dict or graph doesn't define output channels, return as is
|
|
247
|
+
if not isinstance(final_chunk, dict):
|
|
248
|
+
return final_chunk
|
|
249
|
+
|
|
250
|
+
output_channels = self.graph.output_channels
|
|
251
|
+
|
|
252
|
+
# Case 1: Single output channel as string
|
|
253
|
+
if isinstance(output_channels, str):
|
|
254
|
+
return final_chunk.get(output_channels, final_chunk)
|
|
255
|
+
|
|
256
|
+
# Case 2: Multiple output channels as sequence
|
|
257
|
+
elif hasattr(output_channels, "__iter__") and not isinstance(
|
|
258
|
+
output_channels, str
|
|
259
|
+
):
|
|
260
|
+
# Check which channels are present
|
|
261
|
+
available_channels = [ch for ch in output_channels if ch in final_chunk]
|
|
262
|
+
|
|
263
|
+
# If no available channels, output may contain the last_node name as key
|
|
264
|
+
unwrapped_final_chunk = {}
|
|
265
|
+
if not available_channels and len(final_chunk) == 1:
|
|
266
|
+
potential_unwrap = next(iter(final_chunk.values()))
|
|
267
|
+
if isinstance(potential_unwrap, dict):
|
|
268
|
+
unwrapped_final_chunk = potential_unwrap
|
|
269
|
+
available_channels = [
|
|
270
|
+
ch for ch in output_channels if ch in unwrapped_final_chunk
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
if available_channels:
|
|
274
|
+
# Create a dict with the available channels
|
|
275
|
+
return {
|
|
276
|
+
channel: final_chunk.get(channel)
|
|
277
|
+
or unwrapped_final_chunk.get(channel)
|
|
278
|
+
for channel in available_channels
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Fallback for any other case
|
|
282
|
+
return final_chunk
|
|
283
|
+
|
|
284
|
+
def _is_interrupted(self, state: StateSnapshot) -> bool:
|
|
285
|
+
"""Check if execution was interrupted (static or dynamic)."""
|
|
286
|
+
# Check for static interrupts (interrupt_before/after)
|
|
287
|
+
if hasattr(state, "next") and state.next:
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
# Check for dynamic interrupts (interrupt() inside node)
|
|
291
|
+
if hasattr(state, "tasks"):
|
|
292
|
+
for task in state.tasks:
|
|
293
|
+
if hasattr(task, "interrupts") and task.interrupts:
|
|
294
|
+
return True
|
|
295
|
+
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
def _get_dynamic_interrupt(self, state: StateSnapshot) -> Interrupt | None:
|
|
299
|
+
"""Get the first dynamic interrupt if any."""
|
|
300
|
+
if not hasattr(state, "tasks"):
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
for task in state.tasks:
|
|
304
|
+
if hasattr(task, "interrupts") and task.interrupts:
|
|
305
|
+
for interrupt in task.interrupts:
|
|
306
|
+
if isinstance(interrupt, Interrupt):
|
|
307
|
+
return interrupt
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
async def _create_runtime_result(
|
|
311
|
+
self,
|
|
312
|
+
graph_config: RunnableConfig,
|
|
313
|
+
graph_output: Any,
|
|
314
|
+
) -> UiPathRuntimeResult:
|
|
315
|
+
"""
|
|
316
|
+
Get final graph state and create the execution result.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
graph_config: The graph execution configuration
|
|
320
|
+
graph_output: The graph execution output
|
|
321
|
+
"""
|
|
322
|
+
# Get the final state
|
|
323
|
+
graph_state = await self._get_graph_state(graph_config)
|
|
324
|
+
|
|
325
|
+
# Check if execution was interrupted (static or dynamic)
|
|
326
|
+
if graph_state and self._is_interrupted(graph_state):
|
|
327
|
+
return await self._create_suspended_result(graph_state)
|
|
328
|
+
else:
|
|
329
|
+
# Normal completion
|
|
330
|
+
return self._create_success_result(graph_output)
|
|
331
|
+
|
|
332
|
+
async def _create_suspended_result(
|
|
333
|
+
self,
|
|
334
|
+
graph_state: StateSnapshot,
|
|
335
|
+
) -> UiPathRuntimeResult:
|
|
336
|
+
"""Create result for suspended execution."""
|
|
337
|
+
# Check if it's a dynamic interrupt
|
|
338
|
+
dynamic_interrupt = self._get_dynamic_interrupt(graph_state)
|
|
339
|
+
|
|
340
|
+
if dynamic_interrupt:
|
|
341
|
+
# Dynamic interrupt - should create and save resume trigger
|
|
342
|
+
return UiPathRuntimeResult(
|
|
343
|
+
output=dynamic_interrupt.value,
|
|
344
|
+
status=UiPathRuntimeStatus.SUSPENDED,
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
# Static interrupt (breakpoint)
|
|
348
|
+
return self._create_breakpoint_result(graph_state)
|
|
349
|
+
|
|
350
|
+
def _create_breakpoint_result(
|
|
351
|
+
self,
|
|
352
|
+
graph_state: StateSnapshot,
|
|
353
|
+
) -> UiPathBreakpointResult:
|
|
354
|
+
"""Create result for execution paused at a breakpoint."""
|
|
355
|
+
|
|
356
|
+
# Get next nodes - these are the nodes that will execute when resumed
|
|
357
|
+
next_nodes = list(graph_state.next)
|
|
358
|
+
|
|
359
|
+
# Determine breakpoint type and node
|
|
360
|
+
if next_nodes:
|
|
361
|
+
# Breakpoint is BEFORE these nodes (interrupt_before)
|
|
362
|
+
breakpoint_type = "before"
|
|
363
|
+
breakpoint_node = next_nodes[0]
|
|
364
|
+
else:
|
|
365
|
+
# Breakpoint is AFTER the last executed node (interrupt_after)
|
|
366
|
+
# Get the last executed node from tasks
|
|
367
|
+
breakpoint_type = "after"
|
|
368
|
+
if graph_state.tasks:
|
|
369
|
+
# Tasks contain the nodes that just executed
|
|
370
|
+
# Get the last task's name
|
|
371
|
+
breakpoint_node = graph_state.tasks[-1].name
|
|
372
|
+
else:
|
|
373
|
+
# Fallback if no tasks (shouldn't happen)
|
|
374
|
+
breakpoint_node = "unknown"
|
|
375
|
+
|
|
376
|
+
return UiPathBreakpointResult(
|
|
377
|
+
breakpoint_node=breakpoint_node,
|
|
378
|
+
breakpoint_type=breakpoint_type,
|
|
379
|
+
current_state=serialize_output(graph_state.values),
|
|
380
|
+
next_nodes=next_nodes,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def _create_success_result(self, output: Any) -> UiPathRuntimeResult:
|
|
384
|
+
"""Create result for successful completion."""
|
|
385
|
+
return UiPathRuntimeResult(
|
|
386
|
+
output=serialize_output(output),
|
|
387
|
+
status=UiPathRuntimeStatus.SUCCESSFUL,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def _create_runtime_error(self, e: Exception) -> LangGraphRuntimeError:
|
|
391
|
+
"""Handle execution errors and create appropriate LangGraphRuntimeError."""
|
|
392
|
+
if isinstance(e, LangGraphRuntimeError):
|
|
393
|
+
return e
|
|
394
|
+
|
|
395
|
+
detail = f"Error: {str(e)}"
|
|
396
|
+
|
|
397
|
+
if isinstance(e, GraphRecursionError):
|
|
398
|
+
return LangGraphRuntimeError(
|
|
399
|
+
LangGraphErrorCode.GRAPH_LOAD_ERROR,
|
|
400
|
+
"Graph recursion limit exceeded",
|
|
401
|
+
detail,
|
|
402
|
+
UiPathErrorCategory.USER,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if isinstance(e, InvalidUpdateError):
|
|
406
|
+
return LangGraphRuntimeError(
|
|
407
|
+
LangGraphErrorCode.GRAPH_INVALID_UPDATE,
|
|
408
|
+
str(e),
|
|
409
|
+
detail,
|
|
410
|
+
UiPathErrorCategory.USER,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if isinstance(e, EmptyInputError):
|
|
414
|
+
return LangGraphRuntimeError(
|
|
415
|
+
LangGraphErrorCode.GRAPH_EMPTY_INPUT,
|
|
416
|
+
"The input data is empty",
|
|
417
|
+
detail,
|
|
418
|
+
UiPathErrorCategory.USER,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
return LangGraphRuntimeError(
|
|
422
|
+
UiPathErrorCode.EXECUTION_ERROR,
|
|
423
|
+
"Graph execution failed",
|
|
424
|
+
detail,
|
|
425
|
+
UiPathErrorCategory.USER,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _detect_middleware_nodes(self) -> set[str]:
|
|
429
|
+
"""
|
|
430
|
+
Detect middleware nodes by their naming pattern.
|
|
431
|
+
|
|
432
|
+
Middleware nodes always contain both:
|
|
433
|
+
1. "Middleware" in the name (by convention)
|
|
434
|
+
2. A dot "." separator (MiddlewareName.hook_name)
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Set of middleware node names
|
|
438
|
+
"""
|
|
439
|
+
middleware_nodes: set[str] = set()
|
|
440
|
+
|
|
441
|
+
for node_name in self.graph.nodes.keys():
|
|
442
|
+
if "." in node_name and "Middleware" in node_name:
|
|
443
|
+
middleware_nodes.add(node_name)
|
|
444
|
+
|
|
445
|
+
return middleware_nodes
|
|
446
|
+
|
|
447
|
+
def _is_middleware_node(self, node_name: str) -> bool:
|
|
448
|
+
"""Check if a node name represents a middleware node."""
|
|
449
|
+
return node_name in self._middleware_node_names
|
|
450
|
+
|
|
451
|
+
async def dispose(self) -> None:
|
|
452
|
+
"""Cleanup runtime resources."""
|
|
453
|
+
pass
|