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.
- xpander_sdk/__init__.py +6 -0
- xpander_sdk/consts/api_routes.py +9 -0
- xpander_sdk/models/activity.py +65 -0
- xpander_sdk/models/compactization.py +112 -0
- xpander_sdk/models/deep_planning.py +18 -0
- xpander_sdk/models/events.py +6 -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 +11 -5
- xpander_sdk/modules/agents/sub_modules/agent.py +25 -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 +377 -15
- xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
- xpander_sdk/modules/backend/utils/mcp_oauth.py +37 -25
- 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 +28 -1
- xpander_sdk/modules/tasks/models/task.py +3 -14
- xpander_sdk/modules/tasks/sub_modules/task.py +276 -84
- xpander_sdk/modules/tools_repository/models/mcp.py +1 -0
- xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
- xpander_sdk/modules/tools_repository/tools_repository_module.py +6 -2
- 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.144.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +224 -14
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +37 -24
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
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[
|
|
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:
|
|
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[
|
|
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[
|
|
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(
|
|
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
|
|
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(
|
|
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")
|
|
683
|
+
json_event_data.pop("data") # delete data
|
|
545
684
|
yield TaskUpdateEvent(
|
|
546
685
|
**json_event_data,
|
|
547
|
-
data=Task(
|
|
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,
|
|
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.
|