xpander-sdk 2.0.144__py3-none-any.whl → 2.0.192__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xpander_sdk/__init__.py +6 -0
- xpander_sdk/consts/api_routes.py +9 -0
- xpander_sdk/models/activity.py +65 -0
- xpander_sdk/models/compactization.py +112 -0
- xpander_sdk/models/deep_planning.py +18 -0
- xpander_sdk/models/events.py +6 -0
- xpander_sdk/models/frameworks.py +2 -2
- xpander_sdk/models/generic.py +27 -0
- xpander_sdk/models/notifications.py +98 -0
- xpander_sdk/models/orchestrations.py +271 -0
- xpander_sdk/modules/agents/models/agent.py +11 -5
- xpander_sdk/modules/agents/sub_modules/agent.py +25 -10
- xpander_sdk/modules/backend/__init__.py +8 -0
- xpander_sdk/modules/backend/backend_module.py +47 -2
- xpander_sdk/modules/backend/decorators/__init__.py +7 -0
- xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
- xpander_sdk/modules/backend/events_registry.py +172 -0
- xpander_sdk/modules/backend/frameworks/agno.py +377 -15
- xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
- xpander_sdk/modules/backend/utils/mcp_oauth.py +37 -25
- xpander_sdk/modules/events/decorators/__init__.py +3 -0
- xpander_sdk/modules/events/decorators/on_tool.py +384 -0
- xpander_sdk/modules/events/events_module.py +28 -1
- xpander_sdk/modules/tasks/models/task.py +3 -14
- xpander_sdk/modules/tasks/sub_modules/task.py +276 -84
- xpander_sdk/modules/tools_repository/models/mcp.py +1 -0
- xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
- xpander_sdk/modules/tools_repository/tools_repository_module.py +6 -2
- xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
- xpander_sdk/utils/agents/__init__.py +0 -0
- xpander_sdk/utils/agents/compactization_agent.py +257 -0
- xpander_sdk/utils/generic.py +5 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +224 -14
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +37 -24
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/top_level.txt +0 -0
|
@@ -10,6 +10,7 @@ from enum import Enum
|
|
|
10
10
|
from typing import Dict, List, Literal, Optional, Type
|
|
11
11
|
from pydantic import BaseModel, computed_field
|
|
12
12
|
|
|
13
|
+
from xpander_sdk.models.orchestrations import OrchestrationIterativeStrategy, OrchestrationRetryStrategy, OrchestrationStopStrategy
|
|
13
14
|
from xpander_sdk.models.shared import XPanderSharedModel
|
|
14
15
|
from xpander_sdk.modules.tools_repository.models.mcp import MCPServerDetails
|
|
15
16
|
|
|
@@ -383,7 +384,10 @@ class AgentGraphItem(BaseModel):
|
|
|
383
384
|
llm_settings: Optional[List[AgentGraphItemLLMSettings]] = []
|
|
384
385
|
is_first: Optional[bool] = False
|
|
385
386
|
|
|
386
|
-
|
|
387
|
+
class LLMReasoningEffort(str, Enum):
|
|
388
|
+
Low = "low"
|
|
389
|
+
Medium = "medium"
|
|
390
|
+
High = "high"
|
|
387
391
|
|
|
388
392
|
class AIAgentConnectivityDetailsA2AAuthType(str, Enum):
|
|
389
393
|
NoAuth = "none"
|
|
@@ -417,12 +421,14 @@ class AgentType(str, Enum):
|
|
|
417
421
|
Regular: Standard agent for individual task execution.
|
|
418
422
|
A2A: Agent that is used via A2A protocol.
|
|
419
423
|
Curl: Custom Agent that is used via curl.
|
|
424
|
+
Orchestration: marks the agent as an Orchestration object.
|
|
420
425
|
"""
|
|
421
426
|
|
|
422
427
|
Manager = "manager"
|
|
423
428
|
Regular = "regular"
|
|
424
429
|
A2A = "a2a"
|
|
425
430
|
Curl = "curl"
|
|
431
|
+
Orchestration = "orchestration"
|
|
426
432
|
|
|
427
433
|
|
|
428
434
|
@dataclass
|
|
@@ -468,7 +474,7 @@ class AgentOutput(BaseModel):
|
|
|
468
474
|
is_markdown: Optional[bool] = False
|
|
469
475
|
use_json_mode: Optional[bool] = False
|
|
470
476
|
|
|
471
|
-
class
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
477
|
+
class TaskLevelStrategies(XPanderSharedModel):
|
|
478
|
+
retry_strategy: Optional[OrchestrationRetryStrategy] = None
|
|
479
|
+
iterative_strategy: Optional[OrchestrationIterativeStrategy] = None
|
|
480
|
+
stop_strategy: Optional[OrchestrationStopStrategy] = None
|
|
@@ -19,6 +19,8 @@ from xpander_sdk.core.xpander_api_client import APIClient
|
|
|
19
19
|
from xpander_sdk.exceptions.module_exception import ModuleException
|
|
20
20
|
from xpander_sdk.models.configuration import Configuration
|
|
21
21
|
from xpander_sdk.models.frameworks import AgnoSettings, Framework
|
|
22
|
+
from xpander_sdk.models.notifications import NotificationSettings
|
|
23
|
+
from xpander_sdk.models.orchestrations import OrchestrationNode
|
|
22
24
|
from xpander_sdk.modules.agents.utils.generic import get_db_schema_name
|
|
23
25
|
from xpander_sdk.modules.knowledge_bases.models.knowledge_bases import (
|
|
24
26
|
KnowledgeBaseSearchResult,
|
|
@@ -39,8 +41,10 @@ from xpander_sdk.modules.agents.models.agent import (
|
|
|
39
41
|
AgentStatus,
|
|
40
42
|
AgentType,
|
|
41
43
|
DatabaseConnectionString,
|
|
42
|
-
|
|
44
|
+
LLMReasoningEffort,
|
|
45
|
+
TaskLevelStrategies,
|
|
43
46
|
)
|
|
47
|
+
from xpander_sdk.models.generic import LLMCredentials
|
|
44
48
|
from xpander_sdk.modules.agents.models.knowledge_bases import AgentKnowledgeBase
|
|
45
49
|
from xpander_sdk.modules.knowledge_bases.knowledge_bases_module import KnowledgeBases
|
|
46
50
|
from xpander_sdk.modules.knowledge_bases.sub_modules.knowledge_base import KnowledgeBase
|
|
@@ -151,6 +155,9 @@ class Agent(XPanderSharedModel):
|
|
|
151
155
|
using_nemo: Optional[bool]
|
|
152
156
|
model_provider: str
|
|
153
157
|
model_name: str
|
|
158
|
+
llm_reasoning_effort: Optional[LLMReasoningEffort] = LLMReasoningEffort.Medium
|
|
159
|
+
deep_planning: Optional[bool] = False
|
|
160
|
+
enforce_deep_planning: Optional[bool] = False
|
|
154
161
|
llm_api_base: Optional[str]
|
|
155
162
|
webhook_url: Optional[str]
|
|
156
163
|
created_at: Optional[datetime]
|
|
@@ -161,6 +168,9 @@ class Agent(XPanderSharedModel):
|
|
|
161
168
|
llm_credentials: Optional[LLMCredentials]
|
|
162
169
|
expected_output: Optional[str]
|
|
163
170
|
agno_settings: Optional[AgnoSettings]
|
|
171
|
+
orchestration_nodes: Optional[List[OrchestrationNode]] = []
|
|
172
|
+
notification_settings: Optional[NotificationSettings] = {}
|
|
173
|
+
task_level_strategies: Optional[TaskLevelStrategies] = None
|
|
164
174
|
|
|
165
175
|
Example:
|
|
166
176
|
>>> agent = Agent(id="agent123", name="Example Agent")
|
|
@@ -192,6 +202,9 @@ class Agent(XPanderSharedModel):
|
|
|
192
202
|
using_nemo: Optional[bool] = False
|
|
193
203
|
model_provider: str
|
|
194
204
|
model_name: str
|
|
205
|
+
llm_reasoning_effort: Optional[LLMReasoningEffort] = LLMReasoningEffort.Medium
|
|
206
|
+
deep_planning: Optional[bool] = False
|
|
207
|
+
enforce_deep_planning: Optional[bool] = False
|
|
195
208
|
llm_api_base: Optional[str] = None
|
|
196
209
|
webhook_url: Optional[str] = None
|
|
197
210
|
created_at: Optional[datetime] = None
|
|
@@ -202,6 +215,9 @@ class Agent(XPanderSharedModel):
|
|
|
202
215
|
llm_credentials: Optional[LLMCredentials] = None
|
|
203
216
|
expected_output: Optional[str] = ""
|
|
204
217
|
agno_settings: Optional[AgnoSettings] = AgnoSettings()
|
|
218
|
+
orchestration_nodes: Optional[List[OrchestrationNode]] = []
|
|
219
|
+
notification_settings: Optional[NotificationSettings] = {}
|
|
220
|
+
task_level_strategies: Optional[TaskLevelStrategies] = None
|
|
205
221
|
|
|
206
222
|
_connection_string: Optional[DatabaseConnectionString] = None
|
|
207
223
|
|
|
@@ -621,12 +637,12 @@ class Agent(XPanderSharedModel):
|
|
|
621
637
|
schema = get_db_schema_name(agent_id=self.id)
|
|
622
638
|
|
|
623
639
|
client_type = AsyncPostgresDb if async_db else PostgresDb
|
|
624
|
-
|
|
640
|
+
|
|
625
641
|
return client_type(
|
|
626
642
|
db_schema=schema,
|
|
627
643
|
db_url=connection_string.connection_uri.uri.replace("postgresql", "postgresql+psycopg"+("_async" if async_db else "")),
|
|
628
644
|
)
|
|
629
|
-
|
|
645
|
+
|
|
630
646
|
def get_db(self) -> Any:
|
|
631
647
|
"""
|
|
632
648
|
Synchronously retrieve the db for this agent.
|
|
@@ -744,9 +760,9 @@ class Agent(XPanderSharedModel):
|
|
|
744
760
|
Example:
|
|
745
761
|
>>> sessions = await agent.aget_user_sessions(user_id="user_123")
|
|
746
762
|
"""
|
|
747
|
-
db = await self.aget_db()
|
|
763
|
+
db = await self.aget_db(async_db=True)
|
|
748
764
|
from agno.db import SessionType
|
|
749
|
-
sessions = await
|
|
765
|
+
sessions = await db.get_sessions(user_id=user_id, limit=50, session_type = SessionType.TEAM if self.is_a_team else SessionType.AGENT)
|
|
750
766
|
return sessions
|
|
751
767
|
|
|
752
768
|
def get_user_sessions(self, user_id: str):
|
|
@@ -790,10 +806,9 @@ class Agent(XPanderSharedModel):
|
|
|
790
806
|
Example:
|
|
791
807
|
>>> session = await agent.aget_session(session_id="sess_456")
|
|
792
808
|
"""
|
|
793
|
-
db = await self.aget_db()
|
|
809
|
+
db = await self.aget_db(async_db=True)
|
|
794
810
|
from agno.db import SessionType
|
|
795
|
-
|
|
796
|
-
return await session
|
|
811
|
+
return await db.get_session(session_id=session_id, session_type = SessionType.TEAM if self.is_a_team else SessionType.AGENT)
|
|
797
812
|
|
|
798
813
|
def get_session(self, session_id: str):
|
|
799
814
|
"""
|
|
@@ -834,8 +849,8 @@ class Agent(XPanderSharedModel):
|
|
|
834
849
|
Example:
|
|
835
850
|
>>> await agent.adelete_session(session_id="sess_456")
|
|
836
851
|
"""
|
|
837
|
-
db = await self.aget_db(async_db=
|
|
838
|
-
await
|
|
852
|
+
db = await self.aget_db(async_db=True)
|
|
853
|
+
await db.delete_session(session_id=session_id)
|
|
839
854
|
|
|
840
855
|
def delete_session(self, session_id: str):
|
|
841
856
|
"""
|
|
@@ -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
|