xpander-sdk 1.60.4__py3-none-any.whl → 2.0.155__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 +76 -7793
- xpander_sdk/consts/__init__.py +0 -0
- xpander_sdk/consts/api_routes.py +63 -0
- xpander_sdk/core/__init__.py +0 -0
- xpander_sdk/core/module_base.py +164 -0
- xpander_sdk/core/state.py +10 -0
- xpander_sdk/core/xpander_api_client.py +119 -0
- xpander_sdk/exceptions/__init__.py +0 -0
- xpander_sdk/exceptions/module_exception.py +45 -0
- xpander_sdk/models/__init__.py +0 -0
- xpander_sdk/models/activity.py +65 -0
- xpander_sdk/models/configuration.py +92 -0
- xpander_sdk/models/events.py +70 -0
- xpander_sdk/models/frameworks.py +64 -0
- xpander_sdk/models/shared.py +102 -0
- xpander_sdk/models/user.py +21 -0
- xpander_sdk/modules/__init__.py +0 -0
- xpander_sdk/modules/agents/__init__.py +0 -0
- xpander_sdk/modules/agents/agents_module.py +164 -0
- xpander_sdk/modules/agents/models/__init__.py +0 -0
- xpander_sdk/modules/agents/models/agent.py +477 -0
- xpander_sdk/modules/agents/models/agent_list.py +107 -0
- xpander_sdk/modules/agents/models/knowledge_bases.py +33 -0
- xpander_sdk/modules/agents/sub_modules/__init__.py +0 -0
- xpander_sdk/modules/agents/sub_modules/agent.py +953 -0
- xpander_sdk/modules/agents/utils/__init__.py +0 -0
- xpander_sdk/modules/agents/utils/generic.py +2 -0
- xpander_sdk/modules/backend/__init__.py +0 -0
- xpander_sdk/modules/backend/backend_module.py +425 -0
- xpander_sdk/modules/backend/frameworks/__init__.py +0 -0
- xpander_sdk/modules/backend/frameworks/agno.py +627 -0
- xpander_sdk/modules/backend/frameworks/dispatch.py +36 -0
- xpander_sdk/modules/backend/utils/__init__.py +0 -0
- xpander_sdk/modules/backend/utils/mcp_oauth.py +95 -0
- xpander_sdk/modules/events/__init__.py +0 -0
- xpander_sdk/modules/events/decorators/__init__.py +0 -0
- xpander_sdk/modules/events/decorators/on_boot.py +94 -0
- xpander_sdk/modules/events/decorators/on_shutdown.py +94 -0
- xpander_sdk/modules/events/decorators/on_task.py +203 -0
- xpander_sdk/modules/events/events_module.py +629 -0
- xpander_sdk/modules/events/models/__init__.py +0 -0
- xpander_sdk/modules/events/models/deployments.py +25 -0
- xpander_sdk/modules/events/models/events.py +57 -0
- xpander_sdk/modules/events/utils/__init__.py +0 -0
- xpander_sdk/modules/events/utils/generic.py +56 -0
- xpander_sdk/modules/events/utils/git_init.py +32 -0
- xpander_sdk/modules/knowledge_bases/__init__.py +0 -0
- xpander_sdk/modules/knowledge_bases/knowledge_bases_module.py +217 -0
- xpander_sdk/modules/knowledge_bases/models/__init__.py +0 -0
- xpander_sdk/modules/knowledge_bases/models/knowledge_bases.py +11 -0
- xpander_sdk/modules/knowledge_bases/sub_modules/__init__.py +0 -0
- xpander_sdk/modules/knowledge_bases/sub_modules/knowledge_base.py +107 -0
- xpander_sdk/modules/knowledge_bases/sub_modules/knowledge_base_document_item.py +40 -0
- xpander_sdk/modules/knowledge_bases/utils/__init__.py +0 -0
- xpander_sdk/modules/tasks/__init__.py +0 -0
- xpander_sdk/modules/tasks/models/__init__.py +0 -0
- xpander_sdk/modules/tasks/models/task.py +153 -0
- xpander_sdk/modules/tasks/models/tasks_list.py +107 -0
- xpander_sdk/modules/tasks/sub_modules/__init__.py +0 -0
- xpander_sdk/modules/tasks/sub_modules/task.py +887 -0
- xpander_sdk/modules/tasks/tasks_module.py +492 -0
- xpander_sdk/modules/tasks/utils/__init__.py +0 -0
- xpander_sdk/modules/tasks/utils/files.py +114 -0
- xpander_sdk/modules/tools_repository/__init__.py +0 -0
- xpander_sdk/modules/tools_repository/decorators/__init__.py +0 -0
- xpander_sdk/modules/tools_repository/decorators/register_tool.py +108 -0
- xpander_sdk/modules/tools_repository/models/__init__.py +0 -0
- xpander_sdk/modules/tools_repository/models/mcp.py +68 -0
- xpander_sdk/modules/tools_repository/models/tool_invocation_result.py +14 -0
- xpander_sdk/modules/tools_repository/sub_modules/__init__.py +0 -0
- xpander_sdk/modules/tools_repository/sub_modules/tool.py +578 -0
- xpander_sdk/modules/tools_repository/tools_repository_module.py +259 -0
- xpander_sdk/modules/tools_repository/utils/__init__.py +0 -0
- xpander_sdk/modules/tools_repository/utils/generic.py +57 -0
- xpander_sdk/modules/tools_repository/utils/local_tools.py +52 -0
- xpander_sdk/modules/tools_repository/utils/schemas.py +308 -0
- xpander_sdk/utils/__init__.py +0 -0
- xpander_sdk/utils/env.py +44 -0
- xpander_sdk/utils/event_loop.py +67 -0
- xpander_sdk/utils/tools.py +32 -0
- xpander_sdk-2.0.155.dist-info/METADATA +538 -0
- xpander_sdk-2.0.155.dist-info/RECORD +85 -0
- {xpander_sdk-1.60.4.dist-info → xpander_sdk-2.0.155.dist-info}/WHEEL +1 -1
- {xpander_sdk-1.60.4.dist-info → xpander_sdk-2.0.155.dist-info/licenses}/LICENSE +0 -1
- xpander_sdk/_jsii/__init__.py +0 -39
- xpander_sdk/_jsii/xpander-sdk@1.60.4.jsii.tgz +0 -0
- xpander_sdk/py.typed +0 -1
- xpander_sdk-1.60.4.dist-info/METADATA +0 -368
- xpander_sdk-1.60.4.dist-info/RECORD +0 -9
- {xpander_sdk-1.60.4.dist-info → xpander_sdk-2.0.155.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from xpander_sdk.consts.api_routes import APIRoute
|
|
5
|
+
from xpander_sdk.core.xpander_api_client import APIClient
|
|
6
|
+
from xpander_sdk.models.events import TaskUpdateEventType
|
|
7
|
+
from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
|
|
8
|
+
from xpander_sdk.modules.tools_repository.models.mcp import MCPOAuthGetTokenGenericResponse, MCPOAuthGetTokenResponse, MCPOAuthResponseType, MCPServerDetails
|
|
9
|
+
|
|
10
|
+
POLLING_INTERVAL = 1 # every 1s
|
|
11
|
+
MAX_WAIT_FOR_LOGIN = 600 # 10 mintutes
|
|
12
|
+
|
|
13
|
+
async def push_event(task: Task, event: TaskUpdateEvent):
|
|
14
|
+
client = APIClient(configuration=task.configuration)
|
|
15
|
+
await client.make_request(
|
|
16
|
+
path=APIRoute.PushExecutionEventToQueue.format(task_id=task.id),
|
|
17
|
+
method="POST",
|
|
18
|
+
payload=[
|
|
19
|
+
TaskUpdateEvent(
|
|
20
|
+
task_id=task.id,
|
|
21
|
+
organization_id=task.organization_id,
|
|
22
|
+
time=datetime.now(timezone.utc).isoformat(),
|
|
23
|
+
type=TaskUpdateEventType.AuthEvent,
|
|
24
|
+
data=event
|
|
25
|
+
).model_dump_safe()
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
|
|
30
|
+
client = APIClient(configuration=task.configuration)
|
|
31
|
+
result: MCPOAuthGetTokenResponse = await client.make_request(
|
|
32
|
+
path=APIRoute.GetUserMCPAuthToken.format(agent_id=task.agent_id, user_id=user_id),
|
|
33
|
+
method="POST",
|
|
34
|
+
payload=mcp_server.model_dump(),
|
|
35
|
+
model=MCPOAuthGetTokenResponse
|
|
36
|
+
)
|
|
37
|
+
if result.type == MCPOAuthResponseType.TOKEN_READY:
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
|
|
43
|
+
try:
|
|
44
|
+
logger.info(f"Authenticating MCP Server {mcp_server.url}")
|
|
45
|
+
|
|
46
|
+
user_identifier = user_id if mcp_server.share_user_token_across_other_agents else f"{task.agent_id}_{user_id}"
|
|
47
|
+
|
|
48
|
+
client = APIClient(configuration=task.configuration)
|
|
49
|
+
result: MCPOAuthGetTokenResponse = await client.make_request(
|
|
50
|
+
path=APIRoute.GetUserMCPAuthToken.format(agent_id=task.agent_id, user_id=user_identifier),
|
|
51
|
+
method="POST",
|
|
52
|
+
payload=mcp_server.model_dump(),
|
|
53
|
+
model=MCPOAuthGetTokenResponse
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not result:
|
|
57
|
+
raise Exception("Invalid response")
|
|
58
|
+
|
|
59
|
+
if result.type == MCPOAuthResponseType.LOGIN_REQUIRED:
|
|
60
|
+
logger.info(f"Initiating login for MCP Server {mcp_server.url}")
|
|
61
|
+
# Notify user about login requirement
|
|
62
|
+
await push_event(task=task, event=result)
|
|
63
|
+
|
|
64
|
+
# Poll for token with timeout
|
|
65
|
+
elapsed_time = 0
|
|
66
|
+
while elapsed_time < MAX_WAIT_FOR_LOGIN:
|
|
67
|
+
await asyncio.sleep(POLLING_INTERVAL)
|
|
68
|
+
elapsed_time += POLLING_INTERVAL
|
|
69
|
+
|
|
70
|
+
# Check for token
|
|
71
|
+
token_result = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier)
|
|
72
|
+
if token_result and token_result.type == MCPOAuthResponseType.TOKEN_READY:
|
|
73
|
+
logger.info(f"Successful login for MCP Server {mcp_server.url}")
|
|
74
|
+
redacted_token_result = MCPOAuthGetTokenResponse(**token_result.model_dump_safe())
|
|
75
|
+
redacted_token_result.data.access_token = "REDACTED"
|
|
76
|
+
await push_event(task=task, event=redacted_token_result)
|
|
77
|
+
return token_result
|
|
78
|
+
|
|
79
|
+
# Timeout reached
|
|
80
|
+
raise TimeoutError(f"Authentication timeout for MCP Server {mcp_server.url} after {MAX_WAIT_FOR_LOGIN} seconds")
|
|
81
|
+
|
|
82
|
+
if result.type == MCPOAuthResponseType.TOKEN_READY:
|
|
83
|
+
logger.info(f"Token ready for MCP Server {mcp_server.url}")
|
|
84
|
+
redacted_token_result = MCPOAuthGetTokenResponse(**result.model_dump_safe())
|
|
85
|
+
redacted_token_result.data.access_token = "REDACTED"
|
|
86
|
+
await push_event(task=task, event=redacted_token_result)
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
except Exception as e:
|
|
90
|
+
error = f"Failed to process MCP Auth - {str(e)}"
|
|
91
|
+
logger.error(error)
|
|
92
|
+
return MCPOAuthGetTokenResponse(
|
|
93
|
+
type=MCPOAuthResponseType.TOKEN_ISSUE,
|
|
94
|
+
data=MCPOAuthGetTokenGenericResponse(message=error)
|
|
95
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xpander_sdk.decorators.on_boot
|
|
3
|
+
|
|
4
|
+
This module provides the `@on_boot` decorator, which allows developers to define
|
|
5
|
+
boot functions that execute before event listeners are set up in xpander.ai's event system.
|
|
6
|
+
|
|
7
|
+
The decorator ensures that the registered function:
|
|
8
|
+
- Can be either synchronous or asynchronous
|
|
9
|
+
- Executes before any event listeners are initialized
|
|
10
|
+
- Handles initialization logic, setup tasks, or pre-processing
|
|
11
|
+
|
|
12
|
+
Execution Notes:
|
|
13
|
+
- Boot handlers execute once during the application startup phase
|
|
14
|
+
- They run before event listeners are registered and task processing begins
|
|
15
|
+
- If an exception is raised in the function, the application will fail to start
|
|
16
|
+
- Use boot handlers for initialization tasks like database connections, configuration loading, etc.
|
|
17
|
+
|
|
18
|
+
Example usage:
|
|
19
|
+
--------------
|
|
20
|
+
>>> @on_boot
|
|
21
|
+
... async def initialize_database():
|
|
22
|
+
... database = Database()
|
|
23
|
+
... await database.connect()
|
|
24
|
+
... logger.info("Database connected successfully")
|
|
25
|
+
|
|
26
|
+
>>> @on_boot
|
|
27
|
+
... def load_configuration():
|
|
28
|
+
... config = load_config_file()
|
|
29
|
+
... validate_config(config)
|
|
30
|
+
... logger.info("Configuration loaded and validated")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
from functools import wraps
|
|
35
|
+
from inspect import iscoroutinefunction
|
|
36
|
+
from typing import Optional, Callable
|
|
37
|
+
|
|
38
|
+
from xpander_sdk.models.configuration import Configuration
|
|
39
|
+
from xpander_sdk.modules.events.events_module import Events
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def on_boot(
|
|
43
|
+
_func: Optional[Callable] = None,
|
|
44
|
+
*,
|
|
45
|
+
configuration: Optional[Configuration] = None
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Decorator to register a handler as a boot event executor.
|
|
49
|
+
|
|
50
|
+
The decorated function will be executed during the application boot phase, before
|
|
51
|
+
any event listeners are set up. The function:
|
|
52
|
+
- Can be either synchronous or asynchronous
|
|
53
|
+
- Should handle initialization logic or setup tasks
|
|
54
|
+
- Will be executed only once during application startup
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
_func (Optional[Callable]):
|
|
58
|
+
The function to decorate (for direct usage like `@on_boot`).
|
|
59
|
+
configuration (Optional[Configuration]):
|
|
60
|
+
An optional configuration object used to initialize the Events module.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> @on_boot
|
|
64
|
+
... async def initialize_services():
|
|
65
|
+
... await setup_database()
|
|
66
|
+
... await connect_external_apis()
|
|
67
|
+
... logger.info("All services initialized")
|
|
68
|
+
|
|
69
|
+
>>> @on_boot
|
|
70
|
+
... def load_environment():
|
|
71
|
+
... load_dotenv()
|
|
72
|
+
... validate_environment_variables()
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def decorator(func: Callable) -> Callable:
|
|
76
|
+
@wraps(func)
|
|
77
|
+
async def async_wrapper(*args, **kwargs):
|
|
78
|
+
return await func(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
@wraps(func)
|
|
81
|
+
def sync_wrapper(*args, **kwargs):
|
|
82
|
+
return func(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
|
|
85
|
+
|
|
86
|
+
# Register handler directly on Events class without instantiating
|
|
87
|
+
Events.register_boot_handler(wrapped)
|
|
88
|
+
|
|
89
|
+
return wrapped
|
|
90
|
+
|
|
91
|
+
if _func and callable(_func):
|
|
92
|
+
return decorator(_func)
|
|
93
|
+
|
|
94
|
+
return decorator
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xpander_sdk.decorators.on_shutdown
|
|
3
|
+
|
|
4
|
+
This module provides the `@on_shutdown` decorator, which allows developers to define
|
|
5
|
+
shutdown functions that execute when the application is exiting from xpander.ai's event system.
|
|
6
|
+
|
|
7
|
+
The decorator ensures that the registered function:
|
|
8
|
+
- Can be either synchronous or asynchronous
|
|
9
|
+
- Executes during application shutdown phase
|
|
10
|
+
- Handles cleanup logic, resource disposal, or finalization tasks
|
|
11
|
+
|
|
12
|
+
Execution Notes:
|
|
13
|
+
- Shutdown handlers execute when the application is terminating
|
|
14
|
+
- They run after event listeners are stopped but before final cleanup
|
|
15
|
+
- If an exception is raised in the function, it will be logged but won't prevent shutdown
|
|
16
|
+
- Use shutdown handlers for cleanup tasks like closing database connections, saving state, etc.
|
|
17
|
+
|
|
18
|
+
Example usage:
|
|
19
|
+
--------------
|
|
20
|
+
>>> @on_shutdown
|
|
21
|
+
... async def cleanup_database():
|
|
22
|
+
... database = get_database_connection()
|
|
23
|
+
... await database.close()
|
|
24
|
+
... logger.info("Database connection closed")
|
|
25
|
+
|
|
26
|
+
>>> @on_shutdown
|
|
27
|
+
... def save_application_state():
|
|
28
|
+
... state = get_current_state()
|
|
29
|
+
... save_state_to_file(state)
|
|
30
|
+
... logger.info("Application state saved")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
from functools import wraps
|
|
35
|
+
from inspect import iscoroutinefunction
|
|
36
|
+
from typing import Optional, Callable
|
|
37
|
+
|
|
38
|
+
from xpander_sdk.models.configuration import Configuration
|
|
39
|
+
from xpander_sdk.modules.events.events_module import Events
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def on_shutdown(
|
|
43
|
+
_func: Optional[Callable] = None,
|
|
44
|
+
*,
|
|
45
|
+
configuration: Optional[Configuration] = None
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Decorator to register a handler as a shutdown event executor.
|
|
49
|
+
|
|
50
|
+
The decorated function will be executed during the application shutdown phase, after
|
|
51
|
+
event listeners are stopped. The function:
|
|
52
|
+
- Can be either synchronous or asynchronous
|
|
53
|
+
- Should handle cleanup logic or resource disposal
|
|
54
|
+
- Will be executed once during application shutdown
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
_func (Optional[Callable]):
|
|
58
|
+
The function to decorate (for direct usage like `@on_shutdown`).
|
|
59
|
+
configuration (Optional[Configuration]):
|
|
60
|
+
An optional configuration object used to initialize the Events module.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> @on_shutdown
|
|
64
|
+
... async def cleanup_resources():
|
|
65
|
+
... await close_database_connections()
|
|
66
|
+
... await cleanup_temp_files()
|
|
67
|
+
... logger.info("All resources cleaned up")
|
|
68
|
+
|
|
69
|
+
>>> @on_shutdown
|
|
70
|
+
... def save_final_state():
|
|
71
|
+
... save_application_metrics()
|
|
72
|
+
... logger.info("Final state saved")
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def decorator(func: Callable) -> Callable:
|
|
76
|
+
@wraps(func)
|
|
77
|
+
async def async_wrapper(*args, **kwargs):
|
|
78
|
+
return await func(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
@wraps(func)
|
|
81
|
+
def sync_wrapper(*args, **kwargs):
|
|
82
|
+
return func(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
|
|
85
|
+
|
|
86
|
+
# Register handler directly on Events class without instantiating
|
|
87
|
+
Events.register_shutdown_handler(wrapped)
|
|
88
|
+
|
|
89
|
+
return wrapped
|
|
90
|
+
|
|
91
|
+
if _func and callable(_func):
|
|
92
|
+
return decorator(_func)
|
|
93
|
+
|
|
94
|
+
return decorator
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xpander_sdk.decorators.on_task
|
|
3
|
+
|
|
4
|
+
This module provides the `@on_task` decorator, which allows developers to define
|
|
5
|
+
task execution functions that integrate with xpander.ai’s event system.
|
|
6
|
+
|
|
7
|
+
The decorator ensures that the registered function:
|
|
8
|
+
- Accepts a single parameter named `task` of type `Task`
|
|
9
|
+
- Returns the modified `Task` after processing
|
|
10
|
+
- Optionally works with both sync and async function styles
|
|
11
|
+
|
|
12
|
+
Execution Notes:
|
|
13
|
+
- The task executor automatically handles status transitions (e.g., success or failure).
|
|
14
|
+
- You must set `task.result = ...` to assign your output.
|
|
15
|
+
- If an exception is raised in the function, the task will be marked as failed.
|
|
16
|
+
- You do not need to manually manage status or lifecycle hooks.
|
|
17
|
+
|
|
18
|
+
Example usage:
|
|
19
|
+
--------------
|
|
20
|
+
>>> @on_task
|
|
21
|
+
... async def handle_agent_task(task: Task):
|
|
22
|
+
... backend = Backend(task.configuration)
|
|
23
|
+
... agno_args = await backend.aget_args(
|
|
24
|
+
... agent_id=task.agent_id,
|
|
25
|
+
... agent_version=task.agent_version,
|
|
26
|
+
... task=task
|
|
27
|
+
... )
|
|
28
|
+
... agno_agent = Agent(**agno_args)
|
|
29
|
+
... result = await agno_agent.arun(message=task.to_message())
|
|
30
|
+
... task.result = result.content
|
|
31
|
+
... return task
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import asyncio
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import sys
|
|
38
|
+
from functools import wraps
|
|
39
|
+
from inspect import iscoroutinefunction, signature
|
|
40
|
+
from typing import Optional, Callable
|
|
41
|
+
|
|
42
|
+
from xpander_sdk.models.configuration import Configuration
|
|
43
|
+
from xpander_sdk.modules.events.events_module import Events
|
|
44
|
+
from xpander_sdk.modules.tasks.models.task import LocalTaskTest, AgentExecutionInput
|
|
45
|
+
from xpander_sdk.models.shared import OutputFormat
|
|
46
|
+
from xpander_sdk.modules.tasks.sub_modules.task import Task
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def on_task(
|
|
50
|
+
_func: Optional[Callable] = None,
|
|
51
|
+
*,
|
|
52
|
+
configuration: Optional[Configuration] = None,
|
|
53
|
+
test_task: Optional[LocalTaskTest] = None
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Decorator to register a handler as an event-driven task executor.
|
|
57
|
+
|
|
58
|
+
The decorated function will be executed when the task is triggered through
|
|
59
|
+
xpander.ai’s event system. The function:
|
|
60
|
+
- Must accept a single parameter named `task` of type `Task`
|
|
61
|
+
- Must return the `Task` instance after applying changes
|
|
62
|
+
- Should assign the final output using `task.result`
|
|
63
|
+
- Does not need to set the task status; it's handled automatically
|
|
64
|
+
|
|
65
|
+
- Supports both synchronous and asynchronous functions
|
|
66
|
+
- Automatically transitions task statuses based on function outcome
|
|
67
|
+
Args:
|
|
68
|
+
_func (Optional[Callable]):
|
|
69
|
+
The function to decorate (for direct usage like `@on_task`).
|
|
70
|
+
configuration (Optional[Configuration]):
|
|
71
|
+
An optional configuration object used to initialize the Events module.
|
|
72
|
+
test_task (Optional[LocalTaskTest]):
|
|
73
|
+
Optional simulated task used for local development or testing.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If the decorated function does not accept a `task` parameter.
|
|
77
|
+
TypeError: If `test_task` is provided but is not a `LocalTaskTest`.
|
|
78
|
+
TypeError: If the input `task` is not an instance of `Task`.
|
|
79
|
+
TypeError: If the return value is not an instance of `Task`.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> @on_task
|
|
83
|
+
... async def handle_agent_task(task: Task):
|
|
84
|
+
... backend = Backend(task.configuration)
|
|
85
|
+
... agno_args = await backend.aget_args(
|
|
86
|
+
... agent_id=task.agent_id,
|
|
87
|
+
... agent_version=task.agent_version,
|
|
88
|
+
... task=task
|
|
89
|
+
... )
|
|
90
|
+
... agno_agent = Agent(**agno_args)
|
|
91
|
+
... result = await agno_agent.arun(message=task.to_message())
|
|
92
|
+
... task.result = result.content
|
|
93
|
+
... return task
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def decorator(func: Callable) -> Callable:
|
|
97
|
+
sig = signature(func)
|
|
98
|
+
|
|
99
|
+
if 'task' not in sig.parameters:
|
|
100
|
+
raise TypeError(f"Function '{func.__name__}' must have a 'task' parameter.")
|
|
101
|
+
|
|
102
|
+
# Initialize effective_test_task with the passed parameter
|
|
103
|
+
effective_test_task = test_task
|
|
104
|
+
|
|
105
|
+
if effective_test_task and not isinstance(effective_test_task, LocalTaskTest):
|
|
106
|
+
raise TypeError(
|
|
107
|
+
f"'test_task' must be an instance of LocalTaskTest, got {type(effective_test_task).__name__}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
param_names = list(sig.parameters)
|
|
111
|
+
|
|
112
|
+
def get_task_from_call(args, kwargs) -> Task:
|
|
113
|
+
if 'task' in kwargs:
|
|
114
|
+
task_value = kwargs['task']
|
|
115
|
+
else:
|
|
116
|
+
task_index = param_names.index('task')
|
|
117
|
+
task_value = args[task_index] if len(args) > task_index else None
|
|
118
|
+
if not isinstance(task_value, Task):
|
|
119
|
+
raise TypeError(
|
|
120
|
+
f"'task' must be an instance of Task, got {type(task_value).__name__}"
|
|
121
|
+
)
|
|
122
|
+
return task_value
|
|
123
|
+
|
|
124
|
+
@wraps(func)
|
|
125
|
+
async def async_wrapper(*args, **kwargs):
|
|
126
|
+
get_task_from_call(args, kwargs)
|
|
127
|
+
result = await func(*args, **kwargs)
|
|
128
|
+
if not isinstance(result, Task):
|
|
129
|
+
raise TypeError(
|
|
130
|
+
f"Function '{func.__name__}' must return an instance of Task, got {type(result).__name__}"
|
|
131
|
+
)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
@wraps(func)
|
|
135
|
+
def sync_wrapper(*args, **kwargs):
|
|
136
|
+
get_task_from_call(args, kwargs)
|
|
137
|
+
result = func(*args, **kwargs)
|
|
138
|
+
if not isinstance(result, Task):
|
|
139
|
+
raise TypeError(
|
|
140
|
+
f"Function '{func.__name__}' must return an instance of Task, got {type(result).__name__}"
|
|
141
|
+
)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
|
|
145
|
+
|
|
146
|
+
# Check for direct invocation with --invoke and --prompt arguments
|
|
147
|
+
if any('--invoke' in arg for arg in sys.argv):
|
|
148
|
+
# Parse command line arguments
|
|
149
|
+
parser = argparse.ArgumentParser(description='Run task handler locally')
|
|
150
|
+
parser.add_argument('--invoke', action='store_true', help='Invoke the task handler directly')
|
|
151
|
+
parser.add_argument('--prompt', type=str, required='--invoke' in sys.argv, help='Input text for the task')
|
|
152
|
+
parser.add_argument('--output_format', type=str, choices=['json', 'markdown', 'text'],
|
|
153
|
+
help='Output format (json, markdown, text)')
|
|
154
|
+
parser.add_argument('--output_schema', type=str, help='JSON schema as a string for output validation')
|
|
155
|
+
|
|
156
|
+
# Parse only known args to avoid conflicts with other args that might be present
|
|
157
|
+
args, _ = parser.parse_known_args()
|
|
158
|
+
|
|
159
|
+
if args.invoke:
|
|
160
|
+
# Create a LocalTaskTest instance from command line arguments (like existing test_task logic)
|
|
161
|
+
output_format_map = {
|
|
162
|
+
'json': OutputFormat.Json,
|
|
163
|
+
'markdown': OutputFormat.Markdown,
|
|
164
|
+
'text': OutputFormat.Text
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Parse output schema if provided
|
|
168
|
+
output_schema = None
|
|
169
|
+
if args.output_schema:
|
|
170
|
+
try:
|
|
171
|
+
output_schema = json.loads(args.output_schema)
|
|
172
|
+
except json.JSONDecodeError as e:
|
|
173
|
+
print(f"Error parsing output_schema JSON: {e}")
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
# Create LocalTaskTest (same as existing test_task functionality)
|
|
177
|
+
# Only include output_format and output_schema if explicitly provided
|
|
178
|
+
task_kwargs = {
|
|
179
|
+
'input': AgentExecutionInput(text=args.prompt)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if args.output_format:
|
|
183
|
+
task_kwargs['output_format'] = output_format_map.get(args.output_format.lower())
|
|
184
|
+
|
|
185
|
+
if output_schema is not None:
|
|
186
|
+
task_kwargs['output_schema'] = output_schema
|
|
187
|
+
|
|
188
|
+
cli_test_task = LocalTaskTest(**task_kwargs)
|
|
189
|
+
# Override effective_test_task with CLI-provided one
|
|
190
|
+
effective_test_task = cli_test_task
|
|
191
|
+
print(f"Running task handler directly with prompt: {args.prompt}")
|
|
192
|
+
if output_schema:
|
|
193
|
+
print(f"Using output schema: {output_schema}")
|
|
194
|
+
|
|
195
|
+
events_module = Events(configuration=configuration)
|
|
196
|
+
events_module.register(on_task=wrapped, test_task=effective_test_task)
|
|
197
|
+
|
|
198
|
+
return wrapped
|
|
199
|
+
|
|
200
|
+
if _func and callable(_func):
|
|
201
|
+
return decorator(_func)
|
|
202
|
+
|
|
203
|
+
return decorator
|