xpander-sdk 2.0.178__py3-none-any.whl → 2.0.180__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 CHANGED
@@ -17,6 +17,7 @@ For more information, visit: https://xpander.ai
17
17
 
18
18
  # Backend-related imports
19
19
  from .modules.backend.backend_module import Backend
20
+ from .modules.backend.decorators.on_auth_event import on_auth_event
20
21
 
21
22
  # Agent-related imports
22
23
  from .modules.agents.agents_module import Agents, Agent, AgentsListItem
@@ -27,6 +28,7 @@ from .modules.tasks.tasks_module import Tasks, Task, TasksListItem, AgentExecuti
27
28
  from xpander_sdk.modules.events.decorators.on_task import on_task
28
29
  from xpander_sdk.modules.events.decorators.on_boot import on_boot
29
30
  from xpander_sdk.modules.events.decorators.on_shutdown import on_shutdown
31
+ from xpander_sdk.modules.events.decorators.on_tool import on_tool_before, on_tool_after, on_tool_error
30
32
 
31
33
  # Tools and repository imports
32
34
  from .modules.tools_repository.tools_repository_module import ToolsRepository, Tool
@@ -53,6 +55,7 @@ from .models.shared import OutputFormat, Tokens
53
55
  __all__ = [
54
56
  # xpander.ai Backend
55
57
  "Backend",
58
+ "on_auth_event",
56
59
  # Agent management
57
60
  "Agents",
58
61
  "Agent",
@@ -75,6 +78,9 @@ __all__ = [
75
78
  "MCPServerAuthType",
76
79
  "register_tool",
77
80
  "build_model_from_schema",
81
+ "on_tool_before",
82
+ "on_tool_after",
83
+ "on_tool_error",
78
84
  # Knowledge bases
79
85
  "KnowledgeBases",
80
86
  "KnowledgeBase",
@@ -0,0 +1,8 @@
1
+ """
2
+ Backend module for xpander.ai SDK.
3
+ """
4
+
5
+ from .backend_module import Backend
6
+ from .decorators.on_auth_event import on_auth_event
7
+
8
+ __all__ = ["Backend", "on_auth_event"]
@@ -231,6 +231,7 @@ class Backend(ModuleBase):
231
231
  override: Optional[Dict[str, Any]] = None,
232
232
  tools: Optional[List[Callable]] = None,
233
233
  is_async: Optional[bool] = True,
234
+ auth_events_callback: Optional[Callable] = None,
234
235
  ) -> Dict[str, Any]:
235
236
  """
236
237
  Asynchronously resolve runtime arguments for the specified agent.
@@ -243,6 +244,27 @@ class Backend(ModuleBase):
243
244
  override (Optional[Dict[str, Any]]): Optional overrides for final arguments.
244
245
  tools (Optional[List[Callable]]): Optional additional tools to be added to the agent arguments.
245
246
  is_async (Optional[bool]): Is in Async Context?.
247
+ auth_events_callback (Optional[Callable]): Optional callback function (async or sync) that will be called for authentication events only.
248
+ Used specifically for authentication events (e.g., MCP OAuth flows requiring user login).
249
+ The callback signature must be: callback(agent: Agent, task: Task, event: TaskUpdateEvent)
250
+
251
+ Can be provided in two ways:
252
+ 1. Direct function: Pass the function directly to this parameter
253
+ 2. Decorator: Use @on_auth_event decorator and pass the decorated function
254
+
255
+ Example (direct function):
256
+ async def my_callback(agent, task, event):
257
+ print(f"Auth required: {event.data}")
258
+ args = await backend.aget_args(agent_id="...", auth_events_callback=my_callback)
259
+
260
+ Example (decorator):
261
+ from xpander_sdk import on_auth_event
262
+
263
+ @on_auth_event
264
+ async def handle_auth(agent, task, event):
265
+ print(f"Auth required: {event.data}")
266
+
267
+ args = await backend.aget_args(agent_id="...", auth_events_callback=handle_auth)
246
268
 
247
269
  Returns:
248
270
  Dict[str, Any]: Resolved argument dictionary to use with the agent.
@@ -267,7 +289,7 @@ class Backend(ModuleBase):
267
289
  "or set via the 'XPANDER_AGENT_ID' environment variable."
268
290
  )
269
291
 
270
- return await dispatch_get_args(agent=xpander_agent, task=task, override=override, tools=tools, is_async=is_async)
292
+ return await dispatch_get_args(agent=xpander_agent, task=task, override=override, tools=tools, is_async=is_async, auth_events_callback=auth_events_callback)
271
293
 
272
294
  def get_args(
273
295
  self,
@@ -277,6 +299,7 @@ class Backend(ModuleBase):
277
299
  task: Optional[Task] = None,
278
300
  override: Optional[Dict[str, Any]] = None,
279
301
  tools: Optional[List[Callable]] = None,
302
+ auth_events_callback: Optional[Callable] = None,
280
303
  ) -> Dict[str, Any]:
281
304
  """
282
305
  Synchronously resolve runtime arguments for the specified agent.
@@ -291,6 +314,27 @@ class Backend(ModuleBase):
291
314
  task (Optional[Task]): Optional Task object providing runtime input/output context.
292
315
  override (Optional[Dict[str, Any]]): Optional overrides for final arguments.
293
316
  tools (Optional[List[Callable]]): Optional additional tools to be added to the agent arguments.
317
+ auth_events_callback (Optional[Callable]): Optional callback function (async or sync) that will be called for authentication events only.
318
+ Used specifically for authentication events (e.g., MCP OAuth flows requiring user login).
319
+ The callback signature must be: callback(agent: Agent, task: Task, event: TaskUpdateEvent)
320
+
321
+ Can be provided in two ways:
322
+ 1. Direct function: Pass the function directly to this parameter
323
+ 2. Decorator: Use @on_auth_event decorator and pass the decorated function
324
+
325
+ Example (direct function):
326
+ def my_callback(agent, task, event):
327
+ print(f"Auth required: {event.data}")
328
+ args = backend.get_args(agent_id="...", auth_events_callback=my_callback)
329
+
330
+ Example (decorator):
331
+ from xpander_sdk import on_auth_event
332
+
333
+ @on_auth_event
334
+ def handle_auth(agent, task, event):
335
+ print(f"Auth required: {event.data}")
336
+
337
+ args = backend.get_args(agent_id="...", auth_events_callback=handle_auth)
294
338
 
295
339
  Returns:
296
340
  Dict[str, Any]: Resolved argument dictionary to use with the agent.
@@ -306,7 +350,8 @@ class Backend(ModuleBase):
306
350
  task=task,
307
351
  override=override,
308
352
  tools=tools,
309
- is_async=False
353
+ is_async=False,
354
+ auth_events_callback=auth_events_callback
310
355
  )
311
356
  )
312
357
 
@@ -0,0 +1,7 @@
1
+ """
2
+ Backend module decorators.
3
+ """
4
+
5
+ from .on_auth_event import on_auth_event
6
+
7
+ __all__ = ["on_auth_event"]
@@ -0,0 +1,131 @@
1
+ """
2
+ xpander_sdk.modules.backend.decorators.on_auth_event
3
+
4
+ This module provides the `@on_auth_event` decorator, which allows developers to define
5
+ authentication event handlers that respond to OAuth flows and authentication events.
6
+
7
+ The decorator ensures that the registered function:
8
+ - Accepts three parameters: agent (Agent), task (Task), and event (TaskUpdateEvent)
9
+ - Handles authentication events (e.g., MCP OAuth flows requiring user login)
10
+ - Can be either synchronous or asynchronous
11
+
12
+ Execution Notes:
13
+ - The handler is called when authentication events occur (e.g., OAuth login required)
14
+ - The event.type will always be "auth_event"
15
+ - The event.data contains authentication-specific information (e.g., OAuth login URL)
16
+ - Use this for displaying authentication prompts, handling OAuth flows, etc.
17
+
18
+ Example usage:
19
+ --------------
20
+ >>> from xpander_sdk import Backend, on_auth_event
21
+ >>>
22
+ >>> # Define handler with decorator - auto-registers globally
23
+ >>> @on_auth_event
24
+ ... async def handle_auth(agent, task, event):
25
+ ... print(f"Authentication required for {agent.name}")
26
+ ... print(f"Login URL: {event.data.get('auth_url')}")
27
+ ... # Display authentication prompt to user
28
+ >>>
29
+ >>> # Handler is automatically invoked when auth events occur
30
+ >>> backend = Backend()
31
+ >>> args = await backend.aget_args(agent_id="agent-123") # handle_auth is called automatically
32
+ """
33
+
34
+ from functools import wraps
35
+ from inspect import iscoroutinefunction, signature
36
+ from typing import Optional, Callable
37
+
38
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
39
+ from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
40
+ from xpander_sdk.modules.backend.events_registry import EventsRegistry, EventType
41
+
42
+
43
+ def on_auth_event(_func: Optional[Callable] = None):
44
+ """
45
+ Decorator to register a handler for authentication events.
46
+
47
+ The decorated function is automatically registered globally and will be called
48
+ whenever authentication events occur during agent execution (e.g., MCP OAuth flows
49
+ requiring user login). The function:
50
+ - Must accept three parameters: agent (Agent), task (Task), event (TaskUpdateEvent)
51
+ - Can be either synchronous or asynchronous
52
+ - Receives only authentication events (event.type == "auth_event")
53
+ - Is invoked automatically - no need to pass it to aget_args/get_args
54
+
55
+ Args:
56
+ _func (Optional[Callable]):
57
+ The function to decorate (for direct usage like `@on_auth_event`).
58
+
59
+ Raises:
60
+ TypeError: If the decorated function does not have the correct parameters.
61
+
62
+ Example:
63
+ >>> @on_auth_event
64
+ ... async def handle_oauth_login(agent, task, event):
65
+ ... print(f"Agent: {agent.name}")
66
+ ... print(f"Task: {task.id}")
67
+ ... print(f"Auth data: {event.data}")
68
+ ... # Handle OAuth flow
69
+
70
+ >>> @on_auth_event
71
+ ... def sync_auth_handler(agent, task, event):
72
+ ... if 'auth_url' in event.data:
73
+ ... print(f"Please visit: {event.data['auth_url']}")
74
+ """
75
+
76
+ def decorator(func: Callable) -> Callable:
77
+ sig = signature(func)
78
+ params = list(sig.parameters.keys())
79
+
80
+ # Validate function signature
81
+ if len(params) < 3:
82
+ raise TypeError(
83
+ f"Function '{func.__name__}' must accept 3 parameters: agent, task, event. "
84
+ f"Got {len(params)} parameters: {params}"
85
+ )
86
+
87
+ # Store the original function for later use
88
+ @wraps(func)
89
+ async def async_wrapper(agent: Agent, task: Task, event: TaskUpdateEvent):
90
+ return await func(agent, task, event)
91
+
92
+ @wraps(func)
93
+ def sync_wrapper(agent: Agent, task: Task, event: TaskUpdateEvent):
94
+ return func(agent, task, event)
95
+
96
+ wrapped = async_wrapper if iscoroutinefunction(func) else sync_wrapper
97
+
98
+ # Mark the function as an auth event handler
99
+ wrapped._is_auth_event_handler = True
100
+
101
+ # Register the handler in the singleton registry
102
+ registry = EventsRegistry()
103
+ registry.register_auth_event(wrapped)
104
+
105
+ return wrapped
106
+
107
+ if _func and callable(_func):
108
+ return decorator(_func)
109
+
110
+ return decorator
111
+
112
+
113
+ def get_registered_handlers():
114
+ """
115
+ Get all registered authentication event handlers.
116
+
117
+ Returns:
118
+ list: List of registered handler functions.
119
+ """
120
+ registry = EventsRegistry()
121
+ return registry.get_auth_handlers()
122
+
123
+
124
+ def clear_handlers():
125
+ """
126
+ Clear all registered authentication event handlers.
127
+
128
+ Useful for testing or when you want to reset the handlers.
129
+ """
130
+ registry = EventsRegistry()
131
+ registry.clear(EventType.AUTH_EVENT)
@@ -0,0 +1,172 @@
1
+ """
2
+ Events Registry - Singleton pattern for managing event handlers and hooks.
3
+
4
+ This registry supports multiple event types:
5
+ - Authentication events (OAuth flows)
6
+ - Tool call hooks (pre/post/error) - Coming soon
7
+ """
8
+
9
+ from typing import Callable, List, Optional, Dict, Any
10
+ from enum import Enum
11
+ import asyncio
12
+
13
+
14
+ class EventType(str, Enum):
15
+ """Supported event types in the backend module."""
16
+ AUTH_EVENT = "auth_event"
17
+ TOOL_PRE_CALL = "tool_pre_call" # Future: before tool execution
18
+ TOOL_POST_CALL = "tool_post_call" # Future: after successful tool execution
19
+ TOOL_ERROR = "tool_error" # Future: when tool execution fails
20
+
21
+
22
+ class EventsRegistry:
23
+ """
24
+ Singleton registry for managing event handlers and hooks.
25
+
26
+ This registry maintains handlers for different event types and provides
27
+ methods to register, retrieve, and invoke them.
28
+
29
+ Supported event types:
30
+ - AUTH_EVENT: Authentication events (e.g., MCP OAuth flows)
31
+ - TOOL_PRE_CALL: Before tool execution (future)
32
+ - TOOL_POST_CALL: After successful tool execution (future)
33
+ - TOOL_ERROR: When tool execution fails (future)
34
+ """
35
+
36
+ _instance: Optional['EventsRegistry'] = None
37
+ _handlers: Dict[EventType, List[Callable]] = {}
38
+
39
+ def __new__(cls):
40
+ if cls._instance is None:
41
+ cls._instance = super().__new__(cls)
42
+ cls._instance._handlers = {
43
+ EventType.AUTH_EVENT: [],
44
+ EventType.TOOL_PRE_CALL: [],
45
+ EventType.TOOL_POST_CALL: [],
46
+ EventType.TOOL_ERROR: [],
47
+ }
48
+ return cls._instance
49
+
50
+ def register(self, event_type: EventType, handler: Callable) -> None:
51
+ """
52
+ Register an event handler for a specific event type.
53
+
54
+ Args:
55
+ event_type (EventType): The type of event to handle.
56
+ handler (Callable): The handler function to register.
57
+ """
58
+ if event_type not in self._handlers:
59
+ self._handlers[event_type] = []
60
+
61
+ if handler not in self._handlers[event_type]:
62
+ self._handlers[event_type].append(handler)
63
+
64
+ def register_auth_event(self, handler: Callable) -> None:
65
+ """
66
+ Register an authentication event handler.
67
+
68
+ Args:
69
+ handler (Callable): The handler function to register.
70
+ """
71
+ self.register(EventType.AUTH_EVENT, handler)
72
+
73
+ def get_handlers(self, event_type: EventType) -> List[Callable]:
74
+ """
75
+ Get all registered handlers for a specific event type.
76
+
77
+ Args:
78
+ event_type (EventType): The type of event.
79
+
80
+ Returns:
81
+ List[Callable]: List of registered handler functions.
82
+ """
83
+ return self._handlers.get(event_type, []).copy()
84
+
85
+ def get_auth_handlers(self) -> List[Callable]:
86
+ """
87
+ Get all registered authentication event handlers.
88
+
89
+ Returns:
90
+ List[Callable]: List of registered auth handler functions.
91
+ """
92
+ return self.get_handlers(EventType.AUTH_EVENT)
93
+
94
+ def clear(self, event_type: Optional[EventType] = None) -> None:
95
+ """
96
+ Clear registered handlers.
97
+
98
+ Args:
99
+ event_type (Optional[EventType]): If provided, clear only handlers
100
+ for this event type. If None, clear all handlers.
101
+ """
102
+ if event_type:
103
+ if event_type in self._handlers:
104
+ self._handlers[event_type].clear()
105
+ else:
106
+ for handlers_list in self._handlers.values():
107
+ handlers_list.clear()
108
+
109
+ async def invoke_handlers(self, event_type: EventType, *args, **kwargs) -> None:
110
+ """
111
+ Invoke all registered handlers for a specific event type.
112
+
113
+ Args:
114
+ event_type (EventType): The type of event to invoke handlers for.
115
+ *args: Positional arguments to pass to handlers.
116
+ **kwargs: Keyword arguments to pass to handlers.
117
+ """
118
+ handlers = self._handlers.get(event_type, [])
119
+
120
+ for handler in handlers:
121
+ try:
122
+ if asyncio.iscoroutinefunction(handler):
123
+ await handler(*args, **kwargs)
124
+ else:
125
+ handler(*args, **kwargs)
126
+ except Exception as e:
127
+ # Log error but continue with other handlers
128
+ from loguru import logger
129
+ logger.error(f"Error in {event_type.value} handler {handler.__name__}: {e}")
130
+
131
+ async def invoke_auth_handlers(self, agent, task, event) -> None:
132
+ """
133
+ Invoke all registered authentication event handlers.
134
+
135
+ Args:
136
+ agent: The Agent object associated with the task.
137
+ task: The Task object being executed.
138
+ event: The TaskUpdateEvent containing authentication event data.
139
+ """
140
+ await self.invoke_handlers(EventType.AUTH_EVENT, agent, task, event)
141
+
142
+ def has_handlers(self, event_type: EventType) -> bool:
143
+ """
144
+ Check if any handlers are registered for a specific event type.
145
+
146
+ Args:
147
+ event_type (EventType): The type of event.
148
+
149
+ Returns:
150
+ bool: True if at least one handler is registered, False otherwise.
151
+ """
152
+ return len(self._handlers.get(event_type, [])) > 0
153
+
154
+ def has_auth_handlers(self) -> bool:
155
+ """
156
+ Check if any authentication event handlers are registered.
157
+
158
+ Returns:
159
+ bool: True if at least one auth handler is registered, False otherwise.
160
+ """
161
+ return self.has_handlers(EventType.AUTH_EVENT)
162
+
163
+ @classmethod
164
+ def reset(cls) -> None:
165
+ """
166
+ Reset the singleton instance.
167
+
168
+ Useful for testing to ensure a clean state.
169
+ """
170
+ if cls._instance is not None:
171
+ cls._instance.clear()
172
+ cls._instance = None
@@ -36,13 +36,14 @@ async def build_agent_args(
36
36
  override: Optional[Dict[str, Any]] = None,
37
37
  tools: Optional[List[Callable]] = None,
38
38
  is_async: Optional[bool] = True,
39
+ auth_events_callback: Optional[Callable] = None,
39
40
  ) -> Dict[str, Any]:
40
41
  model = _load_llm_model(agent=xpander_agent, override=override)
41
42
  args: Dict[str, Any] = {
42
43
  "id": xpander_agent.id,
43
44
  "store_events": True
44
45
  }
45
-
46
+
46
47
  _configure_output(args=args, agent=xpander_agent, task=task)
47
48
  _configure_session_storage(args=args, agent=xpander_agent, task=task)
48
49
  _configure_agentic_memory(args=args, agent=xpander_agent, task=task)
@@ -55,7 +56,7 @@ async def build_agent_args(
55
56
  # Configure pre-hooks (guardrails, etc.)
56
57
  _configure_pre_hooks(args=args, agent=xpander_agent, model=model)
57
58
 
58
- args["tools"] = await _resolve_agent_tools(agent=xpander_agent, task=task)
59
+ args["tools"] = await _resolve_agent_tools(agent=xpander_agent, task=task, auth_events_callback=auth_events_callback)
59
60
 
60
61
 
61
62
  if tools and len(tools) != 0:
@@ -102,7 +103,7 @@ async def build_agent_args(
102
103
  # convert to members
103
104
  members = await asyncio.gather(
104
105
  *[
105
- build_agent_args(xpander_agent=sub_agent, override=override, task=task, is_async=is_async)
106
+ build_agent_args(xpander_agent=sub_agent, override=override, task=task, is_async=is_async, auth_events_callback=auth_events_callback)
106
107
  for sub_agent in sub_agents
107
108
  ]
108
109
  )
@@ -891,7 +892,7 @@ def _configure_pre_hooks(args: Dict[str, Any], agent: Agent, model: Any) -> None
891
892
  args["pre_hooks"].append(openai_moderation_guardrail)
892
893
 
893
894
 
894
- async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> List[Any]:
895
+ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None, auth_events_callback: Optional[Callable] = None) -> List[Any]:
895
896
  mcp_servers = agent.mcp_servers
896
897
 
897
898
  # combine task mcps and agent mcps
@@ -955,7 +956,7 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
955
956
  if not task.input.user or not task.input.user.id:
956
957
  raise ValueError("MCP server with OAuth authentication detected but user id not set on the task (task.input.user.id)")
957
958
 
958
- auth_result: MCPOAuthGetTokenResponse = await authenticate_mcp_server(mcp_server=mcp,task=task,user_id=task.input.user.id)
959
+ auth_result: MCPOAuthGetTokenResponse = await authenticate_mcp_server(mcp_server=mcp,task=task,user_id=task.input.user.id, auth_events_callback=auth_events_callback)
959
960
  if not auth_result:
960
961
  raise ValueError("MCP Server authentication failed")
961
962
  if auth_result.type != MCPOAuthResponseType.TOKEN_READY:
@@ -10,6 +10,7 @@ async def dispatch_get_args(
10
10
  override: Optional[Dict[str, Any]] = None,
11
11
  tools: Optional[List[Callable]] = None,
12
12
  is_async: Optional[bool] = True,
13
+ auth_events_callback: Optional[Callable] = None,
13
14
  ) -> Dict[str, Any]:
14
15
  """
15
16
  Dispatch to the correct framework-specific argument resolver.
@@ -20,6 +21,7 @@ async def dispatch_get_args(
20
21
  override (Optional[Dict[str, Any]]): Dict of override values.
21
22
  tools (Optional[List[Callable]]): Optional additional tools to be added to the agent arguments.
22
23
  is_async (Optional[bool]): Is in Async Context?.
24
+ auth_events_callback (Optional[Callable]): Optional callback function (async or sync) that receives (agent, task, event) for authentication events only.
23
25
 
24
26
  Returns:
25
27
  Dict[str, Any]: Arguments for instantiating the framework agent.
@@ -28,7 +30,7 @@ async def dispatch_get_args(
28
30
  match agent.framework:
29
31
  case Framework.Agno:
30
32
  from .agno import build_agent_args
31
- return await build_agent_args(xpander_agent=agent, task=task, override=override, tools=tools, is_async=is_async)
33
+ return await build_agent_args(xpander_agent=agent, task=task, override=override, tools=tools, is_async=is_async, auth_events_callback=auth_events_callback)
32
34
  # case Framework.Langchain: # PLACEHOLDER
33
35
  # from .langchain import build_agent_args
34
36
  # return await build_agent_args(xpander_agent=agent, task=task, override=override)
@@ -1,30 +1,47 @@
1
1
  import asyncio
2
2
  from datetime import datetime, timezone
3
+ from typing import Callable, Optional
3
4
  from loguru import logger
4
5
  from xpander_sdk.consts.api_routes import APIRoute
5
6
  from xpander_sdk.core.xpander_api_client import APIClient
6
7
  from xpander_sdk.models.events import TaskUpdateEventType
8
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
7
9
  from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
8
10
  from xpander_sdk.modules.tools_repository.models.mcp import MCPOAuthGetTokenGenericResponse, MCPOAuthGetTokenResponse, MCPOAuthResponseType, MCPServerDetails
11
+ from xpander_sdk.modules.backend.events_registry import EventsRegistry
9
12
 
10
13
  POLLING_INTERVAL = 1 # every 1s
11
14
  MAX_WAIT_FOR_LOGIN = 600 # 10 mintutes
12
15
 
13
- async def push_event(task: Task, event: TaskUpdateEvent, event_type: TaskUpdateEventType):
16
+ async def push_event(task: Task, event: TaskUpdateEvent, event_type: TaskUpdateEventType, auth_events_callback: Optional[Callable] = None):
14
17
  client = APIClient(configuration=task.configuration)
18
+
19
+ evt = TaskUpdateEvent(
20
+ task_id=task.id,
21
+ organization_id=task.organization_id,
22
+ time=datetime.now(timezone.utc).isoformat(),
23
+ type=event_type,
24
+ data=event
25
+ )
26
+
15
27
  await client.make_request(
16
28
  path=APIRoute.PushExecutionEventToQueue.format(task_id=task.id),
17
29
  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=event_type,
24
- data=event
25
- ).model_dump_safe()
26
- ]
30
+ payload=[evt.model_dump_safe()]
27
31
  )
32
+
33
+ # Invoke both explicit callback and registered handlers
34
+ # 1. Call explicit callback if provided
35
+ if auth_events_callback:
36
+ if asyncio.iscoroutinefunction(auth_events_callback):
37
+ await auth_events_callback(task.configuration.state.agent, task, evt)
38
+ else:
39
+ auth_events_callback(task.configuration.state.agent, task, evt)
40
+
41
+ # 2. Always invoke registered handlers from EventsRegistry
42
+ registry = EventsRegistry()
43
+ if registry.has_auth_handlers():
44
+ await registry.invoke_auth_handlers(task.configuration.state.agent, task, evt)
28
45
 
29
46
  async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
30
47
  client = APIClient(configuration=task.configuration)
@@ -39,7 +56,7 @@ async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str) -> M
39
56
 
40
57
  return None
41
58
 
42
- async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
59
+ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str, auth_events_callback: Optional[Callable] = None) -> MCPOAuthGetTokenResponse:
43
60
  try:
44
61
  logger.info(f"Authenticating MCP Server {mcp_server.url}")
45
62
 
@@ -59,7 +76,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
59
76
  if result.type == MCPOAuthResponseType.LOGIN_REQUIRED:
60
77
  logger.info(f"Initiating login for MCP Server {mcp_server.url}")
61
78
  # Notify user about login requirement
62
- await push_event(task=task, event=result, event_type=TaskUpdateEventType.AuthEvent)
79
+ await push_event(task=task, event=result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
63
80
 
64
81
  # Poll for token with timeout
65
82
  elapsed_time = 0
@@ -73,7 +90,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
73
90
  logger.info(f"Successful login for MCP Server {mcp_server.url}")
74
91
  redacted_token_result = MCPOAuthGetTokenResponse(**token_result.model_dump_safe())
75
92
  redacted_token_result.data.access_token = "REDACTED"
76
- await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent)
93
+ await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
77
94
  return token_result
78
95
 
79
96
  # Timeout reached
@@ -83,7 +100,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
83
100
  logger.info(f"Token ready for MCP Server {mcp_server.url}")
84
101
  redacted_token_result = MCPOAuthGetTokenResponse(**result.model_dump_safe())
85
102
  redacted_token_result.data.access_token = "REDACTED"
86
- await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent)
103
+ await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
87
104
 
88
105
  return result
89
106
  except Exception as e:
@@ -0,0 +1,3 @@
1
+ from .on_tool import on_tool_before, on_tool_after, on_tool_error
2
+
3
+ __all__ = ["on_tool_before", "on_tool_after", "on_tool_error"]
@@ -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
@@ -41,6 +41,7 @@ from xpander_sdk.modules.tools_repository.utils.schemas import (
41
41
  schema_enforcement_block_and_descriptions,
42
42
  )
43
43
  from xpander_sdk.utils.event_loop import run_sync
44
+ from xpander_sdk.modules.events.decorators.on_tool import ToolHooksRegistry
44
45
 
45
46
 
46
47
  class Tool(XPanderSharedModel):
@@ -376,6 +377,15 @@ class Tool(XPanderSharedModel):
376
377
  task_id=task_id,
377
378
  )
378
379
 
380
+ # Execute before hooks
381
+ await ToolHooksRegistry.execute_before_hooks(
382
+ tool=self,
383
+ payload=payload,
384
+ payload_extension=payload_extension,
385
+ tool_call_id=tool_call_id,
386
+ agent_version=agent_version
387
+ )
388
+
379
389
  try:
380
390
  if self.schema and payload:
381
391
  try:
@@ -403,17 +413,26 @@ class Tool(XPanderSharedModel):
403
413
 
404
414
  tool_invocation_result.result = result
405
415
  tool_invocation_result.is_success = True
406
- return tool_invocation_result
407
-
408
- tool_invocation_result.result = await self.acall_remote_tool(
409
- agent_id=agent_id,
410
- agent_version=agent_version,
416
+ else:
417
+ tool_invocation_result.result = await self.acall_remote_tool(
418
+ agent_id=agent_id,
419
+ agent_version=agent_version,
420
+ payload=payload,
421
+ payload_extension=payload_extension,
422
+ configuration=configuration,
423
+ task_id=task_id,
424
+ )
425
+ tool_invocation_result.is_success = True
426
+
427
+ # Execute after hooks on success
428
+ await ToolHooksRegistry.execute_after_hooks(
429
+ tool=self,
411
430
  payload=payload,
412
431
  payload_extension=payload_extension,
413
- configuration=configuration,
414
- task_id=task_id,
432
+ tool_call_id=tool_call_id,
433
+ agent_version=agent_version,
434
+ result=tool_invocation_result.result
415
435
  )
416
- tool_invocation_result.is_success = True
417
436
 
418
437
  except Exception as e:
419
438
  tool_invocation_result.is_error = True
@@ -423,6 +442,16 @@ class Tool(XPanderSharedModel):
423
442
  else:
424
443
  tool_invocation_result.status_code = 500
425
444
  tool_invocation_result.result = str(e)
445
+
446
+ # Execute error hooks on failure
447
+ await ToolHooksRegistry.execute_error_hooks(
448
+ tool=self,
449
+ payload=payload,
450
+ payload_extension=payload_extension,
451
+ tool_call_id=tool_call_id,
452
+ agent_version=agent_version,
453
+ error=e
454
+ )
426
455
 
427
456
  return tool_invocation_result
428
457
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xpander-sdk
3
- Version: 2.0.178
3
+ Version: 2.0.180
4
4
  Summary: xpander.ai Backend-as-a-service for AI Agents - SDK
5
5
  Home-page: https://www.xpander.ai
6
6
  Author: xpanderAI
@@ -293,6 +293,89 @@ async for event in task.aevents():
293
293
  print(f"Event Data: {event.data}")
294
294
  ```
295
295
 
296
+ ### Authentication Events Callback
297
+
298
+ Handle authentication events in real-time. This callback is triggered only for authentication flows (e.g., MCP OAuth requiring user login).
299
+
300
+ **You can use both approaches simultaneously** - decorated handlers will always be invoked, and you can also pass an explicit callback for additional handling.
301
+
302
+ You can provide the callback in two ways:
303
+
304
+ #### Option 1: Direct Function
305
+
306
+ ```python
307
+ from xpander_sdk import Backend
308
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
309
+ from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
310
+ from agno.agent import Agent as AgnoAgent
311
+
312
+ # Define event callback (async or sync)
313
+ async def my_event_callback(agent: Agent, task: Task, event: TaskUpdateEvent):
314
+ """Called for authentication events only"""
315
+ # event.type will always be "auth_event"
316
+ print(f"Authentication required: {event.data}")
317
+ # Display login URL or handle OAuth flow
318
+
319
+ # Get args with callback
320
+ backend = Backend(configuration=config)
321
+ args = await backend.aget_args(
322
+ agent_id="agent-123",
323
+ task=my_task,
324
+ auth_events_callback=my_event_callback
325
+ )
326
+ ```
327
+
328
+ #### Option 2: Decorator (Auto-registered)
329
+
330
+ ```python
331
+ from xpander_sdk import Backend, on_auth_event
332
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
333
+ from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
334
+ from agno.agent import Agent as AgnoAgent
335
+
336
+ # Use decorator - auto-registers globally
337
+ @on_auth_event
338
+ async def handle_auth(agent: Agent, task: Task, event: TaskUpdateEvent):
339
+ # event.type will always be "auth_event"
340
+ print(f"Authentication required for {agent.name}")
341
+ print(f"Auth data: {event.data}")
342
+
343
+ # Decorated handler is automatically invoked - no need to pass it
344
+ backend = Backend(configuration=config)
345
+ args = await backend.aget_args(
346
+ agent_id="agent-123",
347
+ task=my_task
348
+ )
349
+ ```
350
+
351
+ #### Option 3: Combine Both
352
+
353
+ ```python
354
+ from xpander_sdk import Backend, on_auth_event
355
+
356
+ # Global handler for all auth events
357
+ @on_auth_event
358
+ async def log_auth(agent, task, event):
359
+ print(f"[GLOBAL] Auth event for {agent.name}")
360
+
361
+ # Additional one-time handler
362
+ async def custom_handler(agent, task, event):
363
+ print(f"[CUSTOM] Specific handling for this call")
364
+
365
+ # Both handlers will be invoked
366
+ args = await backend.aget_args(
367
+ agent_id="agent-123",
368
+ auth_events_callback=custom_handler # Optional additional callback
369
+ )
370
+
371
+ # Use with Agno
372
+ agno_agent = AgnoAgent(**args)
373
+ result = await agno_agent.arun(
374
+ input="Process this data",
375
+ stream=True
376
+ )
377
+ ```
378
+
296
379
  ### Task Activity Monitoring
297
380
 
298
381
  ```python
@@ -1,4 +1,4 @@
1
- xpander_sdk/__init__.py,sha256=subqRII3DHqi4FYdWS11fuliCXiOttOrd57m6zTW7pY,2671
1
+ xpander_sdk/__init__.py,sha256=34l3YcvIdkj81DTfMp_bgZgXpj2U1lTKpHQ0shwnc_8,2927
2
2
  xpander_sdk/consts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  xpander_sdk/consts/api_routes.py,sha256=dKUruLanvprTxZO15fR4OWe6ck8JQ8y64XmUJxsbeJY,2278
4
4
  xpander_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -27,19 +27,23 @@ xpander_sdk/modules/agents/sub_modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
27
27
  xpander_sdk/modules/agents/sub_modules/agent.py,sha256=ALAHTlzIt6Rpgz6wkkAfmJKsh8QYZ2WDbCiFMyUB6OU,36246
28
28
  xpander_sdk/modules/agents/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  xpander_sdk/modules/agents/utils/generic.py,sha256=XbG4OeHMQo4gVYCsasMlW_b8OoqS1xL3MlUZSjXivu0,81
30
- xpander_sdk/modules/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- xpander_sdk/modules/backend/backend_module.py,sha256=wYghMuNXEtXgoyMXBgbMhgE7wYcbRwXJcpEyybF30kA,18927
30
+ xpander_sdk/modules/backend/__init__.py,sha256=-NjikuZgHBhOM9xHML2vKsG0ICX9S2RKHktrWaODCBE,171
31
+ xpander_sdk/modules/backend/backend_module.py,sha256=A8NW5QqVy28O-E5eL1VU2iUHgtxyz7rFN7Ih0Jnv-TE,21713
32
+ xpander_sdk/modules/backend/events_registry.py,sha256=d0V-lsz3I3G1QB643EM1i-a5oJCiHnEfqBY_SmN2WrE,5983
33
+ xpander_sdk/modules/backend/decorators/__init__.py,sha256=ub9c8G0Ll6AuCvfcFB6rqR8iamMJxtcW7QjWw3WSkPU,106
34
+ xpander_sdk/modules/backend/decorators/on_auth_event.py,sha256=Xt_x9nncujMcF_SgM5hG6M-iZ6B-rDS97EPmgZkGdMk,4715
32
35
  xpander_sdk/modules/backend/frameworks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- xpander_sdk/modules/backend/frameworks/agno.py,sha256=6rBi-3wUPmSd5n1hFdhigjiLZ8jM5clLXaxwKiKxOWA,41159
34
- xpander_sdk/modules/backend/frameworks/dispatch.py,sha256=5dP4c37C42U53VjM2kkwIRwEw1i0IN3G0YESHH7J3OE,1557
36
+ xpander_sdk/modules/backend/frameworks/agno.py,sha256=N5XCXIo81MRhg4GVtB5NofbXiu_EDJC_Av_rCabZA-Y,41394
37
+ xpander_sdk/modules/backend/frameworks/dispatch.py,sha256=ht9hT5-cHATofQbWsbWeTARx51Hne3TNNNjw6KECRtA,1814
35
38
  xpander_sdk/modules/backend/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- xpander_sdk/modules/backend/utils/mcp_oauth.py,sha256=slmrtpjGM__g7mkhvnY9yNRtGHS-fDcLH82rIkBKxkA,4527
39
+ xpander_sdk/modules/backend/utils/mcp_oauth.py,sha256=K80bLjFW66TXm-wz5mbtZEeKjpeYr6ezhMFb5r3bhXA,5468
37
40
  xpander_sdk/modules/events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
41
  xpander_sdk/modules/events/events_module.py,sha256=DVlho7JxT6Jy8GeyuSakswmYwR18xqO2JcCJ-8Zc3s8,25317
39
- xpander_sdk/modules/events/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ xpander_sdk/modules/events/decorators/__init__.py,sha256=8GhR9afoLiS83a0OMLpn7TN2W6iTnlcVqos6bM34nac,129
40
43
  xpander_sdk/modules/events/decorators/on_boot.py,sha256=VGtoQcgs3g5bmx3Ze4QB_-ZwBESATYYVR0oZe35eCww,3076
41
44
  xpander_sdk/modules/events/decorators/on_shutdown.py,sha256=rFgChspnLDnZm9FS1K636dvZSQDkeugf2e3M83SDgAY,3127
42
45
  xpander_sdk/modules/events/decorators/on_task.py,sha256=G3jk0xzi3pqH96Bbut_GMJKExIlyyMYk4PbKfc6koa4,8551
46
+ xpander_sdk/modules/events/decorators/on_tool.py,sha256=ZacZ6tADjvl79ISqKxTSH1P0nZUS8C3mRwOL2SyLeZE,13750
43
47
  xpander_sdk/modules/events/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
48
  xpander_sdk/modules/events/models/deployments.py,sha256=6uwxFsybrZ-eHeohJzkm2RtQq4Eo_0xjHk7QouvszxU,1335
45
49
  xpander_sdk/modules/events/models/events.py,sha256=T_89pq48e7fMIbJcCbtM9Ocb6YKXQP7pbF6VbECiGcI,1550
@@ -71,7 +75,7 @@ xpander_sdk/modules/tools_repository/models/__init__.py,sha256=47DEQpj8HBSa-_TIm
71
75
  xpander_sdk/modules/tools_repository/models/mcp.py,sha256=qGpaiXKiuXw6gAcK8CW6ek6FkZNbBxDXUf1PWF6Tenw,1863
72
76
  xpander_sdk/modules/tools_repository/models/tool_invocation_result.py,sha256=Dhowt_fv8v8xWv7xMRJxo6hA8DawXKbWIrsJFMpt5H4,447
73
77
  xpander_sdk/modules/tools_repository/sub_modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
- xpander_sdk/modules/tools_repository/sub_modules/tool.py,sha256=ylSVfekQQd0vPCKXPWN67tlkyVT7R4WsSQtoEZcBETw,22521
78
+ xpander_sdk/modules/tools_repository/sub_modules/tool.py,sha256=rivnznxi6CrrOWE1rukkBRmad2H-rthhrelC7ei1IXM,23617
75
79
  xpander_sdk/modules/tools_repository/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
80
  xpander_sdk/modules/tools_repository/utils/generic.py,sha256=m9FRaVGzRUj23tB52rP9K4O-nTsMSt9iwMxMcYsqfiY,1770
77
81
  xpander_sdk/modules/tools_repository/utils/local_tools.py,sha256=zp5P8hVnRUJQb-w-2jCEMV5eUB_awmvYfY_rin5qvEw,1875
@@ -83,8 +87,8 @@ xpander_sdk/utils/generic.py,sha256=XrRj2-L8c0YWpfPdDyXE-pVL-6lKF9VpyZzKHQ4wuCc,
83
87
  xpander_sdk/utils/tools.py,sha256=lyFkq2yP7DxBkyXYVlnFRwDhQCvf0fZZMDm5fBycze4,1244
84
88
  xpander_sdk/utils/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
89
  xpander_sdk/utils/agents/compactization_agent.py,sha256=pqaf7LVSyPFaeXU62dMPY6iQ160TFga1KJ0Kgu1dIgg,14449
86
- xpander_sdk-2.0.178.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
87
- xpander_sdk-2.0.178.dist-info/METADATA,sha256=3KTc8cfWLwlD3AUb8_lHpWM6SGzsV_mjURapldMrxSA,15339
88
- xpander_sdk-2.0.178.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
89
- xpander_sdk-2.0.178.dist-info/top_level.txt,sha256=UCjnxQpsMy5Zoe7lmRuVDO6DI2V_6PgRFfm4oizRbVs,12
90
- xpander_sdk-2.0.178.dist-info/RECORD,,
90
+ xpander_sdk-2.0.180.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
91
+ xpander_sdk-2.0.180.dist-info/METADATA,sha256=NfaTIonGkunIZxzmX3C82qiGcKeewspcwnJhHcqIYOM,17880
92
+ xpander_sdk-2.0.180.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
93
+ xpander_sdk-2.0.180.dist-info/top_level.txt,sha256=UCjnxQpsMy5Zoe7lmRuVDO6DI2V_6PgRFfm4oizRbVs,12
94
+ xpander_sdk-2.0.180.dist-info/RECORD,,