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 +6 -0
- 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 +6 -5
- xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
- xpander_sdk/modules/backend/utils/mcp_oauth.py +31 -14
- xpander_sdk/modules/events/decorators/__init__.py +3 -0
- xpander_sdk/modules/events/decorators/on_tool.py +384 -0
- xpander_sdk/modules/tools_repository/sub_modules/tool.py +37 -8
- {xpander_sdk-2.0.178.dist-info → xpander_sdk-2.0.180.dist-info}/METADATA +84 -1
- {xpander_sdk-2.0.178.dist-info → xpander_sdk-2.0.180.dist-info}/RECORD +17 -13
- {xpander_sdk-2.0.178.dist-info → xpander_sdk-2.0.180.dist-info}/WHEEL +0 -0
- {xpander_sdk-2.0.178.dist-info → xpander_sdk-2.0.180.dist-info}/licenses/LICENSE +0 -0
- {xpander_sdk-2.0.178.dist-info → xpander_sdk-2.0.180.dist-info}/top_level.txt +0 -0
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",
|
|
@@ -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,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,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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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.
|
|
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=
|
|
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
|
|
31
|
-
xpander_sdk/modules/backend/backend_module.py,sha256=
|
|
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=
|
|
34
|
-
xpander_sdk/modules/backend/frameworks/dispatch.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
87
|
-
xpander_sdk-2.0.
|
|
88
|
-
xpander_sdk-2.0.
|
|
89
|
-
xpander_sdk-2.0.
|
|
90
|
-
xpander_sdk-2.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|