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
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
5
|
-
|
|
6
|
-
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
7
|
-
from langchain_core.messages import BaseMessage
|
|
8
|
-
from langchain_core.runnables.config import RunnableConfig
|
|
9
|
-
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
10
|
-
from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError
|
|
11
|
-
from langgraph.graph.state import CompiledStateGraph
|
|
12
|
-
from uipath._cli._runtime._contracts import (
|
|
13
|
-
UiPathBaseRuntime,
|
|
14
|
-
UiPathErrorCategory,
|
|
15
|
-
UiPathRuntimeResult,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
from .._utils._graph import LangGraphConfig
|
|
19
|
-
from ._context import LangGraphRuntimeContext
|
|
20
|
-
from ._conversation import map_message
|
|
21
|
-
from ._exception import LangGraphRuntimeError
|
|
22
|
-
from ._input import LangGraphInputProcessor
|
|
23
|
-
from ._output import LangGraphOutputProcessor
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class LangGraphRuntime(UiPathBaseRuntime):
|
|
29
|
-
"""
|
|
30
|
-
A runtime class implementing the async context manager protocol.
|
|
31
|
-
This allows using the class with 'async with' statements.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
def __init__(self, context: LangGraphRuntimeContext):
|
|
35
|
-
super().__init__(context)
|
|
36
|
-
self.context: LangGraphRuntimeContext = context
|
|
37
|
-
|
|
38
|
-
async def execute(self) -> Optional[UiPathRuntimeResult]:
|
|
39
|
-
"""
|
|
40
|
-
Execute the graph with the provided input and configuration.
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
Dictionary with execution results
|
|
44
|
-
|
|
45
|
-
Raises:
|
|
46
|
-
LangGraphRuntimeError: If execution fails
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
if self.context.state_graph is None:
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
async with AsyncSqliteSaver.from_conn_string(
|
|
54
|
-
self.state_file_path
|
|
55
|
-
) as memory:
|
|
56
|
-
self.context.memory = memory
|
|
57
|
-
|
|
58
|
-
# Compile the graph with the checkpointer
|
|
59
|
-
graph = self.context.state_graph.compile(
|
|
60
|
-
checkpointer=self.context.memory
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
# Process input, handling resume if needed
|
|
64
|
-
input_processor = LangGraphInputProcessor(context=self.context)
|
|
65
|
-
|
|
66
|
-
processed_input = await input_processor.process()
|
|
67
|
-
|
|
68
|
-
callbacks: List[BaseCallbackHandler] = []
|
|
69
|
-
|
|
70
|
-
graph_config: RunnableConfig = {
|
|
71
|
-
"configurable": {
|
|
72
|
-
"thread_id": (
|
|
73
|
-
self.context.execution_id
|
|
74
|
-
or self.context.job_id
|
|
75
|
-
or "default"
|
|
76
|
-
)
|
|
77
|
-
},
|
|
78
|
-
"callbacks": callbacks,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
recursion_limit = os.environ.get("LANGCHAIN_RECURSION_LIMIT", None)
|
|
82
|
-
max_concurrency = os.environ.get("LANGCHAIN_MAX_CONCURRENCY", None)
|
|
83
|
-
|
|
84
|
-
if recursion_limit is not None:
|
|
85
|
-
graph_config["recursion_limit"] = int(recursion_limit)
|
|
86
|
-
if max_concurrency is not None:
|
|
87
|
-
graph_config["max_concurrency"] = int(max_concurrency)
|
|
88
|
-
|
|
89
|
-
if self.context.chat_handler:
|
|
90
|
-
async for stream_chunk in graph.astream(
|
|
91
|
-
processed_input,
|
|
92
|
-
graph_config,
|
|
93
|
-
stream_mode="messages",
|
|
94
|
-
subgraphs=True,
|
|
95
|
-
):
|
|
96
|
-
if not isinstance(stream_chunk, tuple) or len(stream_chunk) < 2:
|
|
97
|
-
continue
|
|
98
|
-
|
|
99
|
-
_, (message, _) = stream_chunk
|
|
100
|
-
event = map_message(
|
|
101
|
-
message=message,
|
|
102
|
-
conversation_id=self.context.execution_id,
|
|
103
|
-
exchange_id=self.context.execution_id,
|
|
104
|
-
)
|
|
105
|
-
if event:
|
|
106
|
-
self.context.chat_handler.on_event(event)
|
|
107
|
-
|
|
108
|
-
# Stream the output at debug time
|
|
109
|
-
elif self.is_debug_run():
|
|
110
|
-
# Get final chunk while streaming
|
|
111
|
-
final_chunk = None
|
|
112
|
-
async for stream_chunk in graph.astream(
|
|
113
|
-
processed_input,
|
|
114
|
-
graph_config,
|
|
115
|
-
stream_mode="updates",
|
|
116
|
-
subgraphs=True,
|
|
117
|
-
):
|
|
118
|
-
self._pretty_print(stream_chunk)
|
|
119
|
-
final_chunk = stream_chunk
|
|
120
|
-
|
|
121
|
-
self.context.output = self._extract_graph_result(final_chunk, graph)
|
|
122
|
-
else:
|
|
123
|
-
# Execute the graph normally at runtime or eval
|
|
124
|
-
self.context.output = await graph.ainvoke(
|
|
125
|
-
processed_input, graph_config
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
# Get the state if available
|
|
129
|
-
try:
|
|
130
|
-
self.context.state = await 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
|
|
139
|
-
|
|
140
|
-
except Exception as e:
|
|
141
|
-
if isinstance(e, LangGraphRuntimeError):
|
|
142
|
-
raise
|
|
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
|
|
178
|
-
|
|
179
|
-
async def validate(self) -> None:
|
|
180
|
-
"""Validate runtime inputs."""
|
|
181
|
-
"""Load and validate the graph configuration ."""
|
|
182
|
-
if self.context.langgraph_config is None:
|
|
183
|
-
self.context.langgraph_config = LangGraphConfig()
|
|
184
|
-
if not self.context.langgraph_config.exists:
|
|
185
|
-
raise LangGraphRuntimeError(
|
|
186
|
-
"CONFIG_MISSING",
|
|
187
|
-
"Invalid configuration",
|
|
188
|
-
"Failed to load configuration",
|
|
189
|
-
UiPathErrorCategory.DEPLOYMENT,
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
try:
|
|
193
|
-
self.context.langgraph_config.load_config()
|
|
194
|
-
except Exception as e:
|
|
195
|
-
raise LangGraphRuntimeError(
|
|
196
|
-
"CONFIG_INVALID",
|
|
197
|
-
"Invalid configuration",
|
|
198
|
-
f"Failed to load configuration: {str(e)}",
|
|
199
|
-
UiPathErrorCategory.DEPLOYMENT,
|
|
200
|
-
) from e
|
|
201
|
-
|
|
202
|
-
# Determine entrypoint if not provided
|
|
203
|
-
graphs = self.context.langgraph_config.graphs
|
|
204
|
-
if not self.context.entrypoint and len(graphs) == 1:
|
|
205
|
-
self.context.entrypoint = graphs[0].name
|
|
206
|
-
elif not self.context.entrypoint:
|
|
207
|
-
graph_names = ", ".join(g.name for g in graphs)
|
|
208
|
-
raise LangGraphRuntimeError(
|
|
209
|
-
"ENTRYPOINT_MISSING",
|
|
210
|
-
"Entrypoint required",
|
|
211
|
-
f"Multiple graphs available. Please specify one of: {graph_names}.",
|
|
212
|
-
UiPathErrorCategory.DEPLOYMENT,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Get the specified graph
|
|
216
|
-
self.graph_config = self.context.langgraph_config.get_graph(
|
|
217
|
-
self.context.entrypoint
|
|
218
|
-
)
|
|
219
|
-
if not self.graph_config:
|
|
220
|
-
raise LangGraphRuntimeError(
|
|
221
|
-
"GRAPH_NOT_FOUND",
|
|
222
|
-
"Graph not found",
|
|
223
|
-
f"Graph '{self.context.entrypoint}' not found.",
|
|
224
|
-
UiPathErrorCategory.DEPLOYMENT,
|
|
225
|
-
)
|
|
226
|
-
try:
|
|
227
|
-
loaded_graph = await self.graph_config.load_graph()
|
|
228
|
-
self.context.state_graph = (
|
|
229
|
-
loaded_graph.builder
|
|
230
|
-
if isinstance(loaded_graph, CompiledStateGraph)
|
|
231
|
-
else loaded_graph
|
|
232
|
-
)
|
|
233
|
-
except ImportError as e:
|
|
234
|
-
raise LangGraphRuntimeError(
|
|
235
|
-
"GRAPH_IMPORT_ERROR",
|
|
236
|
-
"Graph import failed",
|
|
237
|
-
f"Failed to import graph '{self.context.entrypoint}': {str(e)}",
|
|
238
|
-
UiPathErrorCategory.USER,
|
|
239
|
-
) from e
|
|
240
|
-
except TypeError as e:
|
|
241
|
-
raise LangGraphRuntimeError(
|
|
242
|
-
"GRAPH_TYPE_ERROR",
|
|
243
|
-
"Invalid graph type",
|
|
244
|
-
f"Graph '{self.context.entrypoint}' is not a valid StateGraph or CompiledStateGraph: {str(e)}",
|
|
245
|
-
UiPathErrorCategory.USER,
|
|
246
|
-
) from e
|
|
247
|
-
except ValueError as e:
|
|
248
|
-
raise LangGraphRuntimeError(
|
|
249
|
-
"GRAPH_VALUE_ERROR",
|
|
250
|
-
"Invalid graph value",
|
|
251
|
-
f"Invalid value in graph '{self.context.entrypoint}': {str(e)}",
|
|
252
|
-
UiPathErrorCategory.USER,
|
|
253
|
-
) from e
|
|
254
|
-
except Exception as e:
|
|
255
|
-
raise LangGraphRuntimeError(
|
|
256
|
-
"GRAPH_LOAD_ERROR",
|
|
257
|
-
"Failed to load graph",
|
|
258
|
-
f"Unexpected error loading graph '{self.context.entrypoint}': {str(e)}",
|
|
259
|
-
UiPathErrorCategory.USER,
|
|
260
|
-
) from e
|
|
261
|
-
|
|
262
|
-
async def cleanup(self):
|
|
263
|
-
if hasattr(self, "graph_config") and self.graph_config:
|
|
264
|
-
await self.graph_config.cleanup()
|
|
265
|
-
|
|
266
|
-
def _extract_graph_result(
|
|
267
|
-
self, final_chunk, graph: CompiledStateGraph[Any, Any, Any]
|
|
268
|
-
):
|
|
269
|
-
"""
|
|
270
|
-
Extract the result from a LangGraph output chunk according to the graph's output channels.
|
|
271
|
-
|
|
272
|
-
Args:
|
|
273
|
-
final_chunk: The final chunk from graph.astream()
|
|
274
|
-
graph: The LangGraph instance
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
The extracted result according to the graph's output_channels configuration
|
|
278
|
-
"""
|
|
279
|
-
# Unwrap from subgraph tuple format if needed
|
|
280
|
-
if isinstance(final_chunk, tuple) and len(final_chunk) == 2:
|
|
281
|
-
final_chunk = final_chunk[
|
|
282
|
-
1
|
|
283
|
-
] # Extract data part from (namespace, data) tuple
|
|
284
|
-
|
|
285
|
-
# If the result isn't a dict or graph doesn't define output channels, return as is
|
|
286
|
-
if not isinstance(final_chunk, dict) or not hasattr(graph, "output_channels"):
|
|
287
|
-
return final_chunk
|
|
288
|
-
|
|
289
|
-
output_channels = graph.output_channels
|
|
290
|
-
|
|
291
|
-
# Case 1: Single output channel as string
|
|
292
|
-
if isinstance(output_channels, str):
|
|
293
|
-
if output_channels in final_chunk:
|
|
294
|
-
return final_chunk[output_channels]
|
|
295
|
-
else:
|
|
296
|
-
return final_chunk
|
|
297
|
-
|
|
298
|
-
# Case 2: Multiple output channels as sequence
|
|
299
|
-
elif hasattr(output_channels, "__iter__") and not isinstance(
|
|
300
|
-
output_channels, str
|
|
301
|
-
):
|
|
302
|
-
# Check which channels are present
|
|
303
|
-
available_channels = [ch for ch in output_channels if ch in final_chunk]
|
|
304
|
-
|
|
305
|
-
# if no available channels, output may contain the last_node name as key
|
|
306
|
-
unwrapped_final_chunk = {}
|
|
307
|
-
if not available_channels:
|
|
308
|
-
if len(final_chunk) == 1 and isinstance(
|
|
309
|
-
unwrapped_final_chunk := next(iter(final_chunk.values())), dict
|
|
310
|
-
):
|
|
311
|
-
available_channels = [
|
|
312
|
-
ch for ch in output_channels if ch in unwrapped_final_chunk
|
|
313
|
-
]
|
|
314
|
-
|
|
315
|
-
if available_channels:
|
|
316
|
-
# Create a dict with the available channels
|
|
317
|
-
return {
|
|
318
|
-
channel: final_chunk.get(channel, None)
|
|
319
|
-
or unwrapped_final_chunk[channel]
|
|
320
|
-
for channel in available_channels
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
# Fallback for any other case
|
|
324
|
-
return final_chunk
|
|
325
|
-
|
|
326
|
-
def _pretty_print(self, stream_chunk: Union[Tuple[Any, Any], Dict[str, Any], Any]):
|
|
327
|
-
"""
|
|
328
|
-
Pretty print a chunk from a LangGraph stream with stream_mode="updates" and subgraphs=True.
|
|
329
|
-
|
|
330
|
-
Args:
|
|
331
|
-
stream_chunk: A tuple of (namespace, updates) from graph.astream()
|
|
332
|
-
"""
|
|
333
|
-
if not isinstance(stream_chunk, tuple) or len(stream_chunk) < 2:
|
|
334
|
-
return
|
|
335
|
-
|
|
336
|
-
node_namespace = ""
|
|
337
|
-
chunk_namespace = stream_chunk[0]
|
|
338
|
-
node_updates = stream_chunk[1]
|
|
339
|
-
|
|
340
|
-
# Extract namespace if available
|
|
341
|
-
if chunk_namespace and len(chunk_namespace) > 0:
|
|
342
|
-
node_namespace = chunk_namespace[0]
|
|
343
|
-
|
|
344
|
-
if not isinstance(node_updates, dict):
|
|
345
|
-
logger.info("Raw update: %s", node_updates)
|
|
346
|
-
return
|
|
347
|
-
|
|
348
|
-
# Process each node's updates
|
|
349
|
-
for node_name, node_result in node_updates.items():
|
|
350
|
-
# Log node identifier with appropriate namespace context
|
|
351
|
-
if node_namespace:
|
|
352
|
-
logger.info("[%s][%s]", node_namespace, node_name)
|
|
353
|
-
else:
|
|
354
|
-
logger.info("[%s]", node_name)
|
|
355
|
-
|
|
356
|
-
# Handle non-dict results
|
|
357
|
-
if not isinstance(node_result, dict):
|
|
358
|
-
logger.info("%s", node_result)
|
|
359
|
-
continue
|
|
360
|
-
|
|
361
|
-
# Process messages specially
|
|
362
|
-
messages = node_result.get("messages", [])
|
|
363
|
-
if isinstance(messages, list):
|
|
364
|
-
for message in messages:
|
|
365
|
-
if isinstance(message, BaseMessage):
|
|
366
|
-
message.pretty_print()
|
|
367
|
-
|
|
368
|
-
# Exclude "messages" from node_result and pretty-print the rest
|
|
369
|
-
metadata = {k: v for k, v in node_result.items() if k != "messages"}
|
|
370
|
-
if metadata:
|
|
371
|
-
try:
|
|
372
|
-
formatted_metadata = json.dumps(
|
|
373
|
-
metadata,
|
|
374
|
-
indent=2,
|
|
375
|
-
ensure_ascii=False,
|
|
376
|
-
)
|
|
377
|
-
logger.info("%s", formatted_metadata)
|
|
378
|
-
except (TypeError, ValueError):
|
|
379
|
-
pass
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import importlib.util
|
|
2
|
-
import inspect
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import sys
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any, Dict, List, Optional, Union, cast
|
|
10
|
-
|
|
11
|
-
from langgraph.graph import StateGraph
|
|
12
|
-
from langgraph.graph.state import CompiledStateGraph
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class GraphConfig:
|
|
19
|
-
name: str
|
|
20
|
-
path: str
|
|
21
|
-
file_path: str
|
|
22
|
-
graph_var: str
|
|
23
|
-
_graph: Optional[Union[StateGraph[Any, Any], CompiledStateGraph[Any, Any, Any]]] = (
|
|
24
|
-
None
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
@classmethod
|
|
28
|
-
def from_config(cls, name: str, path: str) -> "GraphConfig":
|
|
29
|
-
file_path, graph_var = path.split(":")
|
|
30
|
-
return cls(name=name, path=path, file_path=file_path, graph_var=graph_var)
|
|
31
|
-
|
|
32
|
-
async def load_graph(
|
|
33
|
-
self,
|
|
34
|
-
) -> Union[StateGraph[Any, Any], CompiledStateGraph[Any, Any, Any]]:
|
|
35
|
-
"""Load graph from the specified path"""
|
|
36
|
-
try:
|
|
37
|
-
cwd = os.path.abspath(os.getcwd())
|
|
38
|
-
abs_file_path = os.path.abspath(os.path.normpath(self.file_path))
|
|
39
|
-
|
|
40
|
-
if not abs_file_path.startswith(cwd):
|
|
41
|
-
raise ValueError(
|
|
42
|
-
f"Script path must be within the current directory. Found: {self.file_path}"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
if not os.path.exists(abs_file_path):
|
|
46
|
-
raise FileNotFoundError(f"Script not found: {abs_file_path}")
|
|
47
|
-
|
|
48
|
-
if cwd not in sys.path:
|
|
49
|
-
sys.path.insert(0, cwd)
|
|
50
|
-
|
|
51
|
-
# For src-layout projects, add src directory to sys.path
|
|
52
|
-
# This mimics an editable/dev install
|
|
53
|
-
src_dir = os.path.join(cwd, "src")
|
|
54
|
-
if os.path.isdir(src_dir) and src_dir not in sys.path:
|
|
55
|
-
sys.path.insert(0, src_dir)
|
|
56
|
-
|
|
57
|
-
module_name = Path(abs_file_path).stem
|
|
58
|
-
spec = importlib.util.spec_from_file_location(module_name, abs_file_path)
|
|
59
|
-
|
|
60
|
-
if not spec or not spec.loader:
|
|
61
|
-
raise ImportError(f"Could not load module from: {abs_file_path}")
|
|
62
|
-
|
|
63
|
-
module = importlib.util.module_from_spec(spec)
|
|
64
|
-
sys.modules[module_name] = module
|
|
65
|
-
spec.loader.exec_module(module)
|
|
66
|
-
|
|
67
|
-
graph = getattr(module, self.graph_var, None)
|
|
68
|
-
|
|
69
|
-
# Get the graph object or function
|
|
70
|
-
graph_obj = getattr(module, self.graph_var, None)
|
|
71
|
-
|
|
72
|
-
# Handle callable graph factory
|
|
73
|
-
if callable(graph_obj):
|
|
74
|
-
if inspect.iscoroutinefunction(graph_obj):
|
|
75
|
-
# Handle async function
|
|
76
|
-
try:
|
|
77
|
-
graph_obj = await graph_obj()
|
|
78
|
-
except RuntimeError as e:
|
|
79
|
-
raise e
|
|
80
|
-
else:
|
|
81
|
-
# Call regular function
|
|
82
|
-
graph_obj = graph_obj()
|
|
83
|
-
|
|
84
|
-
# Handle async context manager
|
|
85
|
-
if (
|
|
86
|
-
graph_obj is not None
|
|
87
|
-
and hasattr(graph_obj, "__aenter__")
|
|
88
|
-
and callable(graph_obj.__aenter__)
|
|
89
|
-
):
|
|
90
|
-
self._context_manager = graph_obj
|
|
91
|
-
graph = await graph_obj.__aenter__()
|
|
92
|
-
|
|
93
|
-
# No need for atexit registration - the calling code should
|
|
94
|
-
# maintain a reference to this object and call cleanup explicitly
|
|
95
|
-
|
|
96
|
-
else:
|
|
97
|
-
# Not a context manager, use directly
|
|
98
|
-
graph = graph_obj
|
|
99
|
-
|
|
100
|
-
if not isinstance(graph, (StateGraph, CompiledStateGraph)):
|
|
101
|
-
raise TypeError(
|
|
102
|
-
f"Expected StateGraph, CompiledStateGraph, or a callable returning one of these, got {type(graph)}"
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
self._graph = graph
|
|
106
|
-
return graph
|
|
107
|
-
|
|
108
|
-
except Exception as e:
|
|
109
|
-
logger.error(f"Failed to load graph {self.name}: {str(e)}")
|
|
110
|
-
raise
|
|
111
|
-
|
|
112
|
-
async def get_input_schema(self) -> Dict[str, Any]:
|
|
113
|
-
"""Extract input schema from graph"""
|
|
114
|
-
if not self._graph:
|
|
115
|
-
self._graph = await self.load_graph()
|
|
116
|
-
|
|
117
|
-
if hasattr(self._graph, "input_schema"):
|
|
118
|
-
return cast(dict[str, Any], self._graph.input_schema)
|
|
119
|
-
return {}
|
|
120
|
-
|
|
121
|
-
async def cleanup(self):
|
|
122
|
-
"""
|
|
123
|
-
Clean up resources when done with the graph.
|
|
124
|
-
This should be called when the graph is no longer needed.
|
|
125
|
-
"""
|
|
126
|
-
if hasattr(self, "_context_manager") and self._context_manager:
|
|
127
|
-
try:
|
|
128
|
-
await self._context_manager.__aexit__(None, None, None)
|
|
129
|
-
except Exception as e:
|
|
130
|
-
logger.warning(f"Error during context cleanup: {str(e)}")
|
|
131
|
-
finally:
|
|
132
|
-
self._context_manager = None
|
|
133
|
-
self._graph = None
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
class LangGraphConfig:
|
|
137
|
-
def __init__(self, config_path: str = "langgraph.json"):
|
|
138
|
-
self.config_path = config_path
|
|
139
|
-
self._config: Optional[Dict[str, Any]] = None
|
|
140
|
-
self._graphs: List[GraphConfig] = []
|
|
141
|
-
|
|
142
|
-
@property
|
|
143
|
-
def exists(self) -> bool:
|
|
144
|
-
"""Check if langgraph.json exists"""
|
|
145
|
-
return os.path.exists(self.config_path)
|
|
146
|
-
|
|
147
|
-
def load_config(self) -> Dict[str, Any]:
|
|
148
|
-
"""Load and validate langgraph configuration"""
|
|
149
|
-
if not self.exists:
|
|
150
|
-
raise FileNotFoundError(f"Config file not found: {self.config_path}")
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
with open(self.config_path, "r") as f:
|
|
154
|
-
config = json.load(f)
|
|
155
|
-
|
|
156
|
-
required_fields = ["graphs"]
|
|
157
|
-
missing_fields = [field for field in required_fields if field not in config]
|
|
158
|
-
if missing_fields:
|
|
159
|
-
raise ValueError(
|
|
160
|
-
f"Missing required fields in langgraph.json: {missing_fields}"
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
self._config = config
|
|
164
|
-
self._load_graphs()
|
|
165
|
-
return config
|
|
166
|
-
except Exception as e:
|
|
167
|
-
logger.error(f"Failed to load langgraph.json: {str(e)}")
|
|
168
|
-
raise
|
|
169
|
-
|
|
170
|
-
def _load_graphs(self):
|
|
171
|
-
"""Load all graph configurations"""
|
|
172
|
-
if not self._config:
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
self._graphs = [
|
|
176
|
-
GraphConfig.from_config(name, path)
|
|
177
|
-
for name, path in self._config["graphs"].items()
|
|
178
|
-
]
|
|
179
|
-
|
|
180
|
-
@property
|
|
181
|
-
def graphs(self) -> List[GraphConfig]:
|
|
182
|
-
"""Get all graph configurations"""
|
|
183
|
-
if not self._graphs:
|
|
184
|
-
self.load_config()
|
|
185
|
-
return self._graphs
|
|
186
|
-
|
|
187
|
-
def get_graph(self, name: str) -> Optional[GraphConfig]:
|
|
188
|
-
"""Get a specific graph configuration by name"""
|
|
189
|
-
return next((g for g in self.graphs if g.name == name), None)
|
|
190
|
-
|
|
191
|
-
@property
|
|
192
|
-
def dependencies(self) -> List[str]:
|
|
193
|
-
"""Get project dependencies"""
|
|
194
|
-
return self._config.get("dependencies", []) if self._config else []
|
|
195
|
-
|
|
196
|
-
@property
|
|
197
|
-
def env_file(self) -> Optional[str]:
|
|
198
|
-
"""Get environment file path"""
|
|
199
|
-
return self._config.get("env") if self._config else None
|
uipath_langchain/_cli/cli_dev.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
from openinference.instrumentation.langchain import (
|
|
5
|
-
LangChainInstrumentor,
|
|
6
|
-
get_current_span,
|
|
7
|
-
)
|
|
8
|
-
from uipath._cli._dev._terminal import UiPathDevTerminal
|
|
9
|
-
from uipath._cli._runtime._contracts import UiPathRuntimeFactory
|
|
10
|
-
from uipath._cli._utils._console import ConsoleLogger
|
|
11
|
-
from uipath._cli.middlewares import MiddlewareResult
|
|
12
|
-
|
|
13
|
-
from .._tracing import _instrument_traceable_attributes
|
|
14
|
-
from ._runtime._context import LangGraphRuntimeContext
|
|
15
|
-
from ._runtime._runtime import LangGraphRuntime
|
|
16
|
-
|
|
17
|
-
console = ConsoleLogger()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def langgraph_dev_middleware(interface: Optional[str]) -> MiddlewareResult:
|
|
21
|
-
"""Middleware to launch the developer terminal"""
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
if interface == "terminal":
|
|
25
|
-
runtime_factory = UiPathRuntimeFactory(
|
|
26
|
-
LangGraphRuntime, LangGraphRuntimeContext
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
_instrument_traceable_attributes()
|
|
30
|
-
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
|
|
31
|
-
app = UiPathDevTerminal(runtime_factory)
|
|
32
|
-
asyncio.run(app.run_async())
|
|
33
|
-
else:
|
|
34
|
-
console.error(f"Unknown interface: {interface}")
|
|
35
|
-
except KeyboardInterrupt:
|
|
36
|
-
console.info("Debug session interrupted by user")
|
|
37
|
-
except Exception as e:
|
|
38
|
-
console.error(f"Error occurred: {e}")
|
|
39
|
-
return MiddlewareResult(
|
|
40
|
-
should_continue=False,
|
|
41
|
-
should_include_stacktrace=True,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
return MiddlewareResult(should_continue=False)
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import List, Optional
|
|
3
|
-
|
|
4
|
-
from openinference.instrumentation.langchain import (
|
|
5
|
-
LangChainInstrumentor,
|
|
6
|
-
get_current_span,
|
|
7
|
-
)
|
|
8
|
-
from uipath._cli._evals._runtime import UiPathEvalContext, UiPathEvalRuntime
|
|
9
|
-
from uipath._cli._runtime._contracts import (
|
|
10
|
-
UiPathRuntimeFactory,
|
|
11
|
-
)
|
|
12
|
-
from uipath._cli._utils._eval_set import EvalHelpers
|
|
13
|
-
from uipath._cli.middlewares import MiddlewareResult
|
|
14
|
-
from uipath.eval._helpers import auto_discover_entrypoint
|
|
15
|
-
|
|
16
|
-
from uipath_langchain._cli._runtime._context import LangGraphRuntimeContext
|
|
17
|
-
from uipath_langchain._cli._runtime._runtime import LangGraphRuntime
|
|
18
|
-
from uipath_langchain._cli._utils._graph import LangGraphConfig
|
|
19
|
-
from uipath_langchain._tracing import (
|
|
20
|
-
LangChainExporter,
|
|
21
|
-
_instrument_traceable_attributes,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def langgraph_eval_middleware(
|
|
26
|
-
entrypoint: Optional[str], eval_set: Optional[str], eval_ids: List[str], **kwargs
|
|
27
|
-
) -> MiddlewareResult:
|
|
28
|
-
config = LangGraphConfig()
|
|
29
|
-
if not config.exists:
|
|
30
|
-
return MiddlewareResult(
|
|
31
|
-
should_continue=True
|
|
32
|
-
) # Continue with normal flow if no langgraph.json
|
|
33
|
-
|
|
34
|
-
eval_context = UiPathEvalContext.with_defaults(**kwargs)
|
|
35
|
-
eval_context.eval_set = eval_set or EvalHelpers.auto_discover_eval_set()
|
|
36
|
-
eval_context.eval_ids = eval_ids
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
_instrument_traceable_attributes()
|
|
40
|
-
|
|
41
|
-
runtime_entrypoint = entrypoint or auto_discover_entrypoint()
|
|
42
|
-
|
|
43
|
-
def generate_runtime_context(
|
|
44
|
-
context_entrypoint: str, langgraph_config: LangGraphConfig, **context_kwargs
|
|
45
|
-
) -> LangGraphRuntimeContext:
|
|
46
|
-
context = LangGraphRuntimeContext.with_defaults(**context_kwargs)
|
|
47
|
-
context.langgraph_config = langgraph_config
|
|
48
|
-
context.entrypoint = context_entrypoint
|
|
49
|
-
return context
|
|
50
|
-
|
|
51
|
-
runtime_factory = UiPathRuntimeFactory(
|
|
52
|
-
LangGraphRuntime,
|
|
53
|
-
LangGraphRuntimeContext,
|
|
54
|
-
context_generator=lambda **context_kwargs: generate_runtime_context(
|
|
55
|
-
context_entrypoint=runtime_entrypoint,
|
|
56
|
-
langgraph_config=config,
|
|
57
|
-
**context_kwargs,
|
|
58
|
-
),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if eval_context.job_id:
|
|
62
|
-
runtime_factory.add_span_exporter(LangChainExporter())
|
|
63
|
-
|
|
64
|
-
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
|
|
65
|
-
|
|
66
|
-
async def execute():
|
|
67
|
-
async with UiPathEvalRuntime.from_eval_context(
|
|
68
|
-
factory=runtime_factory, context=eval_context
|
|
69
|
-
) as eval_runtime:
|
|
70
|
-
await eval_runtime.execute()
|
|
71
|
-
|
|
72
|
-
asyncio.run(execute())
|
|
73
|
-
return MiddlewareResult(should_continue=False)
|
|
74
|
-
|
|
75
|
-
except Exception as e:
|
|
76
|
-
return MiddlewareResult(
|
|
77
|
-
should_continue=False, error_message=f"Error running evaluation: {str(e)}"
|
|
78
|
-
)
|