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.
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 +24 -0
  14. uipath_langchain/agent/guardrails/actions/block_action.py +42 -0
  15. uipath_langchain/agent/guardrails/actions/escalate_action.py +499 -0
  16. uipath_langchain/agent/guardrails/actions/log_action.py +58 -0
  17. uipath_langchain/agent/guardrails/guardrail_nodes.py +173 -0
  18. uipath_langchain/agent/guardrails/guardrails_factory.py +70 -0
  19. uipath_langchain/agent/guardrails/guardrails_subgraph.py +283 -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 +117 -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/mapper.py +309 -0
  45. uipath_langchain/chat/models.py +248 -35
  46. uipath_langchain/chat/openai.py +133 -0
  47. uipath_langchain/chat/supported_models.py +42 -0
  48. uipath_langchain/chat/vertex.py +255 -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 +386 -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.28.dist-info}/METADATA +44 -23
  63. uipath_langchain-0.1.28.dist-info/RECORD +76 -0
  64. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.28.dist-info}/WHEEL +1 -1
  65. uipath_langchain-0.1.28.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.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