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
@@ -28,39 +28,68 @@ Typical usage example:
28
28
  """
29
29
 
30
30
  from datetime import datetime
31
- from typing import Any, AsyncGenerator, Dict, Generator, List, Optional, Type, TypeVar, Union
31
+ from typing import (
32
+ Any,
33
+ AsyncGenerator,
34
+ Dict,
35
+ Generator,
36
+ List,
37
+ Optional,
38
+ Type,
39
+ TypeVar,
40
+ Union,
41
+ )
32
42
  from httpx import HTTPStatusError
33
43
  import httpx
34
44
  import json
35
45
  from httpx_sse import aconnect_sse
46
+ from pydantic import Field
36
47
 
37
48
  from xpander_sdk.consts.api_routes import APIRoute
38
49
  from xpander_sdk.core.xpander_api_client import APIClient
39
50
  from xpander_sdk.exceptions.module_exception import ModuleException
51
+ from xpander_sdk.models.activity import AgentActivityThread
40
52
  from xpander_sdk.models.configuration import Configuration
53
+ from xpander_sdk.models.deep_planning import PlanFollowingStatus, DeepPlanning
41
54
  from xpander_sdk.models.events import (
42
55
  TaskUpdateEventType,
43
56
  ToolCallRequest,
44
57
  ToolCallResult,
45
58
  )
46
- from xpander_sdk.models.shared import ExecutionTokens, OutputFormat, ThinkMode, Tokens, XPanderSharedModel
59
+ from xpander_sdk.models.shared import (
60
+ ExecutionTokens,
61
+ OutputFormat,
62
+ ThinkMode,
63
+ Tokens,
64
+ XPanderSharedModel,
65
+ )
47
66
  from xpander_sdk.modules.events.utils.generic import get_events_base, get_events_headers
48
67
  from xpander_sdk.modules.tasks.models.task import (
49
68
  AgentExecutionInput,
50
69
  AgentExecutionStatus,
51
- HumanInTheLoop,
70
+ HumanInTheLoopRequest,
52
71
  ExecutionMetricsReport,
53
72
  PendingECARequest,
54
- TaskReportRequest
73
+ TaskReportRequest,
74
+ )
75
+ from xpander_sdk.modules.tasks.utils.files import (
76
+ categorize_files,
77
+ fetch_urls,
78
+ fetch_file,
79
+ )
80
+ from xpander_sdk.modules.tools_repository.models.mcp import (
81
+ MCPOAuthGetTokenResponse,
82
+ MCPServerDetails,
55
83
  )
56
- from xpander_sdk.modules.tasks.utils.files import categorize_files, fetch_urls, fetch_file
57
- from xpander_sdk.modules.tools_repository.models.mcp import MCPOAuthGetTokenResponse, MCPServerDetails
58
84
  from xpander_sdk.utils.event_loop import run_sync
85
+ from xpander_sdk.models.compactization import TaskCompactizationEvent
59
86
 
60
87
  # Type variable for Task class methods
61
88
  T = TypeVar("T", bound="Task")
62
89
 
63
- TaskUpdateEventData = Union[T, ToolCallRequest, ToolCallResult, MCPOAuthGetTokenResponse]
90
+ TaskUpdateEventData = Union[
91
+ TaskCompactizationEvent, T, ToolCallRequest, ToolCallResult, MCPOAuthGetTokenResponse, DeepPlanning
92
+ ]
64
93
 
65
94
 
66
95
  class TaskUpdateEvent(XPanderSharedModel):
@@ -68,7 +97,7 @@ class TaskUpdateEvent(XPanderSharedModel):
68
97
  task_id: str
69
98
  organization_id: str
70
99
  time: datetime
71
- data: TaskUpdateEventData
100
+ data: Any
72
101
 
73
102
 
74
103
  class Task(XPanderSharedModel):
@@ -96,7 +125,7 @@ class Task(XPanderSharedModel):
96
125
  sub_executions (Optional[List[str]]): List of sub-execution IDs.
97
126
  is_manually_stopped (Optional[bool]): Flag indicating if the task was manually stopped.
98
127
  payload_extension (Optional[dict]): Additional data for the task.
99
- hitl_request (Optional[HumanInTheLoop]): Human-in-the-loop request state.
128
+ hitl_request (Optional[HumanInTheLoopRequest]): Human-in-the-loop request state.
100
129
  pending_eca_request (Optional[PendingECARequest]): Pending ECA request, if any.
101
130
  source (Optional[str]): Source information of the task.
102
131
  output_format (Optional[OutputFormat]): Desired output format of the task.
@@ -107,12 +136,14 @@ class Task(XPanderSharedModel):
107
136
  mcp_servers (Optional[List[MCPServerDetails]]): Optional list of mcp servers to use.
108
137
  triggering_agent_id (Optional[str]): Optional triggering agent id.
109
138
  title (Optional[str]): Optional task title.
139
+ deep_planning: Optional[DeepPlanning] = Field(default_factory=DeepPlanning)
140
+ execution_attempts: Optional[int] = 1
110
141
 
111
142
  Example:
112
143
  >>> task = Task.load(task_id="task_123")
113
144
  >>> task.set_status(AgentExecutionStatus.Running)
114
145
  >>> task.save()
115
- >>>
146
+ >>>
116
147
  >>> # Get files for Agno integration
117
148
  >>> files = task.get_files() # PDF files as Agno File objects
118
149
  >>> images = task.get_images() # Image files as Agno Image objects
@@ -141,20 +172,23 @@ class Task(XPanderSharedModel):
141
172
  sub_executions: Optional[List[str]] = []
142
173
  is_manually_stopped: Optional[bool] = False
143
174
  payload_extension: Optional[dict] = None
144
- hitl_request: Optional[HumanInTheLoop] = None
175
+ hitl_request: Optional[HumanInTheLoopRequest] = None
145
176
  pending_eca_request: Optional[PendingECARequest] = None
146
177
  source: Optional[str] = None
147
178
  output_format: Optional[OutputFormat] = None
148
179
  output_schema: Optional[Dict] = None
149
180
  events_streaming: Optional[bool] = False
181
+ is_orchestration: Optional[bool] = False
150
182
  additional_context: Optional[str] = None
151
- expected_output: Optional[str] = None,
152
- mcp_servers: Optional[List[MCPServerDetails]] = [],
153
- triggering_agent_id: Optional[str] = None,
154
- title: Optional[str] = None,
183
+ expected_output: Optional[str] = (None,)
184
+ mcp_servers: Optional[List[MCPServerDetails]] = ([],)
185
+ triggering_agent_id: Optional[str] = (None,)
186
+ title: Optional[str] = (None,)
155
187
  think_mode: Optional[ThinkMode] = ThinkMode.Default
156
188
  disable_attachment_injection: Optional[bool] = False
157
-
189
+ deep_planning: Optional[DeepPlanning] = Field(default_factory=DeepPlanning)
190
+ execution_attempts: Optional[int] = 1
191
+
158
192
  # metrics
159
193
  tokens: Optional[Tokens] = None
160
194
  used_tools: Optional[List[str]] = []
@@ -202,7 +236,6 @@ class Task(XPanderSharedModel):
202
236
  self.__dict__.update(new_obj.__dict__)
203
237
  return self
204
238
 
205
-
206
239
  def reload(self):
207
240
  """
208
241
  Reload the current object synchronously.
@@ -217,7 +250,6 @@ class Task(XPanderSharedModel):
217
250
  """
218
251
  run_sync(self.areload())
219
252
 
220
-
221
253
  @classmethod
222
254
  async def aload(
223
255
  cls: Type[T], task_id: str, configuration: Optional[Configuration] = None
@@ -243,7 +275,9 @@ class Task(XPanderSharedModel):
243
275
  response_data = await client.make_request(
244
276
  path=APIRoute.GetTask.format(task_id=task_id)
245
277
  )
246
- task = cls.model_validate({**response_data, "configuration": configuration or Configuration()})
278
+ task = cls.model_validate(
279
+ {**response_data, "configuration": configuration or Configuration()}
280
+ )
247
281
  return task
248
282
  except HTTPStatusError as e:
249
283
  raise ModuleException(
@@ -307,10 +341,13 @@ class Task(XPanderSharedModel):
307
341
  """
308
342
  return run_sync(self.aset_status(status=status, result=result))
309
343
 
310
- async def asave(self):
344
+ async def asave(self, with_deep_plan_update: Optional[bool] = False):
311
345
  """
312
346
  Asynchronously saves the current task state to the backend.
313
347
 
348
+ Args:
349
+ with_deep_plan_update (Optional[bool]): should update deep plan as well? default false.
350
+
314
351
  Raises:
315
352
  ModuleException: Error related to HTTP requests or task saving.
316
353
 
@@ -319,10 +356,15 @@ class Task(XPanderSharedModel):
319
356
  """
320
357
  client = APIClient(configuration=self.configuration)
321
358
  try:
359
+ exclude = {"configuration"}
360
+
361
+ if not with_deep_plan_update:
362
+ exclude.add("deep_planning")
363
+
322
364
  response = await client.make_request(
323
365
  path=APIRoute.UpdateTask.format(task_id=self.id),
324
366
  method="PATCH",
325
- payload=self.model_dump_safe(),
367
+ payload=self.model_dump_safe(exclude=exclude),
326
368
  )
327
369
  updated_task = Task(**response, configuration=self.configuration)
328
370
  for field, value in updated_task.__dict__.items():
@@ -332,16 +374,19 @@ class Task(XPanderSharedModel):
332
374
  except Exception as e:
333
375
  raise ModuleException(500, f"Failed to save task: {str(e)}")
334
376
 
335
- def save(self):
377
+ def save(self, with_deep_plan_update: Optional[bool] = False):
336
378
  """
337
379
  Saves the current task state synchronously.
338
380
 
339
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.
340
385
 
341
386
  Example:
342
387
  >>> task.save()
343
388
  """
344
- return run_sync(self.asave())
389
+ return run_sync(self.asave(with_deep_plan_update=with_deep_plan_update))
345
390
 
346
391
  async def astop(self):
347
392
  """
@@ -382,15 +427,15 @@ class Task(XPanderSharedModel):
382
427
  def get_files(self) -> list[Any]:
383
428
  """
384
429
  Get PDF files from task input, formatted for Agno integration.
385
-
430
+
386
431
  Returns PDF files as Agno File objects when the Agno framework is available,
387
432
  or as URL strings otherwise. This method is designed for seamless integration
388
433
  with Agno agents.
389
-
434
+
390
435
  Returns:
391
436
  list[Any]: List of File objects (when Agno is available) or URL strings.
392
437
  Returns empty list if no PDF files are present in task input.
393
-
438
+
394
439
  Example:
395
440
  >>> files = task.get_files()
396
441
  >>> result = await agno_agent.arun(
@@ -398,33 +443,34 @@ class Task(XPanderSharedModel):
398
443
  ... files=files
399
444
  ... )
400
445
  """
401
-
446
+
402
447
  if not self.input.files or len(self.input.files) == 0:
403
448
  return []
404
-
449
+
405
450
  categorized_files = categorize_files(file_urls=self.input.files)
406
-
451
+
407
452
  if not categorized_files.pdfs or len(categorized_files.pdfs) == 0:
408
453
  return []
409
454
 
410
455
  try:
411
- from agno.media import File # test import
456
+ from agno.media import File # test import
457
+
412
458
  return [fetch_file(url=url) for url in categorized_files.pdfs]
413
459
  except Exception:
414
460
  return categorized_files.pdfs
415
-
461
+
416
462
  def get_images(self) -> list[Any]:
417
463
  """
418
464
  Get image files from task input, formatted for Agno integration.
419
-
465
+
420
466
  Returns image files as Agno Image objects when the Agno framework is available,
421
467
  or as URL strings otherwise. This method is designed for seamless integration
422
468
  with Agno agents that support image processing.
423
-
469
+
424
470
  Returns:
425
471
  list[Any]: List of Image objects (when Agno is available) or URL strings.
426
472
  Returns empty list if no image files are present in task input.
427
-
473
+
428
474
  Example:
429
475
  >>> images = task.get_images()
430
476
  >>> result = await agno_agent.arun(
@@ -434,30 +480,31 @@ class Task(XPanderSharedModel):
434
480
  """
435
481
  if not self.input.files or len(self.input.files) == 0:
436
482
  return []
437
-
483
+
438
484
  categorized_files = categorize_files(file_urls=self.input.files)
439
-
485
+
440
486
  if not categorized_files.images or len(categorized_files.images) == 0:
441
487
  return []
442
488
 
443
489
  try:
444
490
  from agno.media import Image
491
+
445
492
  return [Image(url=url) for url in categorized_files.images]
446
493
  except Exception:
447
494
  return categorized_files.images
448
-
495
+
449
496
  def get_human_readable_files(self) -> list[Any]:
450
497
  """
451
498
  Get human-readable files from task input with their content.
452
-
499
+
453
500
  Returns text-based files (like .txt, .csv, .json, .py, etc.) with their content
454
501
  fetched and parsed. This method is automatically used by to_message() to include
455
502
  file contents in the task message.
456
-
503
+
457
504
  Returns:
458
505
  list[dict[str, str]]: List of dictionaries with 'url' and 'content' keys.
459
506
  Returns empty list if no human-readable files are present.
460
-
507
+
461
508
  Example:
462
509
  >>> readable_files = task.get_human_readable_files()
463
510
  >>> for file_data in readable_files:
@@ -466,14 +513,19 @@ class Task(XPanderSharedModel):
466
513
  """
467
514
  if not self.input.files or len(self.input.files) == 0:
468
515
  return []
469
-
516
+
470
517
  categorized_files = categorize_files(file_urls=self.input.files)
471
-
518
+
472
519
  if not categorized_files.files or len(categorized_files.files) == 0:
473
520
  return []
474
521
 
475
- return run_sync(fetch_urls(urls=categorized_files.files, disable_attachment_injection=self.disable_attachment_injection))
476
-
522
+ return run_sync(
523
+ fetch_urls(
524
+ urls=categorized_files.files,
525
+ disable_attachment_injection=self.disable_attachment_injection,
526
+ )
527
+ )
528
+
477
529
  def to_message(self) -> str:
478
530
  """
479
531
  Converts the input data into a formatted message string.
@@ -495,7 +547,7 @@ class Task(XPanderSharedModel):
495
547
  if len(message) != 0:
496
548
  message += "\n"
497
549
  message += "Files: " + (", ".join(self.input.files))
498
-
550
+
499
551
  # append human readable content like csv and such
500
552
  readable_files = self.get_human_readable_files()
501
553
  if readable_files and len(readable_files) != 0:
@@ -503,8 +555,95 @@ class Task(XPanderSharedModel):
503
555
  for f in readable_files:
504
556
  message += f"\n{json.dumps(f)}"
505
557
 
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
+
561
+ self.reload()
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)
580
+
506
581
  return message
507
582
 
583
+ async def aget_activity_log(self) -> AgentActivityThread:
584
+ """
585
+ Asynchronously retrieves the activity log for this task.
586
+
587
+ Fetches a detailed activity thread containing all messages, tool calls,
588
+ reasoning steps, sub-agent triggers, and authentication events that
589
+ occurred during the task execution.
590
+
591
+ Returns:
592
+ AgentActivityThread: Complete activity log including messages,
593
+ tool calls, reasoning, and other execution events.
594
+
595
+ Raises:
596
+ ModuleException: If the activity log cannot be retrieved or doesn't exist.
597
+
598
+ Example:
599
+ >>> task = await Task.aload(task_id="task_123")
600
+ >>> activity_log = await task.aget_activity_log()
601
+ >>> for message in activity_log.messages:
602
+ ... print(f"{message.role}: {message.content.text}")
603
+ """
604
+ try:
605
+ client = APIClient(configuration=self.configuration)
606
+ activity_log: AgentActivityThread = await client.make_request(
607
+ path=APIRoute.GetTaskActivityLog.format(
608
+ agent_id=self.agent_id, task_id=self.id
609
+ ),
610
+ model=AgentActivityThread,
611
+ )
612
+ if not activity_log:
613
+ raise HTTPStatusError(404, "Log not found")
614
+
615
+ return activity_log
616
+ except HTTPStatusError as e:
617
+ raise ModuleException(
618
+ status_code=e.response.status_code, description=e.response.text
619
+ )
620
+ except Exception as e:
621
+ raise ModuleException(
622
+ status_code=500, description=f"Failed to get activity log - {str(e)}"
623
+ )
624
+
625
+ def get_activity_log(self) -> AgentActivityThread:
626
+ """
627
+ Retrieves the activity log for this task synchronously.
628
+
629
+ This method wraps the asynchronous aget_activity_log method for use
630
+ in synchronous environments.
631
+
632
+ Returns:
633
+ AgentActivityThread: Complete activity log including messages,
634
+ tool calls, reasoning, and other execution events.
635
+
636
+ Raises:
637
+ ModuleException: If the activity log cannot be retrieved or doesn't exist.
638
+
639
+ Example:
640
+ >>> task = Task.load(task_id="task_123")
641
+ >>> activity_log = task.get_activity_log()
642
+ >>> for message in activity_log.messages:
643
+ ... print(f"{message.role}: {message.content.text}")
644
+ """
645
+ return run_sync(self.aget_activity_log())
646
+
508
647
  async def aevents(self) -> AsyncGenerator[TaskUpdateEvent, None]:
509
648
  """
510
649
  Asynchronously streams task events.
@@ -541,10 +680,12 @@ class Task(XPanderSharedModel):
541
680
  json_event_data: dict = json.loads(event.data)
542
681
  if json_event_data.get("type", None).startswith("task"):
543
682
  task_data = json_event_data.get("data")
544
- json_event_data.pop("data") # delete data
683
+ json_event_data.pop("data") # delete data
545
684
  yield TaskUpdateEvent(
546
685
  **json_event_data,
547
- data=Task(**task_data,configuration=self.configuration)
686
+ data=Task(
687
+ **task_data, configuration=self.configuration
688
+ ),
548
689
  )
549
690
  continue
550
691
  except Exception:
@@ -585,16 +726,13 @@ class Task(XPanderSharedModel):
585
726
 
586
727
  while queue:
587
728
  yield queue.pop(0)
588
-
589
- async def areport_metrics(
590
- self,
591
- configuration: Optional[Configuration] = None
592
- ):
729
+
730
+ async def areport_metrics(self, configuration: Optional[Configuration] = None):
593
731
  """
594
732
  Asynchronously report LLM task metrics to xpander.ai.
595
733
 
596
734
  Args:
597
- configuration (Optional[Configuration], optional):
735
+ configuration (Optional[Configuration], optional):
598
736
  API client configuration. If not provided, a new instance is created. Defaults to None.
599
737
 
600
738
  Raises:
@@ -606,10 +744,10 @@ class Task(XPanderSharedModel):
606
744
  try:
607
745
  configuration = configuration or Configuration()
608
746
  client = APIClient(configuration=configuration)
609
-
747
+
610
748
  if not self.tokens:
611
749
  raise ValueError("tokens must be provided. task.tokens = Tokens()")
612
-
750
+
613
751
  task_report_request = ExecutionMetricsReport(
614
752
  execution_id=self.id,
615
753
  source=self.source,
@@ -621,38 +759,30 @@ class Task(XPanderSharedModel):
621
759
  ai_model="xpander",
622
760
  api_calls_made=self.used_tools,
623
761
  result=self.result or None,
624
- llm_tokens=ExecutionTokens(worker=self.tokens)
762
+ llm_tokens=ExecutionTokens(worker=self.tokens),
625
763
  )
626
764
 
627
765
  await client.make_request(
628
- path=APIRoute.ReportExecutionMetrics.format(
629
- agent_id=self.agent_id
630
- ),
766
+ path=APIRoute.ReportExecutionMetrics.format(agent_id=self.agent_id),
631
767
  method="POST",
632
768
  payload=task_report_request.model_dump_safe(),
633
769
  )
634
770
 
635
771
  except HTTPStatusError as e:
636
772
  raise ModuleException(
637
- status_code=e.response.status_code,
638
- description=e.response.text
773
+ status_code=e.response.status_code, description=e.response.text
639
774
  )
640
775
  except Exception as e:
641
776
  raise ModuleException(
642
- status_code=500,
643
- description=f"Failed to report metrics - {str(e)}"
777
+ status_code=500, description=f"Failed to report metrics - {str(e)}"
644
778
  )
645
779
 
646
-
647
- def report_metrics(
648
- self,
649
- configuration: Optional[Configuration] = None
650
- ):
780
+ def report_metrics(self, configuration: Optional[Configuration] = None):
651
781
  """
652
782
  Report LLM task metrics to xpander.ai.
653
783
 
654
784
  Args:
655
- configuration (Optional[Configuration], optional):
785
+ configuration (Optional[Configuration], optional):
656
786
  API client configuration. If not provided, a new instance is created. Defaults to None.
657
787
 
658
788
  Raises:
@@ -661,15 +791,76 @@ class Task(XPanderSharedModel):
661
791
  Returns:
662
792
  None
663
793
  """
664
- return run_sync(
665
- self.areport_metrics(
666
- configuration=configuration
667
- )
668
- )
794
+ return run_sync(self.areport_metrics(configuration=configuration))
669
795
 
796
+ async def aget_plan_following_status(self) -> PlanFollowingStatus:
797
+ """
798
+ Asynchronously check if the task's deep planning is complete.
799
+
800
+ Reloads the task to get the latest deep planning state and checks for
801
+ any uncompleted tasks. If deep planning is disabled or all tasks are
802
+ completed, returns a status indicating the task can finish.
803
+
804
+ Returns:
805
+ PlanFollowingStatus: Status object containing:
806
+ - can_finish (bool): True if all tasks are completed or deep planning is disabled.
807
+ - uncompleted_tasks (List[DeepPlanningItem]): List of tasks not yet completed.
808
+
809
+ Example:
810
+ >>> status = await task.aget_plan_following_status()
811
+ >>> if not status.can_finish:
812
+ ... print(f"Remaining tasks: {len(status.uncompleted_tasks)}")
813
+ """
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
+
823
+ if self.deep_planning and self.deep_planning.enabled and self.deep_planning.started and self.deep_planning.enforce:
824
+
825
+ # allow early exit to ask question
826
+ if self.deep_planning.question_raised:
827
+ return PlanFollowingStatus(can_finish=True)
828
+
829
+ uncompleted_tasks = [
830
+ task for task in self.deep_planning.tasks if not task.completed
831
+ ]
832
+ if len(uncompleted_tasks) != 0:
833
+ return PlanFollowingStatus(
834
+ can_finish=False, uncompleted_tasks=uncompleted_tasks
835
+ )
836
+ except Exception:
837
+ pass
838
+
839
+ return PlanFollowingStatus(can_finish=True)
840
+
841
+ def get_plan_following_status(self) -> PlanFollowingStatus:
842
+ """
843
+ Check if the task's deep planning is complete synchronously.
844
+
845
+ This function wraps the asynchronous aget_plan_following_status method.
846
+ Reloads the task to get the latest deep planning state and checks for
847
+ any uncompleted tasks.
848
+
849
+ Returns:
850
+ PlanFollowingStatus: Status object containing:
851
+ - can_finish (bool): True if all tasks are completed or deep planning is disabled.
852
+ - uncompleted_tasks (List[DeepPlanningItem]): List of tasks not yet completed.
853
+
854
+ Example:
855
+ >>> status = task.get_plan_following_status()
856
+ >>> if not status.can_finish:
857
+ ... print(f"Remaining tasks: {len(status.uncompleted_tasks)}")
858
+ """
859
+ return run_sync(self.aget_plan_following_status())
860
+
670
861
  @classmethod
671
862
  async def areport_external_task(
672
- cls: Type[T],
863
+ cls: Type[T],
673
864
  configuration: Optional[Configuration] = None,
674
865
  agent_id: Optional[str] = None,
675
866
  id: Optional[str] = None,
@@ -679,7 +870,7 @@ class Task(XPanderSharedModel):
679
870
  is_success: Optional[bool] = True,
680
871
  result: Optional[str] = None,
681
872
  duration: Optional[float] = 0,
682
- used_tools: Optional[List[str]] = []
873
+ used_tools: Optional[List[str]] = [],
683
874
  ) -> T:
684
875
  """
685
876
  Asynchronously reports an external task to the xpander.ai backend.
@@ -687,7 +878,7 @@ class Task(XPanderSharedModel):
687
878
  This method is used to report the result of a task that was executed
688
879
  externally (outside the xpander.ai platform). It submits execution details,
689
880
  including inputs, outputs, success status, and resource usage, to the backend.
690
-
881
+
691
882
  Args:
692
883
  configuration (Optional[Configuration]): Optional configuration for API calls.
693
884
  agent_id (Optional[str]): Identifier of the agent associated with the task.
@@ -699,13 +890,13 @@ class Task(XPanderSharedModel):
699
890
  result (Optional[str]): String representation of the final result.
700
891
  duration (Optional[float]): Task execution duration, in seconds. Defaults to 0.
701
892
  used_tools (Optional[List[str]]): List of tools used during the execution. Defaults to empty list.
702
-
893
+
703
894
  Returns:
704
895
  T: Instance of the Task class, representing the reported task.
705
-
896
+
706
897
  Raises:
707
898
  ModuleException: Raised on backend or network errors.
708
-
899
+
709
900
  Example:
710
901
  >>> task = await Task.areport_external_task(
711
902
  ... agent_id="agent_xyz",
@@ -725,7 +916,7 @@ class Task(XPanderSharedModel):
725
916
  is_success=is_success,
726
917
  result=result,
727
918
  duration=duration,
728
- used_tools=used_tools
919
+ used_tools=used_tools,
729
920
  )
730
921
  response_data = await client.make_request(
731
922
  path=APIRoute.ReportExternalTask.format(agent_id=agent_id),
@@ -739,12 +930,13 @@ class Task(XPanderSharedModel):
739
930
  )
740
931
  except Exception as e:
741
932
  raise ModuleException(
742
- status_code=500, description=f"Failed to report external task - {str(e)}"
933
+ status_code=500,
934
+ description=f"Failed to report external task - {str(e)}",
743
935
  )
744
936
 
745
937
  @classmethod
746
938
  def report_external_task(
747
- cls: Type[T],
939
+ cls: Type[T],
748
940
  configuration: Optional[Configuration] = None,
749
941
  agent_id: Optional[str] = None,
750
942
  id: Optional[str] = None,
@@ -754,7 +946,7 @@ class Task(XPanderSharedModel):
754
946
  is_success: Optional[bool] = True,
755
947
  result: Optional[str] = None,
756
948
  duration: Optional[float] = 0,
757
- used_tools: Optional[List[str]] = []
949
+ used_tools: Optional[List[str]] = [],
758
950
  ) -> T:
759
951
  """
760
952
  Synchronously reports an external task to the xpander.ai backend.
@@ -34,6 +34,7 @@ class MCPServerDetails(BaseModel):
34
34
  headers: Optional[Dict] = {}
35
35
  env_vars: Optional[Dict] = {}
36
36
  allowed_tools: Optional[List[str]] = []
37
+ additional_scopes: Optional[List[str]] = []
37
38
  share_user_token_across_other_agents: Optional[bool] = True
38
39
 
39
40