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.
Files changed (33) hide show
  1. xpander_sdk/__init__.py +6 -0
  2. xpander_sdk/consts/api_routes.py +8 -0
  3. xpander_sdk/models/compactization.py +112 -0
  4. xpander_sdk/models/events.py +3 -0
  5. xpander_sdk/models/frameworks.py +2 -2
  6. xpander_sdk/models/generic.py +27 -0
  7. xpander_sdk/models/notifications.py +98 -0
  8. xpander_sdk/models/orchestrations.py +271 -0
  9. xpander_sdk/modules/agents/models/agent.py +7 -4
  10. xpander_sdk/modules/agents/sub_modules/agent.py +18 -10
  11. xpander_sdk/modules/backend/__init__.py +8 -0
  12. xpander_sdk/modules/backend/backend_module.py +47 -2
  13. xpander_sdk/modules/backend/decorators/__init__.py +7 -0
  14. xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
  15. xpander_sdk/modules/backend/events_registry.py +172 -0
  16. xpander_sdk/modules/backend/frameworks/agno.py +176 -54
  17. xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
  18. xpander_sdk/modules/backend/utils/mcp_oauth.py +36 -24
  19. xpander_sdk/modules/events/decorators/__init__.py +3 -0
  20. xpander_sdk/modules/events/decorators/on_tool.py +384 -0
  21. xpander_sdk/modules/events/events_module.py +9 -3
  22. xpander_sdk/modules/tasks/models/task.py +3 -14
  23. xpander_sdk/modules/tasks/sub_modules/task.py +54 -20
  24. xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
  25. xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
  26. xpander_sdk/utils/agents/__init__.py +0 -0
  27. xpander_sdk/utils/agents/compactization_agent.py +257 -0
  28. xpander_sdk/utils/generic.py +5 -0
  29. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +97 -13
  30. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +33 -22
  31. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
  32. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
  33. {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 >= 2: # 0, 1, 2 = 3 total attempts
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}/3)")
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
- 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)
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 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
  """
@@ -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, computed_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
- HumanInTheLoop,
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: TaskUpdateEventData
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[HumanInTheLoop]): Human-in-the-loop request state.
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[HumanInTheLoop] = None
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={"configuration","deep_planning"}),
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
- uncompleted_tasks = [task for task in self.deep_planning.tasks if not task.completed]
546
- if len(uncompleted_tasks) != 0:
547
- message = "\n".join([
548
- "Task not finished, uncompleted tasks detected:",
549
- f"Uncompleted tasks: {[task.model_dump_json() for task in uncompleted_tasks]}",
550
- "You must complete tasks if fulfilled",
551
- f"User's original request: \"{message}\""
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: