uipath-langchain 0.0.140__py3-none-any.whl → 0.0.142__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.
Potentially problematic release.
This version of uipath-langchain might be problematic. Click here for more details.
- uipath_langchain/_cli/_runtime/_context.py +1 -7
- uipath_langchain/_cli/_runtime/_input.py +125 -117
- uipath_langchain/_cli/_runtime/_output.py +111 -197
- uipath_langchain/_cli/_runtime/_runtime.py +376 -150
- uipath_langchain/_cli/cli_debug.py +95 -0
- uipath_langchain/_cli/cli_init.py +134 -2
- uipath_langchain/_cli/cli_run.py +30 -17
- uipath_langchain/_resources/AGENTS.md +21 -0
- uipath_langchain/_resources/REQUIRED_STRUCTURE.md +92 -0
- uipath_langchain/middlewares.py +2 -0
- uipath_langchain/py.typed +0 -0
- uipath_langchain/tools/preconfigured.py +2 -2
- {uipath_langchain-0.0.140.dist-info → uipath_langchain-0.0.142.dist-info}/METADATA +2 -2
- {uipath_langchain-0.0.140.dist-info → uipath_langchain-0.0.142.dist-info}/RECORD +17 -13
- {uipath_langchain-0.0.140.dist-info → uipath_langchain-0.0.142.dist-info}/WHEEL +0 -0
- {uipath_langchain-0.0.140.dist-info → uipath_langchain-0.0.142.dist-info}/entry_points.txt +0 -0
- {uipath_langchain-0.0.140.dist-info → uipath_langchain-0.0.142.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
from
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import Any, AsyncGenerator, AsyncIterator, Optional, Sequence
|
|
4
5
|
|
|
5
|
-
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
6
|
-
from langchain_core.messages import BaseMessage
|
|
7
6
|
from langchain_core.runnables.config import RunnableConfig
|
|
8
7
|
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
9
8
|
from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError
|
|
9
|
+
from langgraph.graph.state import CompiledStateGraph, StateGraph
|
|
10
|
+
from langgraph.types import Interrupt, StateSnapshot
|
|
10
11
|
from uipath._cli._runtime._contracts import (
|
|
11
12
|
UiPathBaseRuntime,
|
|
13
|
+
UiPathBreakpointResult,
|
|
12
14
|
UiPathErrorCategory,
|
|
15
|
+
UiPathResumeTrigger,
|
|
13
16
|
UiPathRuntimeResult,
|
|
17
|
+
UiPathRuntimeStatus,
|
|
18
|
+
)
|
|
19
|
+
from uipath._events._events import (
|
|
20
|
+
UiPathAgentMessageEvent,
|
|
21
|
+
UiPathAgentStateEvent,
|
|
22
|
+
UiPathRuntimeEvent,
|
|
14
23
|
)
|
|
15
24
|
|
|
16
25
|
from ._context import LangGraphRuntimeContext
|
|
17
|
-
from ._conversation import map_message
|
|
18
26
|
from ._exception import LangGraphRuntimeError
|
|
19
27
|
from ._graph_resolver import AsyncResolver, LangGraphJsonResolver
|
|
20
|
-
from ._input import
|
|
21
|
-
from ._output import
|
|
28
|
+
from ._input import get_graph_input
|
|
29
|
+
from ._output import create_and_save_resume_trigger, serialize_output
|
|
22
30
|
|
|
23
31
|
logger = logging.getLogger(__name__)
|
|
24
32
|
|
|
@@ -33,171 +41,221 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
33
41
|
super().__init__(context)
|
|
34
42
|
self.context: LangGraphRuntimeContext = context
|
|
35
43
|
self.graph_resolver: AsyncResolver = graph_resolver
|
|
44
|
+
self.resume_triggers_table: str = "__uipath_resume_triggers"
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
@asynccontextmanager
|
|
47
|
+
async def _get_or_create_memory(self) -> AsyncIterator[AsyncSqliteSaver]:
|
|
38
48
|
"""
|
|
39
|
-
|
|
49
|
+
Get existing memory from context or create a new one.
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
If memory is created, it will be automatically disposed at the end.
|
|
52
|
+
If memory already exists in context, it will be reused without disposal.
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
Yields:
|
|
55
|
+
AsyncSqliteSaver instance
|
|
46
56
|
"""
|
|
57
|
+
# Check if memory already exists in context
|
|
58
|
+
if self.context.memory is not None:
|
|
59
|
+
# Use existing memory, don't dispose
|
|
60
|
+
yield self.context.memory
|
|
61
|
+
else:
|
|
62
|
+
# Create new memory and dispose at the end
|
|
63
|
+
async with AsyncSqliteSaver.from_conn_string(
|
|
64
|
+
self.state_file_path
|
|
65
|
+
) as memory:
|
|
66
|
+
yield memory
|
|
67
|
+
# Memory is automatically disposed by the context manager
|
|
47
68
|
|
|
69
|
+
async def execute(self) -> Optional[UiPathRuntimeResult]:
|
|
70
|
+
"""Execute the graph with the provided input and configuration."""
|
|
48
71
|
graph = await self.graph_resolver()
|
|
49
72
|
if not graph:
|
|
50
73
|
return None
|
|
51
74
|
|
|
52
75
|
try:
|
|
53
|
-
async with
|
|
54
|
-
self.
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
async with self._get_or_create_memory() as memory:
|
|
77
|
+
compiled_graph = await self._setup_graph(memory, graph)
|
|
78
|
+
graph_input = await self._get_graph_input(memory)
|
|
79
|
+
graph_config = self._get_graph_config()
|
|
57
80
|
|
|
58
|
-
#
|
|
59
|
-
|
|
81
|
+
# Execute without streaming
|
|
82
|
+
graph_output = await compiled_graph.ainvoke(graph_input, graph_config)
|
|
60
83
|
|
|
61
|
-
#
|
|
62
|
-
|
|
84
|
+
# Get final state and create result
|
|
85
|
+
self.context.result = await self._create_runtime_result(
|
|
86
|
+
compiled_graph, graph_config, memory, graph_output
|
|
87
|
+
)
|
|
63
88
|
|
|
64
|
-
|
|
89
|
+
return self.context.result
|
|
65
90
|
|
|
66
|
-
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise self._create_runtime_error(e) from e
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
async def stream(
|
|
95
|
+
self,
|
|
96
|
+
) -> AsyncGenerator[UiPathRuntimeEvent | UiPathRuntimeResult, None]:
|
|
97
|
+
"""
|
|
98
|
+
Stream graph execution events in real-time.
|
|
99
|
+
|
|
100
|
+
Yields UiPath UiPathRuntimeEvent instances (thin wrappers around framework data),
|
|
101
|
+
then yields the final UiPathRuntimeResult as the last item.
|
|
102
|
+
The result is also stored in self.context.result.
|
|
103
|
+
|
|
104
|
+
Yields:
|
|
105
|
+
- UiPathAgentMessageEvent: Wraps framework messages (BaseMessage, chunks, etc.)
|
|
106
|
+
- UiPathAgentStateEvent: Wraps framework state updates
|
|
107
|
+
- Final event: UiPathRuntimeResult or UiPathBreakpointResult
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
async for event in runtime.stream():
|
|
111
|
+
if isinstance(event, UiPathRuntimeResult):
|
|
112
|
+
# Last event is the result
|
|
113
|
+
print(f"Final result: {event}")
|
|
114
|
+
elif isinstance(event, UiPathAgentMessageEvent):
|
|
115
|
+
# Access framework-specific message
|
|
116
|
+
message = event.payload # BaseMessage or AIMessageChunk
|
|
117
|
+
print(f"Message: {message.content}")
|
|
118
|
+
elif isinstance(event, UiPathAgentStateEvent):
|
|
119
|
+
# Access framework-specific state
|
|
120
|
+
state = event.payload
|
|
121
|
+
print(f"Node {event.node_name} updated: {state}")
|
|
78
122
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
Raises:
|
|
124
|
+
LangGraphRuntimeError: If execution fails
|
|
125
|
+
"""
|
|
126
|
+
graph = await self.graph_resolver()
|
|
127
|
+
if not graph:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
async with self._get_or_create_memory() as memory:
|
|
132
|
+
compiled_graph = await self._setup_graph(memory, graph)
|
|
133
|
+
graph_input = await self._get_graph_input(memory)
|
|
134
|
+
graph_config = self._get_graph_config()
|
|
135
|
+
|
|
136
|
+
# Track final chunk for result creation
|
|
137
|
+
final_chunk: Optional[dict[Any, Any]] = None
|
|
138
|
+
|
|
139
|
+
# Stream events from graph
|
|
140
|
+
async for stream_chunk in compiled_graph.astream(
|
|
141
|
+
graph_input,
|
|
142
|
+
graph_config,
|
|
143
|
+
stream_mode=["messages", "updates"],
|
|
144
|
+
subgraphs=True,
|
|
145
|
+
):
|
|
146
|
+
_, chunk_type, data = stream_chunk
|
|
147
|
+
|
|
148
|
+
# Emit UiPathAgentMessageEvent for messages
|
|
149
|
+
if chunk_type == "messages":
|
|
150
|
+
if isinstance(data, tuple):
|
|
151
|
+
message, _ = data
|
|
152
|
+
event = UiPathAgentMessageEvent(
|
|
153
|
+
payload=message,
|
|
154
|
+
execution_id=self.context.execution_id,
|
|
155
|
+
)
|
|
156
|
+
yield event
|
|
157
|
+
|
|
158
|
+
# Emit UiPathAgentStateEvent for state updates
|
|
159
|
+
elif chunk_type == "updates":
|
|
160
|
+
if isinstance(data, dict):
|
|
161
|
+
final_chunk = data
|
|
162
|
+
|
|
163
|
+
# Emit state update event for each node
|
|
164
|
+
for node_name, agent_data in data.items():
|
|
165
|
+
if isinstance(agent_data, dict):
|
|
166
|
+
state_event = UiPathAgentStateEvent(
|
|
167
|
+
payload=agent_data,
|
|
168
|
+
node_name=node_name,
|
|
169
|
+
execution_id=self.context.execution_id,
|
|
104
170
|
)
|
|
105
|
-
|
|
106
|
-
self.context.chat_handler.on_event(event)
|
|
107
|
-
elif chunk_type == "updates":
|
|
108
|
-
if isinstance(data, dict):
|
|
109
|
-
# data is a dict, e.g. {'agent': {'messages': [...]}}
|
|
110
|
-
for agent_data in data.values():
|
|
111
|
-
if isinstance(agent_data, dict):
|
|
112
|
-
messages = agent_data.get("messages", [])
|
|
113
|
-
if isinstance(messages, list):
|
|
114
|
-
for message in messages:
|
|
115
|
-
if isinstance(message, BaseMessage):
|
|
116
|
-
message.pretty_print()
|
|
117
|
-
final_chunk = data
|
|
118
|
-
|
|
119
|
-
self.context.output = self._extract_graph_result(
|
|
120
|
-
final_chunk, compiled_graph.output_channels
|
|
121
|
-
)
|
|
122
|
-
else:
|
|
123
|
-
# Execute the graph normally at runtime or eval
|
|
124
|
-
self.context.output = await compiled_graph.ainvoke(
|
|
125
|
-
processed_input, graph_config
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
# Get the state if available
|
|
129
|
-
try:
|
|
130
|
-
self.context.state = await compiled_graph.aget_state(graph_config)
|
|
131
|
-
except Exception:
|
|
132
|
-
pass
|
|
133
|
-
|
|
134
|
-
output_processor = await LangGraphOutputProcessor.create(self.context)
|
|
135
|
-
|
|
136
|
-
self.context.result = await output_processor.process()
|
|
137
|
-
|
|
138
|
-
return self.context.result
|
|
171
|
+
yield state_event
|
|
139
172
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
detail = f"Error: {str(e)}"
|
|
145
|
-
|
|
146
|
-
if isinstance(e, GraphRecursionError):
|
|
147
|
-
raise LangGraphRuntimeError(
|
|
148
|
-
"GRAPH_RECURSION_ERROR",
|
|
149
|
-
"Graph recursion limit exceeded",
|
|
150
|
-
detail,
|
|
151
|
-
UiPathErrorCategory.USER,
|
|
152
|
-
) from e
|
|
153
|
-
|
|
154
|
-
if isinstance(e, InvalidUpdateError):
|
|
155
|
-
raise LangGraphRuntimeError(
|
|
156
|
-
"GRAPH_INVALID_UPDATE",
|
|
157
|
-
str(e),
|
|
158
|
-
detail,
|
|
159
|
-
UiPathErrorCategory.USER,
|
|
160
|
-
) from e
|
|
161
|
-
|
|
162
|
-
if isinstance(e, EmptyInputError):
|
|
163
|
-
raise LangGraphRuntimeError(
|
|
164
|
-
"GRAPH_EMPTY_INPUT",
|
|
165
|
-
"The input data is empty",
|
|
166
|
-
detail,
|
|
167
|
-
UiPathErrorCategory.USER,
|
|
168
|
-
) from e
|
|
169
|
-
|
|
170
|
-
raise LangGraphRuntimeError(
|
|
171
|
-
"EXECUTION_ERROR",
|
|
172
|
-
"Graph execution failed",
|
|
173
|
-
detail,
|
|
174
|
-
UiPathErrorCategory.USER,
|
|
175
|
-
) from e
|
|
176
|
-
finally:
|
|
177
|
-
pass
|
|
173
|
+
# Extract output from final chunk
|
|
174
|
+
graph_output = self._extract_graph_result(
|
|
175
|
+
final_chunk, compiled_graph.output_channels
|
|
176
|
+
)
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
# Get final state and create result
|
|
179
|
+
self.context.result = await self._create_runtime_result(
|
|
180
|
+
compiled_graph, graph_config, memory, graph_output
|
|
181
|
+
)
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
# Yield the final result as last event
|
|
184
|
+
yield self.context.result
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise self._create_runtime_error(e) from e
|
|
188
|
+
|
|
189
|
+
async def _setup_graph(
|
|
190
|
+
self, memory: AsyncSqliteSaver, graph: StateGraph[Any, Any, Any]
|
|
191
|
+
) -> CompiledStateGraph[Any, Any, Any]:
|
|
192
|
+
"""Setup and compile the graph with memory and interrupts."""
|
|
193
|
+
interrupt_before: list[str] = []
|
|
194
|
+
interrupt_after: list[str] = []
|
|
195
|
+
|
|
196
|
+
return graph.compile(
|
|
197
|
+
checkpointer=memory,
|
|
198
|
+
interrupt_before=interrupt_before,
|
|
199
|
+
interrupt_after=interrupt_after,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _get_graph_config(self) -> RunnableConfig:
|
|
203
|
+
"""Build graph execution configuration."""
|
|
204
|
+
graph_config: RunnableConfig = {
|
|
205
|
+
"configurable": {
|
|
206
|
+
"thread_id": (
|
|
207
|
+
self.context.execution_id or self.context.job_id or "default"
|
|
208
|
+
)
|
|
209
|
+
},
|
|
210
|
+
"callbacks": [],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Add optional config from environment
|
|
214
|
+
recursion_limit = os.environ.get("LANGCHAIN_RECURSION_LIMIT")
|
|
215
|
+
max_concurrency = os.environ.get("LANGCHAIN_MAX_CONCURRENCY")
|
|
216
|
+
|
|
217
|
+
if recursion_limit is not None:
|
|
218
|
+
graph_config["recursion_limit"] = int(recursion_limit)
|
|
219
|
+
if max_concurrency is not None:
|
|
220
|
+
graph_config["max_concurrency"] = int(max_concurrency)
|
|
221
|
+
|
|
222
|
+
return graph_config
|
|
223
|
+
|
|
224
|
+
async def _get_graph_input(self, memory: AsyncSqliteSaver) -> Any:
|
|
225
|
+
"""Process and return graph input."""
|
|
226
|
+
return await get_graph_input(
|
|
227
|
+
context=self.context,
|
|
228
|
+
memory=memory,
|
|
229
|
+
resume_triggers_table=self.resume_triggers_table,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def _get_graph_state(
|
|
233
|
+
self,
|
|
234
|
+
compiled_graph: CompiledStateGraph[Any, Any, Any],
|
|
235
|
+
graph_config: RunnableConfig,
|
|
236
|
+
) -> Optional[StateSnapshot]:
|
|
237
|
+
"""Get final graph state."""
|
|
238
|
+
try:
|
|
239
|
+
return await compiled_graph.aget_state(graph_config)
|
|
240
|
+
except Exception:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def _extract_graph_result(
|
|
244
|
+
self, final_chunk: Any, output_channels: str | Sequence[str]
|
|
245
|
+
) -> Any:
|
|
186
246
|
"""
|
|
187
247
|
Extract the result from a LangGraph output chunk according to the graph's output channels.
|
|
188
248
|
|
|
189
249
|
Args:
|
|
190
250
|
final_chunk: The final chunk from graph.astream()
|
|
191
|
-
|
|
251
|
+
output_channels: The graph's output channel configuration
|
|
192
252
|
|
|
193
253
|
Returns:
|
|
194
254
|
The extracted result according to the graph's output_channels configuration
|
|
195
255
|
"""
|
|
196
256
|
# Unwrap from subgraph tuple format if needed
|
|
197
257
|
if isinstance(final_chunk, tuple) and len(final_chunk) == 2:
|
|
198
|
-
final_chunk = final_chunk[
|
|
199
|
-
1
|
|
200
|
-
] # Extract data part from (namespace, data) tuple
|
|
258
|
+
final_chunk = final_chunk[1]
|
|
201
259
|
|
|
202
260
|
# If the result isn't a dict or graph doesn't define output channels, return as is
|
|
203
261
|
if not isinstance(final_chunk, dict):
|
|
@@ -205,10 +263,7 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
205
263
|
|
|
206
264
|
# Case 1: Single output channel as string
|
|
207
265
|
if isinstance(output_channels, str):
|
|
208
|
-
|
|
209
|
-
return final_chunk[output_channels]
|
|
210
|
-
else:
|
|
211
|
-
return final_chunk
|
|
266
|
+
return final_chunk.get(output_channels, final_chunk)
|
|
212
267
|
|
|
213
268
|
# Case 2: Multiple output channels as sequence
|
|
214
269
|
elif hasattr(output_channels, "__iter__") and not isinstance(
|
|
@@ -217,12 +272,12 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
217
272
|
# Check which channels are present
|
|
218
273
|
available_channels = [ch for ch in output_channels if ch in final_chunk]
|
|
219
274
|
|
|
220
|
-
#
|
|
275
|
+
# If no available channels, output may contain the last_node name as key
|
|
221
276
|
unwrapped_final_chunk = {}
|
|
222
|
-
if not available_channels:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
277
|
+
if not available_channels and len(final_chunk) == 1:
|
|
278
|
+
potential_unwrap = next(iter(final_chunk.values()))
|
|
279
|
+
if isinstance(potential_unwrap, dict):
|
|
280
|
+
unwrapped_final_chunk = potential_unwrap
|
|
226
281
|
available_channels = [
|
|
227
282
|
ch for ch in output_channels if ch in unwrapped_final_chunk
|
|
228
283
|
]
|
|
@@ -230,14 +285,184 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
230
285
|
if available_channels:
|
|
231
286
|
# Create a dict with the available channels
|
|
232
287
|
return {
|
|
233
|
-
channel: final_chunk.get(channel
|
|
234
|
-
or unwrapped_final_chunk
|
|
288
|
+
channel: final_chunk.get(channel)
|
|
289
|
+
or unwrapped_final_chunk.get(channel)
|
|
235
290
|
for channel in available_channels
|
|
236
291
|
}
|
|
237
292
|
|
|
238
293
|
# Fallback for any other case
|
|
239
294
|
return final_chunk
|
|
240
295
|
|
|
296
|
+
def _is_interrupted(self, state: StateSnapshot) -> bool:
|
|
297
|
+
"""Check if execution was interrupted (static or dynamic)."""
|
|
298
|
+
# Check for static interrupts (interrupt_before/after)
|
|
299
|
+
if hasattr(state, "next") and state.next:
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
# Check for dynamic interrupts (interrupt() inside node)
|
|
303
|
+
if hasattr(state, "tasks"):
|
|
304
|
+
for task in state.tasks:
|
|
305
|
+
if hasattr(task, "interrupts") and task.interrupts:
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def _get_dynamic_interrupt(self, state: StateSnapshot) -> Optional[Interrupt]:
|
|
311
|
+
"""Get the first dynamic interrupt if any."""
|
|
312
|
+
if not hasattr(state, "tasks"):
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
for task in state.tasks:
|
|
316
|
+
if hasattr(task, "interrupts") and task.interrupts:
|
|
317
|
+
for interrupt in task.interrupts:
|
|
318
|
+
if isinstance(interrupt, Interrupt):
|
|
319
|
+
return interrupt
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
async def _create_runtime_result(
|
|
323
|
+
self,
|
|
324
|
+
compiled_graph: CompiledStateGraph[Any, Any, Any],
|
|
325
|
+
graph_config: RunnableConfig,
|
|
326
|
+
memory: AsyncSqliteSaver,
|
|
327
|
+
graph_output: Optional[Any],
|
|
328
|
+
) -> UiPathRuntimeResult:
|
|
329
|
+
"""
|
|
330
|
+
Get final graph state and create the execution result.
|
|
331
|
+
|
|
332
|
+
Stores the result in self.context.result and self.context.state.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
compiled_graph: The compiled graph instance
|
|
336
|
+
graph_config: The graph execution configuration
|
|
337
|
+
memory: The SQLite memory instance
|
|
338
|
+
graph_output: The graph execution output
|
|
339
|
+
"""
|
|
340
|
+
# Get the final state
|
|
341
|
+
graph_state = await self._get_graph_state(compiled_graph, graph_config)
|
|
342
|
+
|
|
343
|
+
# Check if execution was interrupted (static or dynamic)
|
|
344
|
+
if graph_state and self._is_interrupted(graph_state):
|
|
345
|
+
return await self._create_suspended_result(
|
|
346
|
+
graph_state, memory, graph_output
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
# Normal completion
|
|
350
|
+
return self._create_success_result(graph_output)
|
|
351
|
+
|
|
352
|
+
async def _create_suspended_result(
|
|
353
|
+
self,
|
|
354
|
+
graph_state: StateSnapshot,
|
|
355
|
+
graph_memory: AsyncSqliteSaver,
|
|
356
|
+
graph_output: Optional[Any],
|
|
357
|
+
) -> UiPathRuntimeResult:
|
|
358
|
+
"""Create result for suspended execution."""
|
|
359
|
+
# Check if it's a dynamic interrupt
|
|
360
|
+
dynamic_interrupt = self._get_dynamic_interrupt(graph_state)
|
|
361
|
+
resume_trigger: Optional[UiPathResumeTrigger] = None
|
|
362
|
+
|
|
363
|
+
if dynamic_interrupt:
|
|
364
|
+
# Dynamic interrupt - create and save resume trigger
|
|
365
|
+
resume_trigger = await create_and_save_resume_trigger(
|
|
366
|
+
interrupt_value=dynamic_interrupt.value,
|
|
367
|
+
memory=graph_memory,
|
|
368
|
+
resume_triggers_table=self.resume_triggers_table,
|
|
369
|
+
)
|
|
370
|
+
output = serialize_output(graph_output)
|
|
371
|
+
return UiPathRuntimeResult(
|
|
372
|
+
output=output,
|
|
373
|
+
status=UiPathRuntimeStatus.SUSPENDED,
|
|
374
|
+
resume=resume_trigger,
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
# Static interrupt (breakpoint)
|
|
378
|
+
return self._create_breakpoint_result(graph_state)
|
|
379
|
+
|
|
380
|
+
def _create_breakpoint_result(
|
|
381
|
+
self,
|
|
382
|
+
graph_state: StateSnapshot,
|
|
383
|
+
) -> UiPathBreakpointResult:
|
|
384
|
+
"""Create result for execution paused at a breakpoint."""
|
|
385
|
+
|
|
386
|
+
# Get next nodes - these are the nodes that will execute when resumed
|
|
387
|
+
next_nodes = list(graph_state.next)
|
|
388
|
+
|
|
389
|
+
# Determine breakpoint type and node
|
|
390
|
+
if next_nodes:
|
|
391
|
+
# Breakpoint is BEFORE these nodes (interrupt_before)
|
|
392
|
+
breakpoint_type = "before"
|
|
393
|
+
breakpoint_node = next_nodes[0]
|
|
394
|
+
else:
|
|
395
|
+
# Breakpoint is AFTER the last executed node (interrupt_after)
|
|
396
|
+
# Get the last executed node from tasks
|
|
397
|
+
breakpoint_type = "after"
|
|
398
|
+
if graph_state.tasks:
|
|
399
|
+
# Tasks contain the nodes that just executed
|
|
400
|
+
# Get the last task's name
|
|
401
|
+
breakpoint_node = graph_state.tasks[-1].name
|
|
402
|
+
else:
|
|
403
|
+
# Fallback if no tasks (shouldn't happen)
|
|
404
|
+
breakpoint_node = "unknown"
|
|
405
|
+
|
|
406
|
+
return UiPathBreakpointResult(
|
|
407
|
+
breakpoint_node=breakpoint_node,
|
|
408
|
+
breakpoint_type=breakpoint_type,
|
|
409
|
+
current_state=graph_state.values,
|
|
410
|
+
next_nodes=next_nodes,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def _create_success_result(self, output: Optional[Any]) -> UiPathRuntimeResult:
|
|
414
|
+
"""Create result for successful completion."""
|
|
415
|
+
return UiPathRuntimeResult(
|
|
416
|
+
output=serialize_output(output),
|
|
417
|
+
status=UiPathRuntimeStatus.SUCCESSFUL,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def _create_runtime_error(self, e: Exception) -> LangGraphRuntimeError:
|
|
421
|
+
"""Handle execution errors and create appropriate LangGraphRuntimeError."""
|
|
422
|
+
if isinstance(e, LangGraphRuntimeError):
|
|
423
|
+
return e
|
|
424
|
+
|
|
425
|
+
detail = f"Error: {str(e)}"
|
|
426
|
+
|
|
427
|
+
if isinstance(e, GraphRecursionError):
|
|
428
|
+
return LangGraphRuntimeError(
|
|
429
|
+
"GRAPH_RECURSION_ERROR",
|
|
430
|
+
"Graph recursion limit exceeded",
|
|
431
|
+
detail,
|
|
432
|
+
UiPathErrorCategory.USER,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if isinstance(e, InvalidUpdateError):
|
|
436
|
+
return LangGraphRuntimeError(
|
|
437
|
+
"GRAPH_INVALID_UPDATE",
|
|
438
|
+
str(e),
|
|
439
|
+
detail,
|
|
440
|
+
UiPathErrorCategory.USER,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if isinstance(e, EmptyInputError):
|
|
444
|
+
return LangGraphRuntimeError(
|
|
445
|
+
"GRAPH_EMPTY_INPUT",
|
|
446
|
+
"The input data is empty",
|
|
447
|
+
detail,
|
|
448
|
+
UiPathErrorCategory.USER,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return LangGraphRuntimeError(
|
|
452
|
+
"EXECUTION_ERROR",
|
|
453
|
+
"Graph execution failed",
|
|
454
|
+
detail,
|
|
455
|
+
UiPathErrorCategory.USER,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
async def validate(self) -> None:
|
|
459
|
+
"""Validate runtime inputs."""
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
async def cleanup(self) -> None:
|
|
463
|
+
"""Cleanup runtime resources."""
|
|
464
|
+
pass
|
|
465
|
+
|
|
241
466
|
|
|
242
467
|
class LangGraphScriptRuntime(LangGraphRuntime):
|
|
243
468
|
"""
|
|
@@ -250,6 +475,7 @@ class LangGraphScriptRuntime(LangGraphRuntime):
|
|
|
250
475
|
self.resolver = LangGraphJsonResolver(entrypoint=entrypoint)
|
|
251
476
|
super().__init__(context, self.resolver)
|
|
252
477
|
|
|
253
|
-
async def cleanup(self):
|
|
478
|
+
async def cleanup(self) -> None:
|
|
479
|
+
"""Cleanup runtime resources including resolver."""
|
|
254
480
|
await super().cleanup()
|
|
255
481
|
await self.resolver.cleanup()
|