xpander-sdk 2.0.161__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.
- xpander_sdk/__init__.py +6 -0
- xpander_sdk/consts/api_routes.py +8 -0
- xpander_sdk/models/compactization.py +112 -0
- xpander_sdk/models/events.py +3 -0
- xpander_sdk/models/frameworks.py +2 -2
- xpander_sdk/models/generic.py +27 -0
- xpander_sdk/models/notifications.py +98 -0
- xpander_sdk/models/orchestrations.py +271 -0
- xpander_sdk/modules/agents/models/agent.py +7 -4
- xpander_sdk/modules/agents/sub_modules/agent.py +18 -10
- xpander_sdk/modules/backend/__init__.py +8 -0
- xpander_sdk/modules/backend/backend_module.py +47 -2
- xpander_sdk/modules/backend/decorators/__init__.py +7 -0
- xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
- xpander_sdk/modules/backend/events_registry.py +172 -0
- xpander_sdk/modules/backend/frameworks/agno.py +176 -54
- xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
- xpander_sdk/modules/backend/utils/mcp_oauth.py +36 -24
- xpander_sdk/modules/events/decorators/__init__.py +3 -0
- xpander_sdk/modules/events/decorators/on_tool.py +384 -0
- xpander_sdk/modules/events/events_module.py +9 -3
- xpander_sdk/modules/tasks/models/task.py +3 -14
- xpander_sdk/modules/tasks/sub_modules/task.py +54 -20
- xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
- xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
- xpander_sdk/utils/agents/__init__.py +0 -0
- xpander_sdk/utils/agents/compactization_agent.py +257 -0
- xpander_sdk/utils/generic.py +5 -0
- {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +97 -13
- {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +33 -22
- {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
- {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
- {xpander_sdk-2.0.161.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] = []
|
|
@@ -358,12 +359,12 @@ class Events(ModuleBase):
|
|
|
358
359
|
plan_following_status = await task.aget_plan_following_status()
|
|
359
360
|
if not plan_following_status.can_finish:
|
|
360
361
|
# Check if we've exceeded max retries
|
|
361
|
-
if retry_count >=
|
|
362
|
+
if retry_count >= 50: # 0, 1, 2 = 50 total attempts
|
|
362
363
|
logger.warning(f"Failed to complete plan after {retry_count + 1} attempts. Remaining incomplete tasks.")
|
|
363
364
|
return
|
|
364
365
|
|
|
365
366
|
# Recursively call with incremented retry count
|
|
366
|
-
logger.info(f"Plan not complete, retrying (attempt {retry_count + 2}
|
|
367
|
+
logger.info(f"Plan not complete, retrying (attempt {retry_count + 2})")
|
|
367
368
|
await self.handle_task_execution_request(
|
|
368
369
|
agent_worker,
|
|
369
370
|
task,
|
|
@@ -500,7 +501,12 @@ class Events(ModuleBase):
|
|
|
500
501
|
)
|
|
501
502
|
)
|
|
502
503
|
|
|
503
|
-
|
|
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)
|
|
504
510
|
|
|
505
511
|
elif event.event == EventType.AgentExecution:
|
|
506
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
|
|
42
|
+
class HumanInTheLoopRequest(BaseModel):
|
|
43
43
|
"""
|
|
44
44
|
Model representing human-in-the-loop approval records for tasks.
|
|
45
45
|
|
|
46
46
|
Attributes:
|
|
47
|
-
|
|
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
|
-
|
|
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
|
"""
|
|
@@ -43,7 +43,7 @@ from httpx import HTTPStatusError
|
|
|
43
43
|
import httpx
|
|
44
44
|
import json
|
|
45
45
|
from httpx_sse import aconnect_sse
|
|
46
|
-
from pydantic import Field
|
|
46
|
+
from pydantic import Field
|
|
47
47
|
|
|
48
48
|
from xpander_sdk.consts.api_routes import APIRoute
|
|
49
49
|
from xpander_sdk.core.xpander_api_client import APIClient
|
|
@@ -67,7 +67,7 @@ from xpander_sdk.modules.events.utils.generic import get_events_base, get_events
|
|
|
67
67
|
from xpander_sdk.modules.tasks.models.task import (
|
|
68
68
|
AgentExecutionInput,
|
|
69
69
|
AgentExecutionStatus,
|
|
70
|
-
|
|
70
|
+
HumanInTheLoopRequest,
|
|
71
71
|
ExecutionMetricsReport,
|
|
72
72
|
PendingECARequest,
|
|
73
73
|
TaskReportRequest,
|
|
@@ -82,12 +82,13 @@ from xpander_sdk.modules.tools_repository.models.mcp import (
|
|
|
82
82
|
MCPServerDetails,
|
|
83
83
|
)
|
|
84
84
|
from xpander_sdk.utils.event_loop import run_sync
|
|
85
|
+
from xpander_sdk.models.compactization import TaskCompactizationEvent
|
|
85
86
|
|
|
86
87
|
# Type variable for Task class methods
|
|
87
88
|
T = TypeVar("T", bound="Task")
|
|
88
89
|
|
|
89
90
|
TaskUpdateEventData = Union[
|
|
90
|
-
T, ToolCallRequest, ToolCallResult, MCPOAuthGetTokenResponse, DeepPlanning
|
|
91
|
+
TaskCompactizationEvent, T, ToolCallRequest, ToolCallResult, MCPOAuthGetTokenResponse, DeepPlanning
|
|
91
92
|
]
|
|
92
93
|
|
|
93
94
|
|
|
@@ -96,7 +97,7 @@ class TaskUpdateEvent(XPanderSharedModel):
|
|
|
96
97
|
task_id: str
|
|
97
98
|
organization_id: str
|
|
98
99
|
time: datetime
|
|
99
|
-
data:
|
|
100
|
+
data: Any
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
class Task(XPanderSharedModel):
|
|
@@ -124,7 +125,7 @@ class Task(XPanderSharedModel):
|
|
|
124
125
|
sub_executions (Optional[List[str]]): List of sub-execution IDs.
|
|
125
126
|
is_manually_stopped (Optional[bool]): Flag indicating if the task was manually stopped.
|
|
126
127
|
payload_extension (Optional[dict]): Additional data for the task.
|
|
127
|
-
hitl_request (Optional[
|
|
128
|
+
hitl_request (Optional[HumanInTheLoopRequest]): Human-in-the-loop request state.
|
|
128
129
|
pending_eca_request (Optional[PendingECARequest]): Pending ECA request, if any.
|
|
129
130
|
source (Optional[str]): Source information of the task.
|
|
130
131
|
output_format (Optional[OutputFormat]): Desired output format of the task.
|
|
@@ -136,6 +137,7 @@ class Task(XPanderSharedModel):
|
|
|
136
137
|
triggering_agent_id (Optional[str]): Optional triggering agent id.
|
|
137
138
|
title (Optional[str]): Optional task title.
|
|
138
139
|
deep_planning: Optional[DeepPlanning] = Field(default_factory=DeepPlanning)
|
|
140
|
+
execution_attempts: Optional[int] = 1
|
|
139
141
|
|
|
140
142
|
Example:
|
|
141
143
|
>>> task = Task.load(task_id="task_123")
|
|
@@ -170,12 +172,13 @@ class Task(XPanderSharedModel):
|
|
|
170
172
|
sub_executions: Optional[List[str]] = []
|
|
171
173
|
is_manually_stopped: Optional[bool] = False
|
|
172
174
|
payload_extension: Optional[dict] = None
|
|
173
|
-
hitl_request: Optional[
|
|
175
|
+
hitl_request: Optional[HumanInTheLoopRequest] = None
|
|
174
176
|
pending_eca_request: Optional[PendingECARequest] = None
|
|
175
177
|
source: Optional[str] = None
|
|
176
178
|
output_format: Optional[OutputFormat] = None
|
|
177
179
|
output_schema: Optional[Dict] = None
|
|
178
180
|
events_streaming: Optional[bool] = False
|
|
181
|
+
is_orchestration: Optional[bool] = False
|
|
179
182
|
additional_context: Optional[str] = None
|
|
180
183
|
expected_output: Optional[str] = (None,)
|
|
181
184
|
mcp_servers: Optional[List[MCPServerDetails]] = ([],)
|
|
@@ -184,6 +187,7 @@ class Task(XPanderSharedModel):
|
|
|
184
187
|
think_mode: Optional[ThinkMode] = ThinkMode.Default
|
|
185
188
|
disable_attachment_injection: Optional[bool] = False
|
|
186
189
|
deep_planning: Optional[DeepPlanning] = Field(default_factory=DeepPlanning)
|
|
190
|
+
execution_attempts: Optional[int] = 1
|
|
187
191
|
|
|
188
192
|
# metrics
|
|
189
193
|
tokens: Optional[Tokens] = None
|
|
@@ -337,10 +341,13 @@ class Task(XPanderSharedModel):
|
|
|
337
341
|
"""
|
|
338
342
|
return run_sync(self.aset_status(status=status, result=result))
|
|
339
343
|
|
|
340
|
-
async def asave(self):
|
|
344
|
+
async def asave(self, with_deep_plan_update: Optional[bool] = False):
|
|
341
345
|
"""
|
|
342
346
|
Asynchronously saves the current task state to the backend.
|
|
343
347
|
|
|
348
|
+
Args:
|
|
349
|
+
with_deep_plan_update (Optional[bool]): should update deep plan as well? default false.
|
|
350
|
+
|
|
344
351
|
Raises:
|
|
345
352
|
ModuleException: Error related to HTTP requests or task saving.
|
|
346
353
|
|
|
@@ -349,10 +356,15 @@ class Task(XPanderSharedModel):
|
|
|
349
356
|
"""
|
|
350
357
|
client = APIClient(configuration=self.configuration)
|
|
351
358
|
try:
|
|
359
|
+
exclude = {"configuration"}
|
|
360
|
+
|
|
361
|
+
if not with_deep_plan_update:
|
|
362
|
+
exclude.add("deep_planning")
|
|
363
|
+
|
|
352
364
|
response = await client.make_request(
|
|
353
365
|
path=APIRoute.UpdateTask.format(task_id=self.id),
|
|
354
366
|
method="PATCH",
|
|
355
|
-
payload=self.model_dump_safe(exclude=
|
|
367
|
+
payload=self.model_dump_safe(exclude=exclude),
|
|
356
368
|
)
|
|
357
369
|
updated_task = Task(**response, configuration=self.configuration)
|
|
358
370
|
for field, value in updated_task.__dict__.items():
|
|
@@ -362,16 +374,19 @@ class Task(XPanderSharedModel):
|
|
|
362
374
|
except Exception as e:
|
|
363
375
|
raise ModuleException(500, f"Failed to save task: {str(e)}")
|
|
364
376
|
|
|
365
|
-
def save(self):
|
|
377
|
+
def save(self, with_deep_plan_update: Optional[bool] = False):
|
|
366
378
|
"""
|
|
367
379
|
Saves the current task state synchronously.
|
|
368
380
|
|
|
369
381
|
This function wraps the asynchronous asave method.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
with_deep_plan_update (Optional[bool]): should update deep plan as well? default false.
|
|
370
385
|
|
|
371
386
|
Example:
|
|
372
387
|
>>> task.save()
|
|
373
388
|
"""
|
|
374
|
-
return run_sync(self.asave())
|
|
389
|
+
return run_sync(self.asave(with_deep_plan_update=with_deep_plan_update))
|
|
375
390
|
|
|
376
391
|
async def astop(self):
|
|
377
392
|
"""
|
|
@@ -540,16 +555,28 @@ class Task(XPanderSharedModel):
|
|
|
540
555
|
for f in readable_files:
|
|
541
556
|
message += f"\n{json.dumps(f)}"
|
|
542
557
|
|
|
543
|
-
if self.deep_planning and self.deep_planning.enabled == True:
|
|
558
|
+
if self.deep_planning and self.deep_planning.enabled == True and self.deep_planning.started:
|
|
559
|
+
task_backup = self.model_copy() # backup result and status
|
|
560
|
+
|
|
544
561
|
self.reload()
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
]
|
|
562
|
+
|
|
563
|
+
# restore result and status
|
|
564
|
+
self.result = task_backup.result
|
|
565
|
+
self.status = task_backup.status
|
|
566
|
+
self.tokens = task_backup.tokens
|
|
567
|
+
|
|
568
|
+
if not self.deep_planning.question_raised:
|
|
569
|
+
uncompleted_tasks = [task for task in self.deep_planning.tasks if not task.completed]
|
|
570
|
+
if len(uncompleted_tasks) != 0: # make a retry with compactization
|
|
571
|
+
from xpander_sdk.utils.agents.compactization_agent import run_task_compactization
|
|
572
|
+
compactization_result = run_task_compactization(message=message, task=self, uncompleted_tasks=uncompleted_tasks)
|
|
573
|
+
if isinstance(compactization_result, str):
|
|
574
|
+
message = compactization_result
|
|
575
|
+
else:
|
|
576
|
+
message = f"<user_input>{compactization_result.new_task_prompt}</user_input><task_context>{compactization_result.task_context}</task_context>"
|
|
577
|
+
else:
|
|
578
|
+
self.deep_planning.question_raised = False # reset question raised indicator
|
|
579
|
+
self.save(with_deep_plan_update=True)
|
|
553
580
|
|
|
554
581
|
return message
|
|
555
582
|
|
|
@@ -785,8 +812,15 @@ class Task(XPanderSharedModel):
|
|
|
785
812
|
... print(f"Remaining tasks: {len(status.uncompleted_tasks)}")
|
|
786
813
|
"""
|
|
787
814
|
try:
|
|
815
|
+
task_backup = self.model_copy() # backup result and status
|
|
816
|
+
await self.areload() # reload
|
|
817
|
+
|
|
818
|
+
# restore result and status
|
|
819
|
+
self.result = task_backup.result
|
|
820
|
+
self.status = task_backup.status
|
|
821
|
+
self.tokens = task_backup.tokens
|
|
822
|
+
|
|
788
823
|
if self.deep_planning and self.deep_planning.enabled and self.deep_planning.started and self.deep_planning.enforce:
|
|
789
|
-
await self.areload()
|
|
790
824
|
|
|
791
825
|
# allow early exit to ask question
|
|
792
826
|
if self.deep_planning.question_raised:
|