langchain-trigger-server 0.1.0__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.

Potentially problematic release.


This version of langchain-trigger-server might be problematic. Click here for more details.

@@ -0,0 +1,83 @@
1
+ """Core types and interfaces for the triggers framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+
10
+ class ProviderAuthInfo(BaseModel):
11
+ """Authentication info for a specific OAuth provider."""
12
+
13
+ token: Optional[str] = None
14
+ auth_required: bool = False
15
+ auth_url: Optional[str] = None
16
+ auth_id: Optional[str] = None
17
+
18
+
19
+ class UserAuthInfo(BaseModel):
20
+ """User authentication info containing OAuth tokens or auth requirements."""
21
+
22
+ user_id: str
23
+ providers: Dict[str, ProviderAuthInfo] = Field(default_factory=dict)
24
+
25
+ class Config:
26
+ arbitrary_types_allowed = True
27
+
28
+
29
+ class MetadataManager:
30
+ """Manages trigger registration metadata with database persistence."""
31
+
32
+ def __init__(self, database: Any, registration_id: str, initial_metadata: Dict[str, Any]):
33
+ self.database = database
34
+ self.registration_id = registration_id
35
+ self.metadata = initial_metadata.copy()
36
+
37
+ def get(self, key: str, default: Any = None) -> Any:
38
+ """Get a metadata value by key."""
39
+ return self.metadata.get(key, default)
40
+
41
+ async def update(self, updates: Dict[str, Any]) -> None:
42
+ """Update metadata and persist to database."""
43
+ # Update local state
44
+ self.metadata.update(updates)
45
+
46
+ # Persist to database
47
+ await self.database.update_trigger_metadata(self.registration_id, updates)
48
+
49
+
50
+
51
+ class AgentInvocationRequest(BaseModel):
52
+ """Request to invoke an AI agent."""
53
+
54
+ assistant_id: str
55
+ user_id: str
56
+ input_data: Any
57
+ thread_id: Optional[str] = None
58
+ metadata: Dict[str, Any] = Field(default_factory=dict)
59
+
60
+
61
+ class TriggerHandlerResult(BaseModel):
62
+ """Result returned by trigger handlers."""
63
+
64
+ invoke_agent: bool = Field(default=True, description="Whether to invoke agents for this event")
65
+ data: Optional[str] = Field(default=None, description="String data to send to agents")
66
+
67
+ def model_post_init(self, __context) -> None:
68
+ """Validate that data is provided when invoke_agent is True."""
69
+ if self.invoke_agent and not self.data:
70
+ raise ValueError("data field is required when invoke_agent is True")
71
+
72
+
73
+ class TriggerRegistrationResult(BaseModel):
74
+ """Result returned by registration handlers."""
75
+
76
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata to store with the registration")
77
+
78
+
79
+ class TriggerRegistrationModel(BaseModel):
80
+ """Base class for trigger resource models that define how webhooks are matched to registrations."""
81
+
82
+ class Config:
83
+ arbitrary_types_allowed = True
@@ -0,0 +1,16 @@
1
+ """Database module for trigger operations."""
2
+
3
+ from .interface import TriggerDatabaseInterface
4
+ from .supabase import SupabaseTriggerDatabase
5
+
6
+
7
+ def create_database(database_type: str = "supabase", **kwargs) -> TriggerDatabaseInterface:
8
+ """Factory function to create database implementation."""
9
+
10
+ if database_type == "supabase":
11
+ return SupabaseTriggerDatabase(**kwargs)
12
+ else:
13
+ raise ValueError(f"Unknown database type: {database_type}")
14
+
15
+
16
+ __all__ = ["TriggerDatabaseInterface", "SupabaseTriggerDatabase", "create_database"]
@@ -0,0 +1,150 @@
1
+ """Database interface for trigger operations."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Optional, Dict, Any
5
+
6
+
7
+ class TriggerDatabaseInterface(ABC):
8
+ """Abstract interface for trigger database operations."""
9
+
10
+ # ========== Trigger Templates ==========
11
+
12
+ @abstractmethod
13
+ async def create_trigger_template(
14
+ self,
15
+ id: str,
16
+ name: str,
17
+ description: str = None,
18
+ registration_schema: Dict = None
19
+ ) -> Optional[Dict[str, Any]]:
20
+ """Create a new trigger template."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ async def get_trigger_templates(self) -> List[Dict[str, Any]]:
25
+ """Get all available trigger templates."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ async def get_trigger_template(self, id: str) -> Optional[Dict[str, Any]]:
30
+ """Get a specific trigger template by ID."""
31
+ pass
32
+
33
+ # ========== Trigger Registrations ==========
34
+
35
+ @abstractmethod
36
+ async def create_trigger_registration(
37
+ self,
38
+ user_id: str,
39
+ template_id: str,
40
+ resource: Dict,
41
+ metadata: Dict = None
42
+ ) -> Optional[Dict[str, Any]]:
43
+ """Create a new trigger registration for a user."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ async def get_user_trigger_registrations(self, user_id: str) -> List[Dict[str, Any]]:
48
+ """Get all trigger registrations for a user."""
49
+ pass
50
+
51
+ @abstractmethod
52
+ async def get_trigger_registration(
53
+ self,
54
+ registration_id: str,
55
+ user_id: str = None
56
+ ) -> Optional[Dict[str, Any]]:
57
+ """Get a specific trigger registration."""
58
+ pass
59
+
60
+ @abstractmethod
61
+ async def find_registration_by_resource(
62
+ self,
63
+ template_id: str,
64
+ resource_data: Dict[str, Any]
65
+ ) -> Optional[Dict[str, Any]]:
66
+ """Find trigger registration by matching resource data."""
67
+ pass
68
+
69
+ @abstractmethod
70
+ async def update_trigger_metadata(
71
+ self,
72
+ registration_id: str,
73
+ metadata_updates: Dict,
74
+ user_id: str = None
75
+ ) -> bool:
76
+ """Update metadata for a trigger registration."""
77
+ pass
78
+
79
+ @abstractmethod
80
+ async def delete_trigger_registration(
81
+ self,
82
+ registration_id: str,
83
+ user_id: str = None
84
+ ) -> bool:
85
+ """Delete a trigger registration."""
86
+ pass
87
+
88
+ # ========== Agent-Trigger Links ==========
89
+
90
+ @abstractmethod
91
+ async def link_agent_to_trigger(
92
+ self,
93
+ agent_id: str,
94
+ registration_id: str,
95
+ created_by: str,
96
+ field_selection: Optional[Dict[str, bool]] = None
97
+ ) -> bool:
98
+ """Link an agent to a trigger registration with optional field selection."""
99
+ pass
100
+
101
+ @abstractmethod
102
+ async def unlink_agent_from_trigger(
103
+ self,
104
+ agent_id: str,
105
+ registration_id: str
106
+ ) -> bool:
107
+ """Unlink an agent from a trigger registration."""
108
+ pass
109
+
110
+ @abstractmethod
111
+ async def get_agents_for_trigger(self, registration_id: str) -> List[Dict[str, Any]]:
112
+ """Get all agent links for a trigger registration with field_selection."""
113
+ pass
114
+
115
+ @abstractmethod
116
+ async def get_triggers_for_agent(self, agent_id: str) -> List[Dict[str, Any]]:
117
+ """Get all trigger registrations linked to an agent."""
118
+ pass
119
+
120
+ @abstractmethod
121
+ async def set_agent_trigger_links(
122
+ self,
123
+ agent_id: str,
124
+ registration_ids: List[str],
125
+ created_by: str
126
+ ) -> bool:
127
+ """Replace all trigger links for an agent (atomic operation)."""
128
+ pass
129
+
130
+ @abstractmethod
131
+ async def replace_trigger_agent_links(
132
+ self,
133
+ registration_id: str,
134
+ agent_ids: List[str],
135
+ created_by: str
136
+ ) -> bool:
137
+ """Replace all agent links for a trigger (atomic operation)."""
138
+ pass
139
+
140
+ # ========== Helper Methods ==========
141
+
142
+ @abstractmethod
143
+ async def get_user_from_token(self, token: str) -> Optional[str]:
144
+ """Extract user ID from authentication token."""
145
+ pass
146
+
147
+ @abstractmethod
148
+ async def get_user_by_email(self, email: str) -> Optional[str]:
149
+ """Get user ID by email from trigger registrations."""
150
+ pass
@@ -0,0 +1,365 @@
1
+ """Supabase implementation of trigger database interface."""
2
+
3
+ import os
4
+ import logging
5
+ from typing import List, Optional, Dict, Any
6
+ from supabase import create_client, Client
7
+
8
+ from .interface import TriggerDatabaseInterface
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
14
+ """Supabase implementation of trigger database operations."""
15
+
16
+ def __init__(self, supabase_url: str = None, supabase_key: str = None):
17
+ self.supabase_url = supabase_url or os.getenv("SUPABASE_URL")
18
+ self.supabase_key = supabase_key or os.getenv("SUPABASE_KEY")
19
+
20
+ if not self.supabase_url or not self.supabase_key:
21
+ raise ValueError("SUPABASE_URL and SUPABASE_KEY environment variables are required")
22
+
23
+ self.client = create_client(self.supabase_url, self.supabase_key)
24
+ logger.info("Initialized SupabaseTriggerDatabase")
25
+
26
+
27
+ # ========== Trigger Templates ==========
28
+
29
+ async def create_trigger_template(
30
+ self,
31
+ id: str,
32
+ name: str,
33
+ description: str = None,
34
+ registration_schema: Dict = None
35
+ ) -> Optional[Dict[str, Any]]:
36
+ """Create a new trigger template."""
37
+ try:
38
+ data = {
39
+ "id": id,
40
+ "name": name,
41
+ "description": description,
42
+ "registration_schema": registration_schema or {}
43
+ }
44
+
45
+ response = self.client.table("trigger_templates").insert(data).execute()
46
+ return response.data[0] if response.data else None
47
+
48
+ except Exception as e:
49
+ logger.error(f"Error creating trigger template: {e}")
50
+ return None
51
+
52
+ async def get_trigger_templates(self) -> List[Dict[str, Any]]:
53
+ """Get all available trigger templates."""
54
+ try:
55
+ response = self.client.table("trigger_templates").select("*").execute()
56
+ return response.data or []
57
+ except Exception as e:
58
+ logger.error(f"Error getting trigger templates: {e}")
59
+ return []
60
+
61
+ async def get_trigger_template(self, id: str) -> Optional[Dict[str, Any]]:
62
+ """Get a specific trigger template by ID."""
63
+ try:
64
+ response = self.client.table("trigger_templates").select("*").eq("id", id).single().execute()
65
+ return response.data if response.data else None
66
+ except Exception as e:
67
+ # Don't log as error if template just doesn't exist (expected on first startup)
68
+ if "no rows returned" in str(e).lower() or "multiple (or no) rows returned" in str(e).lower():
69
+ logger.debug(f"Trigger template {id} not found in database")
70
+ else:
71
+ logger.error(f"Error getting trigger template {id}: {e}")
72
+ return None
73
+
74
+ # ========== Trigger Registrations ==========
75
+
76
+ async def create_trigger_registration(
77
+ self,
78
+ user_id: str,
79
+ template_id: str,
80
+ resource: Dict,
81
+ metadata: Dict = None
82
+ ) -> Optional[Dict[str, Any]]:
83
+ """Create a new trigger registration for a user."""
84
+ try:
85
+ # Verify template exists
86
+ template = await self.get_trigger_template(template_id)
87
+ if not template:
88
+ logger.error(f"Template not found for ID: {template_id}")
89
+ return None
90
+
91
+ data = {
92
+ "user_id": user_id,
93
+ "template_id": template_id,
94
+ "resource": resource,
95
+ "metadata": metadata or {},
96
+ "status": "active"
97
+ }
98
+
99
+ response = self.client.table("trigger_registrations").insert(data).execute()
100
+ return response.data[0] if response.data else None
101
+
102
+ except Exception as e:
103
+ logger.error(f"Error creating trigger registration: {e}")
104
+ return None
105
+
106
+ async def get_user_trigger_registrations(self, user_id: str) -> List[Dict[str, Any]]:
107
+ """Get all trigger registrations for a user."""
108
+ try:
109
+ response = self.client.table("trigger_registrations").select("""
110
+ *,
111
+ trigger_templates(id, name, description)
112
+ """).eq("user_id", user_id).order("created_at", desc=True).execute()
113
+
114
+ return response.data or []
115
+ except Exception as e:
116
+ logger.error(f"Error getting user trigger registrations: {e}")
117
+ return []
118
+
119
+ async def get_trigger_registration(self, registration_id: str, user_id: str = None) -> Optional[Dict[str, Any]]:
120
+ """Get a specific trigger registration."""
121
+ try:
122
+ query = self.client.table("trigger_registrations").select("*").eq("id", registration_id)
123
+ if user_id:
124
+ query = query.eq("user_id", user_id)
125
+
126
+ response = query.single().execute()
127
+ return response.data if response.data else None
128
+ except Exception as e:
129
+ logger.error(f"Error getting trigger registration {registration_id}: {e}")
130
+ return None
131
+
132
+ async def update_trigger_metadata(
133
+ self,
134
+ registration_id: str,
135
+ metadata_updates: Dict,
136
+ user_id: str = None
137
+ ) -> bool:
138
+ """Update metadata for a trigger registration."""
139
+ try:
140
+ # Get current registration to merge metadata
141
+ current = await self.get_trigger_registration(registration_id, user_id)
142
+ if not current:
143
+ return False
144
+
145
+ # Merge existing metadata with updates
146
+ current_metadata = current.get("metadata", {})
147
+ updated_metadata = {**current_metadata, **metadata_updates}
148
+
149
+ query = self.client.table("trigger_registrations").update({
150
+ "metadata": updated_metadata,
151
+ "updated_at": "NOW()"
152
+ }).eq("id", registration_id)
153
+
154
+ if user_id:
155
+ query = query.eq("user_id", user_id)
156
+
157
+ response = query.execute()
158
+ return bool(response.data)
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error updating trigger metadata: {e}")
162
+ return False
163
+
164
+ async def delete_trigger_registration(
165
+ self,
166
+ registration_id: str,
167
+ user_id: str = None
168
+ ) -> bool:
169
+ """Delete a trigger registration."""
170
+ try:
171
+ query = self.client.table("trigger_registrations").delete().eq("id", registration_id)
172
+ if user_id:
173
+ query = query.eq("user_id", user_id)
174
+
175
+ response = query.execute()
176
+ return True # Delete operations don't return data
177
+
178
+ except Exception as e:
179
+ logger.error(f"Error deleting trigger registration: {e}")
180
+ return False
181
+
182
+ async def find_registration_by_resource(
183
+ self,
184
+ template_id: str,
185
+ resource_data: Dict[str, Any]
186
+ ) -> Optional[Dict[str, Any]]:
187
+ """Find trigger registration by matching resource data."""
188
+ try:
189
+ # Build query to match against trigger_registrations with template_id filter
190
+ query = self.client.table("trigger_registrations").select(
191
+ "*, trigger_templates(id, name, description)"
192
+ ).eq("trigger_templates.id", template_id)
193
+
194
+ # Add resource field matches
195
+ for field, value in resource_data.items():
196
+ query = query.eq(f"resource->>{field}", value)
197
+
198
+ response = query.execute()
199
+
200
+ if response.data:
201
+ return response.data[0] # Return first match
202
+ return None
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error finding registration by resource: {e}")
206
+ return None
207
+
208
+ # ========== Agent-Trigger Links ==========
209
+
210
+ async def link_agent_to_trigger(
211
+ self,
212
+ agent_id: str,
213
+ registration_id: str,
214
+ created_by: str,
215
+ field_selection: Optional[Dict[str, bool]] = None
216
+ ) -> bool:
217
+ """Link an agent to a trigger registration with optional field selection."""
218
+ try:
219
+ data = {
220
+ "agent_id": agent_id,
221
+ "registration_id": registration_id,
222
+ "created_by": created_by,
223
+ "field_selection": field_selection
224
+ }
225
+
226
+ response = self.client.table("agent_trigger_links").insert(data).execute()
227
+ return bool(response.data)
228
+
229
+ except Exception as e:
230
+ logger.error(f"Error linking agent to trigger: {e}")
231
+ return False
232
+
233
+ async def unlink_agent_from_trigger(
234
+ self,
235
+ agent_id: str,
236
+ registration_id: str
237
+ ) -> bool:
238
+ """Unlink an agent from a trigger registration."""
239
+ try:
240
+ response = self.client.table("agent_trigger_links").delete().eq(
241
+ "agent_id", agent_id
242
+ ).eq("registration_id", registration_id).execute()
243
+
244
+ return True # Delete operations don't return data
245
+
246
+ except Exception as e:
247
+ logger.error(f"Error unlinking agent from trigger: {e}")
248
+ return False
249
+
250
+ async def get_agents_for_trigger(self, registration_id: str) -> List[Dict[str, Any]]:
251
+ """Get all agent links for a trigger registration with field_selection."""
252
+ try:
253
+ response = self.client.table("agent_trigger_links").select("agent_id, field_selection").eq(
254
+ "registration_id", registration_id
255
+ ).execute()
256
+
257
+ return response.data or []
258
+
259
+ except Exception as e:
260
+ logger.error(f"Error getting agents for trigger: {e}")
261
+ return []
262
+
263
+ async def get_triggers_for_agent(self, agent_id: str) -> List[Dict[str, Any]]:
264
+ """Get all trigger registrations linked to an agent."""
265
+ try:
266
+ response = self.client.table("agent_trigger_links").select("""
267
+ registration_id,
268
+ trigger_registrations(
269
+ *,
270
+ trigger_templates(id, name, description)
271
+ )
272
+ """).eq("agent_id", agent_id).execute()
273
+
274
+ return [row["trigger_registrations"] for row in response.data or []]
275
+
276
+ except Exception as e:
277
+ logger.error(f"Error getting triggers for agent: {e}")
278
+ return []
279
+
280
+ async def set_agent_trigger_links(
281
+ self,
282
+ agent_id: str,
283
+ registration_ids: List[str],
284
+ created_by: str
285
+ ) -> bool:
286
+ """Replace all trigger links for an agent (atomic operation)."""
287
+ try:
288
+ # Delete existing links
289
+ await self.client.table("agent_trigger_links").delete().eq("agent_id", agent_id).execute()
290
+
291
+ # Create new links
292
+ if registration_ids:
293
+ links = [
294
+ {
295
+ "agent_id": agent_id,
296
+ "registration_id": reg_id,
297
+ "created_by": created_by
298
+ }
299
+ for reg_id in registration_ids
300
+ ]
301
+
302
+ response = self.client.table("agent_trigger_links").insert(links).execute()
303
+ return bool(response.data)
304
+
305
+ return True # Successfully cleared all links
306
+
307
+ except Exception as e:
308
+ logger.error(f"Error setting agent trigger links: {e}")
309
+ return False
310
+
311
+ async def replace_trigger_agent_links(
312
+ self,
313
+ registration_id: str,
314
+ agent_ids: List[str],
315
+ created_by: str
316
+ ) -> bool:
317
+ """Replace all agent links for a trigger (atomic operation)."""
318
+ try:
319
+ # Delete existing links
320
+ await self.client.table("agent_trigger_links").delete().eq("registration_id", registration_id).execute()
321
+
322
+ # Create new links
323
+ if agent_ids:
324
+ links = [
325
+ {
326
+ "agent_id": agent_id,
327
+ "registration_id": registration_id,
328
+ "created_by": created_by
329
+ }
330
+ for agent_id in agent_ids
331
+ ]
332
+
333
+ response = self.client.table("agent_trigger_links").insert(links).execute()
334
+ return bool(response.data)
335
+
336
+ return True # Successfully cleared all links
337
+
338
+ except Exception as e:
339
+ logger.error(f"Error replacing trigger agent links: {e}")
340
+ return False
341
+
342
+ # ========== Helper Methods ==========
343
+
344
+ async def get_user_from_token(self, token: str) -> Optional[str]:
345
+ """Extract user ID from JWT token via Supabase auth."""
346
+ try:
347
+ client = self._create_user_client(token)
348
+ response = client.auth.get_user(token)
349
+ return response.user.id if response.user else None
350
+ except Exception as e:
351
+ logger.error(f"Error getting user from token: {e}")
352
+ return None
353
+
354
+ async def get_user_by_email(self, email: str) -> Optional[str]:
355
+ """Get user ID by email from trigger registrations."""
356
+ try:
357
+ response = self.client.table("trigger_registrations").select("user_id").eq(
358
+ "resource->>email", email
359
+ ).limit(1).execute()
360
+
361
+ return response.data[0]["user_id"] if response.data else None
362
+
363
+ except Exception as e:
364
+ logger.error(f"Error getting user by email: {e}")
365
+ return None
@@ -0,0 +1,75 @@
1
+ """Trigger system - templates with registration and webhook handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, get_type_hints
7
+ from .core import UserAuthInfo, TriggerRegistrationModel, TriggerHandlerResult, TriggerRegistrationResult, MetadataManager
8
+ from pydantic import BaseModel
9
+
10
+ class TriggerTemplate:
11
+ """A trigger template with registration handler and main handler."""
12
+
13
+ def __init__(
14
+ self,
15
+ id: str,
16
+ name: str,
17
+ description: str,
18
+ registration_model: Type[BaseModel],
19
+
20
+ registration_handler,
21
+ trigger_handler,
22
+ registration_resolver,
23
+
24
+ oauth_providers: Optional[Dict[str, List[str]]] = None,
25
+ ):
26
+ self.id = id
27
+ self.name = name
28
+ self.description = description
29
+ self.registration_model = registration_model
30
+ self.registration_handler = registration_handler
31
+ self.trigger_handler = trigger_handler
32
+ self.registration_resolver = registration_resolver
33
+ self.oauth_providers = oauth_providers or {}
34
+
35
+ self._validate_handler_signatures()
36
+
37
+ def _validate_handler_signatures(self):
38
+ """Validate that all handler functions have the correct signatures."""
39
+ # Expected: async def handler(registration: RegistrationModel, auth_user: UserAuthInfo) -> TriggerRegistrationResult
40
+ self._validate_handler("registration_handler", self.registration_handler, [self.registration_model, UserAuthInfo], TriggerRegistrationResult)
41
+
42
+ # Expected: async def handler(payload: Dict[str, Any], auth_user: UserAuthInfo, metadata: MetadataManager) -> TriggerHandlerResult
43
+ self._validate_handler("trigger_handler", self.trigger_handler, [Dict[str, Any], UserAuthInfo, MetadataManager], TriggerHandlerResult)
44
+
45
+ # Expected: async def resolver(payload: Dict[str, Any]) -> RegistrationModel
46
+ self._validate_handler("registration_resolver", self.registration_resolver, [Dict[str, Any]], self.registration_model)
47
+
48
+ def _validate_handler(self, handler_name: str, handler_func, expected_types: List[Type], expected_return_type: Type = None):
49
+ """Common validation logic for all handler functions."""
50
+ if not inspect.iscoroutinefunction(handler_func):
51
+ raise TypeError(f"{handler_name} for trigger '{self.id}' must be async")
52
+
53
+ sig = inspect.signature(handler_func)
54
+ params = list(sig.parameters.values())
55
+ expected_param_count = len(expected_types)
56
+
57
+ if len(params) != expected_param_count:
58
+ raise TypeError(f"{handler_name} for trigger '{self.id}' must have {expected_param_count} parameters, got {len(params)}")
59
+
60
+ hints = get_type_hints(handler_func)
61
+ param_names = list(sig.parameters.keys())
62
+
63
+ # Check each parameter type if type hints are available
64
+ for i, expected_type in enumerate(expected_types):
65
+ if param_names[i] in hints and hints[param_names[i]] != expected_type:
66
+ expected_name = getattr(expected_type, '__name__', str(expected_type))
67
+ raise TypeError(f"{handler_name} for trigger '{self.id}': param {i+1} should be {expected_name}")
68
+
69
+ # Check return type if expected and available
70
+ if expected_return_type and 'return' in hints:
71
+ actual_return_type = hints['return']
72
+ if actual_return_type != expected_return_type:
73
+ expected_name = getattr(expected_return_type, '__name__', str(expected_return_type))
74
+ actual_name = getattr(actual_return_type, '__name__', str(actual_return_type))
75
+ raise TypeError(f"{handler_name} for trigger '{self.id}': return type should be {expected_name}, got {actual_name}")