xpander-sdk 2.0.144__py3-none-any.whl → 2.0.192__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 (37) hide show
  1. xpander_sdk/__init__.py +6 -0
  2. xpander_sdk/consts/api_routes.py +9 -0
  3. xpander_sdk/models/activity.py +65 -0
  4. xpander_sdk/models/compactization.py +112 -0
  5. xpander_sdk/models/deep_planning.py +18 -0
  6. xpander_sdk/models/events.py +6 -0
  7. xpander_sdk/models/frameworks.py +2 -2
  8. xpander_sdk/models/generic.py +27 -0
  9. xpander_sdk/models/notifications.py +98 -0
  10. xpander_sdk/models/orchestrations.py +271 -0
  11. xpander_sdk/modules/agents/models/agent.py +11 -5
  12. xpander_sdk/modules/agents/sub_modules/agent.py +25 -10
  13. xpander_sdk/modules/backend/__init__.py +8 -0
  14. xpander_sdk/modules/backend/backend_module.py +47 -2
  15. xpander_sdk/modules/backend/decorators/__init__.py +7 -0
  16. xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
  17. xpander_sdk/modules/backend/events_registry.py +172 -0
  18. xpander_sdk/modules/backend/frameworks/agno.py +377 -15
  19. xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
  20. xpander_sdk/modules/backend/utils/mcp_oauth.py +37 -25
  21. xpander_sdk/modules/events/decorators/__init__.py +3 -0
  22. xpander_sdk/modules/events/decorators/on_tool.py +384 -0
  23. xpander_sdk/modules/events/events_module.py +28 -1
  24. xpander_sdk/modules/tasks/models/task.py +3 -14
  25. xpander_sdk/modules/tasks/sub_modules/task.py +276 -84
  26. xpander_sdk/modules/tools_repository/models/mcp.py +1 -0
  27. xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
  28. xpander_sdk/modules/tools_repository/tools_repository_module.py +6 -2
  29. xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
  30. xpander_sdk/utils/agents/__init__.py +0 -0
  31. xpander_sdk/utils/agents/compactization_agent.py +257 -0
  32. xpander_sdk/utils/generic.py +5 -0
  33. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +224 -14
  34. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +37 -24
  35. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
  36. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
  37. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,384 @@
1
+ """
2
+ xpander_sdk.decorators.on_tool
3
+
4
+ This module provides decorators for tool invocation lifecycle hooks:
5
+ - `@on_tool_before`: Execute before tool invocation
6
+ - `@on_tool_after`: Execute after successful tool invocation
7
+ - `@on_tool_error`: Execute when tool invocation fails
8
+
9
+ The decorators ensure that registered functions:
10
+ - Accept parameters: Tool, payload, payload_extension, tool_call_id, agent_version
11
+ - Can be either synchronous or asynchronous
12
+ - Are called at the appropriate lifecycle stage during tool execution
13
+
14
+ Execution Notes:
15
+ - Before-hooks execute before tool invocation and can perform validation or logging
16
+ - After-hooks execute after successful invocation with access to the result
17
+ - Error-hooks execute when an exception occurs during invocation
18
+ - Multiple hooks of the same type can be registered and will execute in registration order
19
+ - Exceptions in hooks are logged but don't prevent tool execution
20
+
21
+ Example usage:
22
+ --------------
23
+ >>> @on_tool_before
24
+ ... async def log_tool_invocation(tool, payload, payload_extension, tool_call_id, agent_version):
25
+ ... logger.info(f"Invoking tool {tool.name} with payload: {payload}")
26
+
27
+ >>> @on_tool_after
28
+ ... def record_tool_result(tool, payload, payload_extension, tool_call_id, agent_version, result):
29
+ ... logger.info(f"Tool {tool.name} completed with result: {result}")
30
+
31
+ >>> @on_tool_error
32
+ ... async def handle_tool_error(tool, payload, payload_extension, tool_call_id, agent_version, error):
33
+ ... logger.error(f"Tool {tool.name} failed: {error}")
34
+ """
35
+
36
+ import asyncio
37
+ from functools import wraps
38
+ from inspect import iscoroutinefunction
39
+ from typing import Optional, Callable, Any, Dict, List
40
+
41
+ from xpander_sdk.models.configuration import Configuration
42
+
43
+
44
+ class ToolHooksRegistry:
45
+ """
46
+ Registry for tool invocation lifecycle hooks.
47
+
48
+ This class maintains class-level lists of hooks that are executed at different
49
+ stages of tool invocation: before, after, and on error.
50
+ """
51
+
52
+ _before_hooks: List[Callable] = []
53
+ _after_hooks: List[Callable] = []
54
+ _error_hooks: List[Callable] = []
55
+
56
+ @classmethod
57
+ def register_before_hook(cls, hook: Callable) -> None:
58
+ """
59
+ Register a before-invocation hook.
60
+
61
+ Args:
62
+ hook (Callable): The hook function to execute before tool invocation.
63
+ """
64
+ cls._before_hooks.append(hook)
65
+
66
+ @classmethod
67
+ def register_after_hook(cls, hook: Callable) -> None:
68
+ """
69
+ Register an after-invocation hook.
70
+
71
+ Args:
72
+ hook (Callable): The hook function to execute after successful tool invocation.
73
+ """
74
+ cls._after_hooks.append(hook)
75
+
76
+ @classmethod
77
+ def register_error_hook(cls, hook: Callable) -> None:
78
+ """
79
+ Register an error hook.
80
+
81
+ Args:
82
+ hook (Callable): The hook function to execute when tool invocation fails.
83
+ """
84
+ cls._error_hooks.append(hook)
85
+
86
+ @classmethod
87
+ async def execute_before_hooks(
88
+ cls,
89
+ tool: Any,
90
+ payload: Any,
91
+ payload_extension: Optional[Dict[str, Any]] = None,
92
+ tool_call_id: Optional[str] = None,
93
+ agent_version: Optional[str] = None
94
+ ) -> None:
95
+ """
96
+ Execute all registered before-invocation hooks.
97
+
98
+ Args:
99
+ tool: The Tool object being invoked.
100
+ payload: The payload being sent to the tool.
101
+ payload_extension: Additional payload data.
102
+ tool_call_id: Unique ID of the tool call.
103
+ agent_version: Version of the agent making the call.
104
+ """
105
+ from loguru import logger
106
+
107
+ for hook in cls._before_hooks:
108
+ try:
109
+ if asyncio.iscoroutinefunction(hook):
110
+ await hook(tool, payload, payload_extension, tool_call_id, agent_version)
111
+ else:
112
+ hook(tool, payload, payload_extension, tool_call_id, agent_version)
113
+ except Exception as e:
114
+ logger.error(f"Before-hook {hook.__name__} failed: {e}")
115
+
116
+ @classmethod
117
+ async def execute_after_hooks(
118
+ cls,
119
+ tool: Any,
120
+ payload: Any,
121
+ payload_extension: Optional[Dict[str, Any]] = None,
122
+ tool_call_id: Optional[str] = None,
123
+ agent_version: Optional[str] = None,
124
+ result: Any = None
125
+ ) -> None:
126
+ """
127
+ Execute all registered after-invocation hooks.
128
+
129
+ Args:
130
+ tool: The Tool object that was invoked.
131
+ payload: The payload sent to the tool.
132
+ payload_extension: Additional payload data.
133
+ tool_call_id: Unique ID of the tool call.
134
+ agent_version: Version of the agent that made the call.
135
+ result: The result returned by the tool invocation.
136
+ """
137
+ from loguru import logger
138
+
139
+ for hook in cls._after_hooks:
140
+ try:
141
+ if asyncio.iscoroutinefunction(hook):
142
+ await hook(tool, payload, payload_extension, tool_call_id, agent_version, result)
143
+ else:
144
+ hook(tool, payload, payload_extension, tool_call_id, agent_version, result)
145
+ except Exception as e:
146
+ logger.error(f"After-hook {hook.__name__} failed: {e}")
147
+
148
+ @classmethod
149
+ async def execute_error_hooks(
150
+ cls,
151
+ tool: Any,
152
+ payload: Any,
153
+ payload_extension: Optional[Dict[str, Any]] = None,
154
+ tool_call_id: Optional[str] = None,
155
+ agent_version: Optional[str] = None,
156
+ error: Optional[Exception] = None
157
+ ) -> None:
158
+ """
159
+ Execute all registered error hooks.
160
+
161
+ Args:
162
+ tool: The Tool object that failed.
163
+ payload: The payload sent to the tool.
164
+ payload_extension: Additional payload data.
165
+ tool_call_id: Unique ID of the tool call.
166
+ agent_version: Version of the agent that made the call.
167
+ error: The exception that occurred during invocation.
168
+ """
169
+ from loguru import logger
170
+
171
+ for hook in cls._error_hooks:
172
+ try:
173
+ if asyncio.iscoroutinefunction(hook):
174
+ await hook(tool, payload, payload_extension, tool_call_id, agent_version, error)
175
+ else:
176
+ hook(tool, payload, payload_extension, tool_call_id, agent_version, error)
177
+ except Exception as e:
178
+ logger.error(f"Error-hook {hook.__name__} failed: {e}")
179
+
180
+
181
+ def on_tool_before(
182
+ _func: Optional[Callable] = None,
183
+ *,
184
+ configuration: Optional[Configuration] = None
185
+ ):
186
+ """
187
+ Decorator to register a handler as a before-invocation hook for tool calls.
188
+
189
+ The decorated function will be executed before any tool invocation. The function:
190
+ - Must accept parameters: tool, payload, payload_extension, tool_call_id, agent_version
191
+ - Can be either synchronous or asynchronous
192
+ - Can perform validation, logging, or modification of invocation context
193
+
194
+ Args:
195
+ _func (Optional[Callable]):
196
+ The function to decorate (for direct usage like `@on_tool_before`).
197
+ configuration (Optional[Configuration]):
198
+ An optional configuration object (reserved for future use).
199
+
200
+ Example:
201
+ >>> from typing import Optional, Dict, Any
202
+ >>> from xpander_sdk import Tool
203
+ >>>
204
+ >>> @on_tool_before
205
+ ... async def validate_tool_input(
206
+ ... tool: Tool,
207
+ ... payload: Any,
208
+ ... payload_extension: Optional[Dict[str, Any]] = None,
209
+ ... tool_call_id: Optional[str] = None,
210
+ ... agent_version: Optional[str] = None
211
+ ... ):
212
+ ... logger.info(f"Pre-invoking tool: {tool.name}")
213
+ ... if not payload:
214
+ ... logger.warning("Empty payload provided")
215
+
216
+ >>> @on_tool_before
217
+ ... def log_tool_metrics(
218
+ ... tool: Tool,
219
+ ... payload: Any,
220
+ ... payload_extension: Optional[Dict[str, Any]] = None,
221
+ ... tool_call_id: Optional[str] = None,
222
+ ... agent_version: Optional[str] = None
223
+ ... ):
224
+ ... metrics.record_tool_invocation(tool.name, tool_call_id)
225
+ """
226
+
227
+ def decorator(func: Callable) -> Callable:
228
+ @wraps(func)
229
+ async def async_wrapper(*args, **kwargs):
230
+ return await func(*args, **kwargs)
231
+
232
+ @wraps(func)
233
+ def sync_wrapper(*args, **kwargs):
234
+ return func(*args, **kwargs)
235
+
236
+ wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
237
+
238
+ # Register hook in the registry
239
+ ToolHooksRegistry.register_before_hook(wrapped)
240
+
241
+ return wrapped
242
+
243
+ if _func and callable(_func):
244
+ return decorator(_func)
245
+
246
+ return decorator
247
+
248
+
249
+ def on_tool_after(
250
+ _func: Optional[Callable] = None,
251
+ *,
252
+ configuration: Optional[Configuration] = None
253
+ ):
254
+ """
255
+ Decorator to register a handler as an after-invocation hook for tool calls.
256
+
257
+ The decorated function will be executed after successful tool invocation. The function:
258
+ - Must accept parameters: tool, payload, payload_extension, tool_call_id, agent_version, result
259
+ - Can be either synchronous or asynchronous
260
+ - Can perform logging, analytics, or result processing
261
+
262
+ Args:
263
+ _func (Optional[Callable]):
264
+ The function to decorate (for direct usage like `@on_tool_after`).
265
+ configuration (Optional[Configuration]):
266
+ An optional configuration object (reserved for future use).
267
+
268
+ Example:
269
+ >>> from typing import Optional, Dict, Any
270
+ >>> from xpander_sdk import Tool
271
+ >>>
272
+ >>> @on_tool_after
273
+ ... async def log_tool_success(
274
+ ... tool: Tool,
275
+ ... payload: Any,
276
+ ... payload_extension: Optional[Dict[str, Any]] = None,
277
+ ... tool_call_id: Optional[str] = None,
278
+ ... agent_version: Optional[str] = None,
279
+ ... result: Any = None
280
+ ... ):
281
+ ... logger.info(f"Tool {tool.name} succeeded with result: {result}")
282
+ ... analytics.record_success(tool.name, tool_call_id)
283
+
284
+ >>> @on_tool_after
285
+ ... def cache_tool_result(
286
+ ... tool: Tool,
287
+ ... payload: Any,
288
+ ... payload_extension: Optional[Dict[str, Any]] = None,
289
+ ... tool_call_id: Optional[str] = None,
290
+ ... agent_version: Optional[str] = None,
291
+ ... result: Any = None
292
+ ... ):
293
+ ... cache.set(f"tool_{tool.id}_{hash(payload)}", result)
294
+ """
295
+
296
+ def decorator(func: Callable) -> Callable:
297
+ @wraps(func)
298
+ async def async_wrapper(*args, **kwargs):
299
+ return await func(*args, **kwargs)
300
+
301
+ @wraps(func)
302
+ def sync_wrapper(*args, **kwargs):
303
+ return func(*args, **kwargs)
304
+
305
+ wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
306
+
307
+ # Register hook in the registry
308
+ ToolHooksRegistry.register_after_hook(wrapped)
309
+
310
+ return wrapped
311
+
312
+ if _func and callable(_func):
313
+ return decorator(_func)
314
+
315
+ return decorator
316
+
317
+
318
+ def on_tool_error(
319
+ _func: Optional[Callable] = None,
320
+ *,
321
+ configuration: Optional[Configuration] = None
322
+ ):
323
+ """
324
+ Decorator to register a handler as an error hook for tool calls.
325
+
326
+ The decorated function will be executed when tool invocation fails. The function:
327
+ - Must accept parameters: tool, payload, payload_extension, tool_call_id, agent_version, error
328
+ - Can be either synchronous or asynchronous
329
+ - Can perform error logging, alerting, or recovery actions
330
+
331
+ Args:
332
+ _func (Optional[Callable]):
333
+ The function to decorate (for direct usage like `@on_tool_error`).
334
+ configuration (Optional[Configuration]):
335
+ An optional configuration object (reserved for future use).
336
+
337
+ Example:
338
+ >>> from typing import Optional, Dict, Any
339
+ >>> from xpander_sdk import Tool
340
+ >>>
341
+ >>> @on_tool_error
342
+ ... async def alert_on_tool_failure(
343
+ ... tool: Tool,
344
+ ... payload: Any,
345
+ ... payload_extension: Optional[Dict[str, Any]] = None,
346
+ ... tool_call_id: Optional[str] = None,
347
+ ... agent_version: Optional[str] = None,
348
+ ... error: Optional[Exception] = None
349
+ ... ):
350
+ ... logger.error(f"Tool {tool.name} failed: {error}")
351
+ ... await send_alert(f"Tool failure: {tool.name}", str(error))
352
+
353
+ >>> @on_tool_error
354
+ ... def log_tool_failure(
355
+ ... tool: Tool,
356
+ ... payload: Any,
357
+ ... payload_extension: Optional[Dict[str, Any]] = None,
358
+ ... tool_call_id: Optional[str] = None,
359
+ ... agent_version: Optional[str] = None,
360
+ ... error: Optional[Exception] = None
361
+ ... ):
362
+ ... error_log.write(f"{tool.name},{tool_call_id},{str(error)}\n")
363
+ """
364
+
365
+ def decorator(func: Callable) -> Callable:
366
+ @wraps(func)
367
+ async def async_wrapper(*args, **kwargs):
368
+ return await func(*args, **kwargs)
369
+
370
+ @wraps(func)
371
+ def sync_wrapper(*args, **kwargs):
372
+ return func(*args, **kwargs)
373
+
374
+ wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
375
+
376
+ # Register hook in the registry
377
+ ToolHooksRegistry.register_error_hook(wrapped)
378
+
379
+ return wrapped
380
+
381
+ if _func and callable(_func):
382
+ return decorator(_func)
383
+
384
+ return decorator
@@ -81,6 +81,7 @@ class Events(ModuleBase):
81
81
 
82
82
  worker: Optional[DeployedAsset] = None
83
83
  test_task: Optional[LocalTaskTest] = None
84
+ _heartbeat_task: Optional[asyncio.Task] = None
84
85
 
85
86
  # Class-level registries for boot and shutdown handlers
86
87
  _boot_handlers: List[BootHandler] = []
@@ -330,6 +331,7 @@ class Events(ModuleBase):
330
331
  agent_worker: DeployedAsset,
331
332
  task: Task,
332
333
  on_execution_request: ExecutionRequestHandler,
334
+ retry_count: Optional[int] = 0,
333
335
  ) -> None:
334
336
  """
335
337
  Handle an incoming task execution request.
@@ -338,6 +340,7 @@ class Events(ModuleBase):
338
340
  agent_worker (DeployedAsset): The deployed asset (agent) to handle the task.
339
341
  task (Task): The task object containing execution details.
340
342
  on_execution_request (ExecutionRequestHandler): The handler function to process the task.
343
+ retry_count (Optional[int]): Current retry attempt count. Defaults to 0.
341
344
  """
342
345
  error = None
343
346
  try:
@@ -351,6 +354,25 @@ class Events(ModuleBase):
351
354
  on_execution_request,
352
355
  task,
353
356
  )
357
+
358
+ # Check if plan is complete, retry if not
359
+ plan_following_status = await task.aget_plan_following_status()
360
+ if not plan_following_status.can_finish:
361
+ # Check if we've exceeded max retries
362
+ if retry_count >= 50: # 0, 1, 2 = 50 total attempts
363
+ logger.warning(f"Failed to complete plan after {retry_count + 1} attempts. Remaining incomplete tasks.")
364
+ return
365
+
366
+ # Recursively call with incremented retry count
367
+ logger.info(f"Plan not complete, retrying (attempt {retry_count + 2})")
368
+ await self.handle_task_execution_request(
369
+ agent_worker,
370
+ task,
371
+ on_execution_request,
372
+ retry_count=retry_count + 1
373
+ )
374
+ return
375
+
354
376
  except Exception as e:
355
377
  logger.exception(f"Execution handler failed - {str(e)}")
356
378
  error = str(e)
@@ -479,7 +501,12 @@ class Events(ModuleBase):
479
501
  )
480
502
  )
481
503
 
482
- self.track(asyncio.create_task(self.heartbeat_loop(agent_worker.id)))
504
+ # Cancel previous heartbeat task if it exists and start a new one
505
+ if self._heartbeat_task and not self._heartbeat_task.done():
506
+ logger.debug(f"Canceling previous heartbeat task for worker {agent_worker.id}")
507
+ self._heartbeat_task.cancel()
508
+ self._heartbeat_task = asyncio.create_task(self.heartbeat_loop(agent_worker.id))
509
+ self.track(self._heartbeat_task)
483
510
 
484
511
  elif event.event == EventType.AgentExecution:
485
512
  task = Task(**json.loads(event.data), configuration=self.configuration)
@@ -39,26 +39,15 @@ class AgentExecutionStatus(str, Enum):
39
39
  Stopped = "stopped"
40
40
 
41
41
 
42
- class HumanInTheLoop(BaseModel):
42
+ class HumanInTheLoopRequest(BaseModel):
43
43
  """
44
44
  Model representing human-in-the-loop approval records for tasks.
45
45
 
46
46
  Attributes:
47
- operation_id (str): Unique identifier of the operation requiring approval.
48
- approved_by (Optional[str]): User who approved the operation.
49
- rejected_by (Optional[str]): User who rejected the operation.
50
- title (Optional[str]): Title/subject of the approval request.
51
- description (Optional[str]): Detailed description of the approval.
52
- content (str): Content or action that requires approval.
47
+ wait_node_id (str): The id of the node that triggered this HITL.
53
48
  """
54
49
 
55
- operation_id: str
56
- approved_by: Optional[str] = None
57
- rejected_by: Optional[str] = None
58
- title: Optional[str] = None
59
- description: Optional[str] = None
60
- content: str
61
-
50
+ wait_node_id: str
62
51
 
63
52
  class AgentExecutionInput(BaseModel):
64
53
  """