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.

@@ -1,4 +1,4 @@
1
- from typing import Any, Optional, Union
1
+ from typing import Optional
2
2
 
3
3
  from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
4
4
  from uipath._cli._runtime._contracts import UiPathRuntimeContext
@@ -7,10 +7,4 @@ from uipath._cli._runtime._contracts import UiPathRuntimeContext
7
7
  class LangGraphRuntimeContext(UiPathRuntimeContext):
8
8
  """Context information passed throughout the runtime execution."""
9
9
 
10
- output: Optional[Any] = None
11
- state: Optional[Any] = (
12
- None # TypedDict issue, the actual type is: Optional[langgraph.types.StateSnapshot]
13
- )
14
10
  memory: Optional[AsyncSqliteSaver] = None
15
- langsmith_tracing_enabled: Union[str, bool, None] = False
16
- resume_triggers_table: str = "__uipath_resume_triggers"
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  from typing import Any, Optional, cast
3
3
 
4
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
4
5
  from langgraph.types import Command
5
6
  from uipath._cli._runtime._contracts import (
6
7
  UiPathApiTrigger,
@@ -17,123 +18,130 @@ from ._exception import LangGraphRuntimeError
17
18
  logger = logging.getLogger(__name__)
18
19
 
19
20
 
20
- class LangGraphInputProcessor:
21
+ async def get_graph_input(
22
+ context: LangGraphRuntimeContext,
23
+ memory: AsyncSqliteSaver,
24
+ resume_triggers_table: str = "__uipath_resume_triggers",
25
+ ) -> Any:
21
26
  """
22
- Handles input processing for graph execution, including resume scenarios
23
- where it needs to fetch data from UiPath.
27
+ Process the input data for graph execution, handling both fresh starts and resume scenarios.
28
+
29
+ This method determines whether the graph is being executed fresh or resumed from a previous state.
30
+ For fresh executions, it returns the input JSON directly. For resume scenarios, it fetches
31
+ the latest trigger information from the database and constructs a Command object with the
32
+ appropriate resume data.
33
+
34
+ The method handles different types of resume triggers:
35
+ - API triggers: Creates an UiPathApiTrigger with inbox_id and request payload
36
+ - Other triggers: Uses the HitlReader to process the resume data
37
+
38
+ Args:
39
+ context: The runtime context for the graph execution.
40
+ memory: AsyncSqliteSaver. The async database saver used to fetch resume trigger data.
41
+ resume_triggers_table: str, optional. The name of the database table containing resume triggers (default: "__uipath_resume_triggers").
42
+
43
+ Returns:
44
+ Any: For fresh executions, returns the input JSON data directly.
45
+ For resume scenarios, returns a Command object containing the resume data
46
+ processed through the appropriate trigger handler.
47
+
48
+ Raises:
49
+ LangGraphRuntimeError: If there's an error fetching trigger data from the database
50
+ during resume processing.
24
51
  """
25
-
26
- def __init__(self, context: LangGraphRuntimeContext):
27
- """
28
- Initialize the LangGraphInputProcessor.
29
-
30
- Args:
31
- context: The runtime context for the graph execution.
32
- """
33
- self.context = context
34
-
35
- async def process(self) -> Any:
36
- """
37
- Process the input data for graph execution, handling both fresh starts and resume scenarios.
38
-
39
- This method determines whether the graph is being executed fresh or resumed from a previous state.
40
- For fresh executions, it returns the input JSON directly. For resume scenarios, it fetches
41
- the latest trigger information from the database and constructs a Command object with the
42
- appropriate resume data.
43
-
44
- The method handles different types of resume triggers:
45
- - API triggers: Creates an UiPathApiTrigger with inbox_id and request payload
46
- - Other triggers: Uses the HitlReader to process the resume data
47
-
48
- Returns:
49
- Any: For fresh executions, returns the input JSON data directly.
50
- For resume scenarios, returns a Command object containing the resume data
51
- processed through the appropriate trigger handler.
52
-
53
- Raises:
54
- LangGraphRuntimeError: If there's an error fetching trigger data from the database
55
- during resume processing.
56
- """
57
- logger.debug(f"Resumed: {self.context.resume} Input: {self.context.input_json}")
58
-
59
- if not self.context.resume:
60
- if self.context.input_message:
61
- return {
62
- "messages": uipath_to_human_messages(self.context.input_message)
63
- }
64
- return self.context.input_json
65
-
66
- if self.context.input_json:
67
- return Command(resume=self.context.input_json)
68
-
69
- trigger = await self._get_latest_trigger()
70
- if not trigger:
71
- return Command(resume=self.context.input_json)
72
-
73
- trigger_type, key, folder_path, folder_key, payload = trigger
74
- resume_trigger = UiPathResumeTrigger(
75
- trigger_type=trigger_type,
76
- item_key=key,
77
- folder_path=folder_path,
78
- folder_key=folder_key,
79
- payload=payload,
52
+ logger.debug(f"Resumed: {context.resume} Input: {context.input_json}")
53
+
54
+ # Fresh execution - return input directly
55
+ if not context.resume:
56
+ if context.input_message:
57
+ return {"messages": uipath_to_human_messages(context.input_message)}
58
+ return context.input_json
59
+
60
+ # Resume with explicit input provided
61
+ if context.input_json:
62
+ return Command(resume=context.input_json)
63
+
64
+ # Resume from database trigger
65
+ trigger = await _get_latest_trigger(
66
+ memory, resume_triggers_table=resume_triggers_table
67
+ )
68
+ if not trigger:
69
+ return Command(resume=context.input_json)
70
+
71
+ trigger_type, key, folder_path, folder_key, payload = trigger
72
+ resume_trigger = UiPathResumeTrigger(
73
+ trigger_type=trigger_type,
74
+ item_key=key,
75
+ folder_path=folder_path,
76
+ folder_key=folder_key,
77
+ payload=payload,
78
+ )
79
+ logger.debug(f"ResumeTrigger: {trigger_type} {key}")
80
+
81
+ # Populate back expected fields for api_triggers
82
+ if resume_trigger.trigger_type == UiPathResumeTriggerType.API:
83
+ resume_trigger.api_resume = UiPathApiTrigger(
84
+ inbox_id=resume_trigger.item_key, request=resume_trigger.payload
80
85
  )
81
- logger.debug(f"ResumeTrigger: {trigger_type} {key}")
82
-
83
- # populate back expected fields for api_triggers
84
- if resume_trigger.trigger_type == UiPathResumeTriggerType.API:
85
- resume_trigger.api_resume = UiPathApiTrigger(
86
- inbox_id=resume_trigger.item_key, request=resume_trigger.payload
87
- )
88
- return Command(resume=await HitlReader.read(resume_trigger))
89
-
90
- async def _get_latest_trigger(self) -> Optional[tuple[str, str, str, str, str]]:
91
- """
92
- Fetch the most recent resume trigger from the database.
93
-
94
- This private method queries the resume triggers table to retrieve the latest trigger
95
- information based on timestamp. It handles database connection setup and executes
96
- a SQL query to fetch trigger data needed for resume operations.
97
-
98
- The method returns trigger information as a tuple containing:
99
- - type: The type of trigger (e.g., 'API', 'MANUAL', etc.)
100
- - key: The unique identifier for the trigger/item
101
- - folder_path: The path to the folder containing the trigger
102
- - folder_key: The unique identifier for the folder
103
- - payload: The serialized payload data associated with the trigger
104
-
105
- Returns:
106
- Optional[tuple[str, str, str, str, str]]: A tuple containing (type, key, folder_path,
107
- folder_key, payload) for the most recent trigger, or None if no triggers are found
108
- or if the memory context is not available.
109
-
110
- Raises:
111
- LangGraphRuntimeError: If there's an error during database connection setup, query
112
- execution, or result fetching. The original exception is wrapped with context
113
- about the database operation failure.
114
- """
115
- if self.context.memory is None:
116
- return None
117
- try:
118
- await self.context.memory.setup()
119
- async with (
120
- self.context.memory.lock,
121
- self.context.memory.conn.cursor() as cur,
122
- ):
123
- await cur.execute(f"""
124
- SELECT type, key, folder_path, folder_key, payload
125
- FROM {self.context.resume_triggers_table}
126
- ORDER BY timestamp DESC
127
- LIMIT 1
128
- """)
129
- result = await cur.fetchone()
130
- if result is None:
131
- return None
132
- return cast(tuple[str, str, str, str, str], tuple(result))
133
- except Exception as e:
134
- raise LangGraphRuntimeError(
135
- "DB_QUERY_FAILED",
136
- "Database query failed",
137
- f"Error querying resume trigger information: {str(e)}",
138
- UiPathErrorCategory.SYSTEM,
139
- ) from e
86
+
87
+ return Command(resume=await HitlReader.read(resume_trigger))
88
+
89
+
90
+ async def _get_latest_trigger(
91
+ memory: AsyncSqliteSaver,
92
+ resume_triggers_table: str = "__uipath_resume_triggers",
93
+ ) -> Optional[tuple[str, str, str, str, str]]:
94
+ """
95
+ Fetch the most recent resume trigger from the database.
96
+
97
+ This private method queries the resume triggers table to retrieve the latest trigger
98
+ information based on timestamp. It handles database connection setup and executes
99
+ a SQL query to fetch trigger data needed for resume operations.
100
+
101
+ The method returns trigger information as a tuple containing:
102
+ - type: The type of trigger (e.g., 'API', 'MANUAL', etc.)
103
+ - key: The unique identifier for the trigger/item
104
+ - folder_path: The path to the folder containing the trigger
105
+ - folder_key: The unique identifier for the folder
106
+ - payload: The serialized payload data associated with the trigger
107
+
108
+ Args:
109
+ memory: The AsyncSqliteSaver instance used to access the database.
110
+ resume_triggers_table: The name of the table containing resume triggers (default: "__uipath_resume_triggers").
111
+
112
+ Returns:
113
+ Optional[tuple[str, str, str, str, str]]: A tuple containing (type, key, folder_path,
114
+ folder_key, payload) for the most recent trigger, or None if no triggers are found
115
+ or if the memory context is not available.
116
+
117
+ Raises:
118
+ LangGraphRuntimeError: If there's an error during database connection setup, query
119
+ execution, or result fetching. The original exception is wrapped with context
120
+ about the database operation failure.
121
+ """
122
+ if memory is None:
123
+ return None
124
+
125
+ try:
126
+ await memory.setup()
127
+ async with (
128
+ memory.lock,
129
+ memory.conn.cursor() as cur,
130
+ ):
131
+ await cur.execute(f"""
132
+ SELECT type, key, folder_path, folder_key, payload
133
+ FROM {resume_triggers_table}
134
+ ORDER BY timestamp DESC
135
+ LIMIT 1
136
+ """)
137
+ result = await cur.fetchone()
138
+ if result is None:
139
+ return None
140
+ return cast(tuple[str, str, str, str, str], tuple(result))
141
+ except Exception as e:
142
+ raise LangGraphRuntimeError(
143
+ "DB_QUERY_FAILED",
144
+ "Database query failed",
145
+ f"Error querying resume trigger information: {str(e)}",
146
+ UiPathErrorCategory.SYSTEM,
147
+ ) from e
@@ -1,234 +1,148 @@
1
1
  import json
2
2
  import logging
3
- from functools import cached_property
4
- from typing import Any, Dict, Optional, cast
3
+ from typing import Any
5
4
 
6
- from langgraph.types import Interrupt, StateSnapshot
5
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
7
6
  from uipath._cli._runtime._contracts import (
8
7
  UiPathErrorCategory,
9
8
  UiPathResumeTrigger,
10
- UiPathRuntimeResult,
11
- UiPathRuntimeStatus,
12
9
  )
13
10
  from uipath._cli._runtime._hitl import HitlProcessor
14
11
 
15
- from ._context import LangGraphRuntimeContext
16
12
  from ._exception import LangGraphRuntimeError
17
13
 
18
14
  logger = logging.getLogger(__name__)
19
15
 
20
16
 
21
- class LangGraphOutputProcessor:
22
- """
23
- Contains and manages the complete output information from graph execution.
24
- Handles serialization, interrupt data, and file output.
17
+ def serialize_output(output: Any) -> Any:
25
18
  """
19
+ Recursively serialize an output object.
26
20
 
27
- def __init__(self, context: LangGraphRuntimeContext) -> None:
28
- """
29
- Initialize the LangGraphOutputProcessor.
30
-
31
- Args:
32
- context: The runtime context for the graph execution.
33
- """
34
- self.context = context
35
- self._hitl_processor: Optional[HitlProcessor] = None
36
- self._resume_trigger: Optional[UiPathResumeTrigger] = None
37
-
38
- @classmethod
39
- async def create(
40
- cls, context: LangGraphRuntimeContext
41
- ) -> "LangGraphOutputProcessor":
42
- """
43
- Create and initialize a new LangGraphOutputProcessor instance asynchronously.
44
-
45
- Args:
46
- context: The runtime context for the graph execution.
47
-
48
- Returns:
49
- LangGraphOutputProcessor: A new initialized instance.
50
- """
51
- instance = cls(context)
52
-
53
- # Process interrupt information during initialization
54
- state = cast(StateSnapshot, context.state)
55
- if not state or not hasattr(state, "next") or not state.next:
56
- return instance
57
-
58
- for task in state.tasks:
59
- if hasattr(task, "interrupts") and task.interrupts:
60
- for interrupt in task.interrupts:
61
- if isinstance(interrupt, Interrupt):
62
- instance._hitl_processor = HitlProcessor(interrupt.value)
63
- return instance
64
-
65
- return instance
66
-
67
- @property
68
- def status(self) -> UiPathRuntimeStatus:
69
- """Determines the execution status based on state."""
70
- return (
71
- UiPathRuntimeStatus.SUSPENDED
72
- if self._hitl_processor
73
- else UiPathRuntimeStatus.SUCCESSFUL
74
- )
75
-
76
- @cached_property
77
- def serialized_output(self) -> Dict[str, Any]:
78
- """Serializes the graph execution result."""
79
- try:
80
- if self.context.output is None:
81
- return {}
21
+ Args:
22
+ output: The object to serialize
82
23
 
83
- return self._serialize_object(self.context.output)
24
+ Returns:
25
+ Dict[str, Any]: Serialized output as dictionary
26
+ """
27
+ if output is None:
28
+ return {}
29
+
30
+ # Handle Pydantic models
31
+ if hasattr(output, "model_dump"):
32
+ return serialize_output(output.model_dump(by_alias=True))
33
+ elif hasattr(output, "dict"):
34
+ return serialize_output(output.dict())
35
+ elif hasattr(output, "to_dict"):
36
+ return serialize_output(output.to_dict())
37
+
38
+ # Handle dictionaries
39
+ elif isinstance(output, dict):
40
+ return {k: serialize_output(v) for k, v in output.items()}
41
+
42
+ # Handle lists
43
+ elif isinstance(output, list):
44
+ return [serialize_output(item) for item in output]
45
+
46
+ # Handle other iterables (convert to dict first)
47
+ elif hasattr(output, "__iter__") and not isinstance(output, (str, bytes)):
48
+ try:
49
+ return serialize_output(dict(output))
50
+ except (TypeError, ValueError):
51
+ return output
84
52
 
85
- except Exception as e:
86
- raise LangGraphRuntimeError(
87
- "OUTPUT_SERIALIZATION_FAILED",
88
- "Failed to serialize graph output",
89
- f"Error serializing output data: {str(e)}",
90
- UiPathErrorCategory.SYSTEM,
91
- ) from e
53
+ # Return primitive types as is
54
+ return output
92
55
 
93
- def _serialize_object(self, obj):
94
- """Recursively serializes an object and all its nested components."""
95
- # Handle Pydantic models
96
- if hasattr(obj, "dict"):
97
- return self._serialize_object(obj.dict())
98
- elif hasattr(obj, "model_dump"):
99
- return self._serialize_object(obj.model_dump(by_alias=True))
100
- elif hasattr(obj, "to_dict"):
101
- return self._serialize_object(obj.to_dict())
102
- # Handle dictionaries
103
- elif isinstance(obj, dict):
104
- return {k: self._serialize_object(v) for k, v in obj.items()}
105
- # Handle lists
106
- elif isinstance(obj, list):
107
- return [self._serialize_object(item) for item in obj]
108
- # Handle other iterable objects (convert to dict first)
109
- elif hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes)):
110
- try:
111
- return self._serialize_object(dict(obj))
112
- except (TypeError, ValueError):
113
- return obj
114
- # Return primitive types as is
115
- else:
116
- return obj
117
56
 
118
- async def process(self) -> UiPathRuntimeResult:
119
- """
120
- Process the output and prepare the final execution result.
57
+ async def create_and_save_resume_trigger(
58
+ interrupt_value: Any,
59
+ memory: AsyncSqliteSaver,
60
+ resume_triggers_table: str = "__uipath_resume_triggers",
61
+ ) -> UiPathResumeTrigger:
62
+ """
63
+ Create a resume trigger from interrupt value and save it to the database.
121
64
 
122
- Returns:
123
- UiPathRuntimeResult: The processed execution result.
65
+ Args:
66
+ interrupt_value: The interrupt value from dynamic interrupt
67
+ memory: The SQLite checkpointer/memory instance
68
+ resume_triggers_table: Name of the resume triggers table
124
69
 
125
- Raises:
126
- LangGraphRuntimeError: If processing fails.
127
- """
128
- try:
129
- await self._save_resume_trigger()
70
+ Returns:
71
+ UiPathResumeTrigger: The created resume trigger
130
72
 
131
- return UiPathRuntimeResult(
132
- output=self.serialized_output,
133
- status=self.status,
134
- resume=self._resume_trigger if self._resume_trigger else None,
135
- )
73
+ Raises:
74
+ LangGraphRuntimeError: If database operations fail
75
+ """
76
+ # Create HITL processor
77
+ hitl_processor = HitlProcessor(interrupt_value)
136
78
 
137
- except LangGraphRuntimeError:
138
- raise
79
+ # Setup database and create table if needed
80
+ await memory.setup()
81
+ async with memory.lock, memory.conn.cursor() as cur:
82
+ try:
83
+ await cur.execute(f"""
84
+ CREATE TABLE IF NOT EXISTS {resume_triggers_table} (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ type TEXT NOT NULL,
87
+ key TEXT,
88
+ folder_key TEXT,
89
+ folder_path TEXT,
90
+ payload TEXT,
91
+ timestamp DATETIME DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now', 'utc'))
92
+ )
93
+ """)
139
94
  except Exception as e:
140
95
  raise LangGraphRuntimeError(
141
- "OUTPUT_PROCESSING_FAILED",
142
- "Failed to process execution output",
143
- f"Unexpected error during output processing: {str(e)}",
96
+ "DB_TABLE_CREATION_FAILED",
97
+ "Failed to create resume triggers table",
98
+ f"Database error while creating table: {str(e)}",
144
99
  UiPathErrorCategory.SYSTEM,
145
100
  ) from e
146
101
 
147
- async def _save_resume_trigger(self) -> None:
148
- """
149
- Stores the resume trigger in the SQLite database if available.
102
+ # Create resume trigger
103
+ try:
104
+ resume_trigger = await hitl_processor.create_resume_trigger()
105
+ except Exception as e:
106
+ raise LangGraphRuntimeError(
107
+ "HITL_EVENT_CREATION_FAILED",
108
+ "Failed to process HITL request",
109
+ f"Error while trying to process HITL request: {str(e)}",
110
+ UiPathErrorCategory.SYSTEM,
111
+ ) from e
150
112
 
151
- Raises:
152
- LangGraphRuntimeError: If database operations fail.
153
- """
154
- if not self._hitl_processor or not self.context.memory:
155
- return
113
+ # Save to database
114
+ if resume_trigger.api_resume:
115
+ trigger_key = resume_trigger.api_resume.inbox_id
116
+ else:
117
+ trigger_key = resume_trigger.item_key
156
118
 
157
119
  try:
158
- await self.context.memory.setup()
159
- async with (
160
- self.context.memory.lock,
161
- self.context.memory.conn.cursor() as cur,
162
- ):
163
- try:
164
- await cur.execute(f"""
165
- CREATE TABLE IF NOT EXISTS {self.context.resume_triggers_table} (
166
- id INTEGER PRIMARY KEY AUTOINCREMENT,
167
- type TEXT NOT NULL,
168
- key TEXT,
169
- folder_key TEXT,
170
- folder_path TEXT,
171
- payload TEXT,
172
- timestamp DATETIME DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now', 'utc'))
173
- )
174
- """)
175
- except Exception as e:
176
- raise LangGraphRuntimeError(
177
- "DB_TABLE_CREATION_FAILED",
178
- "Failed to create resume triggers table",
179
- f"Database error while creating table: {str(e)}",
180
- UiPathErrorCategory.SYSTEM,
181
- ) from e
182
-
183
- try:
184
- self._resume_trigger = (
185
- await self._hitl_processor.create_resume_trigger()
186
- )
187
- except Exception as e:
188
- raise LangGraphRuntimeError(
189
- "HITL_EVENT_CREATION_FAILED",
190
- "Failed to process HITL request",
191
- f"Error while trying to process HITL request: {str(e)}",
192
- UiPathErrorCategory.SYSTEM,
193
- ) from e
194
- # if API trigger, override item_key and payload
195
- if self._resume_trigger:
196
- if self._resume_trigger.api_resume:
197
- trigger_key = self._resume_trigger.api_resume.inbox_id
198
- else:
199
- trigger_key = self._resume_trigger.item_key
200
- try:
201
- logger.debug(
202
- f"ResumeTrigger: {self._resume_trigger.trigger_type} {self._resume_trigger.item_key}"
203
- )
204
- if isinstance(self._resume_trigger.payload, dict):
205
- payload = json.dumps(self._resume_trigger.payload)
206
- else:
207
- payload = str(self._resume_trigger.payload)
208
- await cur.execute(
209
- f"INSERT INTO {self.context.resume_triggers_table} (type, key, payload, folder_path, folder_key) VALUES (?, ?, ?, ?, ?)",
210
- (
211
- self._resume_trigger.trigger_type.value,
212
- trigger_key,
213
- payload,
214
- self._resume_trigger.folder_path,
215
- self._resume_trigger.folder_key,
216
- ),
217
- )
218
- await self.context.memory.conn.commit()
219
- except Exception as e:
220
- raise LangGraphRuntimeError(
221
- "DB_INSERT_FAILED",
222
- "Failed to save resume trigger",
223
- f"Database error while saving resume trigger: {str(e)}",
224
- UiPathErrorCategory.SYSTEM,
225
- ) from e
226
- except LangGraphRuntimeError:
227
- raise
120
+ logger.debug(
121
+ f"ResumeTrigger: {resume_trigger.trigger_type} {resume_trigger.item_key}"
122
+ )
123
+
124
+ if isinstance(resume_trigger.payload, dict):
125
+ payload = json.dumps(resume_trigger.payload)
126
+ else:
127
+ payload = str(resume_trigger.payload)
128
+
129
+ await cur.execute(
130
+ f"INSERT INTO {resume_triggers_table} (type, key, payload, folder_path, folder_key) VALUES (?, ?, ?, ?, ?)",
131
+ (
132
+ resume_trigger.trigger_type.value,
133
+ trigger_key,
134
+ payload,
135
+ resume_trigger.folder_path,
136
+ resume_trigger.folder_key,
137
+ ),
138
+ )
139
+ await memory.conn.commit()
228
140
  except Exception as e:
229
141
  raise LangGraphRuntimeError(
230
- "RESUME_TRIGGER_SAVE_FAILED",
142
+ "DB_INSERT_FAILED",
231
143
  "Failed to save resume trigger",
232
- f"Unexpected error while saving resume trigger: {str(e)}",
144
+ f"Database error while saving resume trigger: {str(e)}",
233
145
  UiPathErrorCategory.SYSTEM,
234
146
  ) from e
147
+
148
+ return resume_trigger