langchain-trigger-server 0.3.4__py3-none-any.whl → 0.3.5__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-trigger-server
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Generic event-driven triggers framework
5
5
  Project-URL: Homepage, https://github.com/langchain-ai/open-agent-platform
6
6
  Project-URL: Repository, https://github.com/langchain-ai/open-agent-platform
@@ -0,0 +1,14 @@
1
+ langchain_triggers/__init__.py,sha256=WoW9LC_FJRs42mLWq2iuM-jjPow2Rue50q2zm56Oul0,536
2
+ langchain_triggers/app.py,sha256=PoyrD7whOJoiRVaCJTXrzprWIyR6gF1i-zwCks-Qscs,40349
3
+ langchain_triggers/core.py,sha256=q7vHsjFuLqydWt9JxAPG4SPT4Z3uoouoqtXjTZoknwk,3669
4
+ langchain_triggers/cron_manager.py,sha256=5cNRUE8xMGhIVBKJVJZhxGuzJTyoBepv3QmH0f9EyEg,16342
5
+ langchain_triggers/decorators.py,sha256=ZhWgUjbl2jUbvGSlqgThebd0tPgqXoQ02D7BcKZ9aIs,4675
6
+ langchain_triggers/auth/__init__.py,sha256=RtDKuBoKYuyHzLNpKr74cmALO0PhHlWO9Ho7k3CUYFE,349
7
+ langchain_triggers/auth/slack_hmac.py,sha256=kiwjhTXITgQvLAtEcOv8BnnWJRJcxaQ9dXkQm3JJDQ4,2948
8
+ langchain_triggers/database/__init__.py,sha256=8OxGLTh2VWQ-GVVFjrOOL1qdgQ9lkmjUV1racLGuk7E,135
9
+ langchain_triggers/database/interface.py,sha256=Wu1MOl1rugFFVcDVM4nEMgyvwW5JxUCNpS3oD6EwkCQ,3716
10
+ langchain_triggers/triggers/__init__.py,sha256=Uw1544gxzN4XDRn2RzpZ5EAG6EAF38ZYQtVvlciEsMs,146
11
+ langchain_triggers/triggers/cron_trigger.py,sha256=YTscULtB8JesnB24IWZl0LDMkf-A9Y6dksAk7GLFJiQ,3355
12
+ langchain_trigger_server-0.3.5.dist-info/METADATA,sha256=Ku7_dTuqu8Ab2KbwabNO9QTlWiQprwgaReuD8ARRxPU,1438
13
+ langchain_trigger_server-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ langchain_trigger_server-0.3.5.dist-info/RECORD,,
langchain_triggers/app.py CHANGED
@@ -23,7 +23,7 @@ from .auth.slack_hmac import (
23
23
  )
24
24
  from .core import TriggerType
25
25
  from .cron_manager import CronTriggerManager
26
- from .database import TriggerDatabaseInterface, create_database
26
+ from .database import TriggerDatabaseInterface
27
27
  from .decorators import TriggerTemplate
28
28
 
29
29
  logger = logging.getLogger(__name__)
@@ -68,17 +68,20 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
68
68
  return await call_next(request)
69
69
 
70
70
  try:
71
- # Run mandatory custom authentication
72
71
  identity = await self.auth_handler({}, dict(request.headers))
73
-
74
- if not identity or not identity.get("identity"):
72
+ if (
73
+ not identity
74
+ or not identity.get("identity")
75
+ or not identity.get("tenant_id")
76
+ ):
77
+ logger.error(
78
+ f"Authentication failed: missing required fields (identity={bool(identity.get('identity') if identity else None)}, tenant_id={bool(identity.get('tenant_id') if identity else None)})"
79
+ )
75
80
  return Response(
76
- content='{"detail": "Authentication required"}',
81
+ content='{"detail": "Authentication required - identity and tenant_id must be provided"}',
77
82
  status_code=401,
78
83
  media_type="application/json",
79
84
  )
80
-
81
- # Store identity in request state for endpoints to access
82
85
  request.state.current_user = identity
83
86
 
84
87
  except Exception as e:
@@ -105,9 +108,7 @@ class TriggerServer:
105
108
  def __init__(
106
109
  self,
107
110
  auth_handler: Callable,
108
- database: TriggerDatabaseInterface | None = None,
109
- database_type: str | None = "supabase",
110
- **database_kwargs: Any,
111
+ database: TriggerDatabaseInterface,
111
112
  ):
112
113
  # Configure uvicorn logging to use consistent formatting
113
114
  self._configure_uvicorn_logging()
@@ -117,15 +118,7 @@ class TriggerServer:
117
118
  description="Event-driven triggers framework",
118
119
  version="0.1.0",
119
120
  )
120
-
121
- # Configure database: allow either instance injection or factory creation
122
- # Defaults to Supabase for backward compatibility
123
- if database and database_type != "supabase":
124
- raise ValueError("Provide either 'database' or 'database_type', not both")
125
- if database is not None:
126
- self.database = database
127
- else:
128
- self.database = create_database(database_type, **database_kwargs)
121
+ self.database = database
129
122
  self.auth_handler = auth_handler
130
123
 
131
124
  # LangGraph configuration
@@ -286,14 +279,15 @@ class TriggerServer:
286
279
  async def api_list_registrations(
287
280
  current_user: dict[str, Any] = Depends(get_current_user),
288
281
  ) -> dict[str, Any]:
289
- """List user's trigger registrations (user-scoped)."""
282
+ """List user's trigger registrations (user and tenant-scoped)."""
290
283
  try:
291
284
  user_id = current_user["identity"]
285
+ tenant_id = current_user["tenant_id"]
292
286
 
293
287
  # Get user's trigger registrations with linked agents in a single query
294
288
  user_registrations = (
295
289
  await self.database.get_user_trigger_registrations_with_agents(
296
- user_id
290
+ user_id, tenant_id
297
291
  )
298
292
  )
299
293
 
@@ -329,6 +323,7 @@ class TriggerServer:
329
323
  logger.info(f"Registration payload received: {payload}")
330
324
 
331
325
  user_id = current_user["identity"]
326
+ tenant_id = current_user["tenant_id"]
332
327
  trigger_id = payload.get("type")
333
328
  if not trigger_id:
334
329
  raise HTTPException(
@@ -340,7 +335,6 @@ class TriggerServer:
340
335
  raise HTTPException(
341
336
  status_code=400, detail=f"Unknown trigger type: {trigger_id}"
342
337
  )
343
- client_metadata = payload.pop("metadata", None)
344
338
 
345
339
  # Parse payload into registration model first
346
340
  try:
@@ -350,11 +344,12 @@ class TriggerServer:
350
344
  status_code=400, detail=f"Invalid payload for trigger: {str(e)}"
351
345
  )
352
346
 
353
- # Check for duplicate registration based on resource data within this user's scope
347
+ # Check for duplicate registration based on resource data within this user's tenant scope
354
348
  resource_dict = registration_instance.model_dump()
355
349
  existing_registration = (
356
350
  await self.database.find_user_registration_by_resource(
357
351
  user_id=user_id,
352
+ tenant_id=tenant_id,
358
353
  template_id=trigger.id,
359
354
  resource_data=resource_dict,
360
355
  )
@@ -386,16 +381,12 @@ class TriggerServer:
386
381
 
387
382
  resource_dict = registration_instance.model_dump()
388
383
 
389
- merged_metadata = {}
390
- if client_metadata:
391
- merged_metadata["client_metadata"] = client_metadata
392
- merged_metadata.update(result.metadata)
393
-
394
384
  registration = await self.database.create_trigger_registration(
395
385
  user_id=user_id,
386
+ tenant_id=tenant_id,
396
387
  template_id=trigger.id,
397
388
  resource=resource_dict,
398
- metadata=merged_metadata,
389
+ metadata=result.metadata,
399
390
  )
400
391
 
401
392
  if not registration:
@@ -427,8 +418,9 @@ class TriggerServer:
427
418
  """Delete a trigger registration."""
428
419
  try:
429
420
  user_id = current_user["identity"]
421
+ tenant_id = current_user["tenant_id"]
430
422
  success = await self.database.delete_trigger_registration(
431
- registration_id, user_id
423
+ registration_id, user_id, tenant_id
432
424
  )
433
425
  if not success:
434
426
  raise HTTPException(
@@ -450,10 +442,11 @@ class TriggerServer:
450
442
  """List agents linked to this registration."""
451
443
  try:
452
444
  user_id = current_user["identity"]
445
+ tenant_id = current_user["tenant_id"]
453
446
 
454
447
  # Get the specific trigger registration
455
448
  trigger = await self.database.get_trigger_registration(
456
- registration_id, user_id
449
+ registration_id, user_id, tenant_id
457
450
  )
458
451
  if not trigger:
459
452
  raise HTTPException(
@@ -490,10 +483,11 @@ class TriggerServer:
490
483
  field_selection = None
491
484
 
492
485
  user_id = current_user["identity"]
486
+ tenant_id = current_user["tenant_id"]
493
487
 
494
488
  # Verify the trigger registration exists and belongs to the user
495
489
  registration = await self.database.get_trigger_registration(
496
- registration_id, user_id
490
+ registration_id, user_id, tenant_id
497
491
  )
498
492
  if not registration:
499
493
  raise HTTPException(
@@ -537,10 +531,11 @@ class TriggerServer:
537
531
  """Remove an agent from a trigger registration."""
538
532
  try:
539
533
  user_id = current_user["identity"]
534
+ tenant_id = current_user["tenant_id"]
540
535
 
541
536
  # Verify the trigger registration exists and belongs to the user
542
537
  registration = await self.database.get_trigger_registration(
543
- registration_id, user_id
538
+ registration_id, user_id, tenant_id
544
539
  )
545
540
  if not registration:
546
541
  raise HTTPException(
@@ -578,10 +573,11 @@ class TriggerServer:
578
573
  """Manually execute a cron trigger registration immediately."""
579
574
  try:
580
575
  user_id = current_user["identity"]
576
+ tenant_id = current_user["tenant_id"]
581
577
 
582
578
  # Verify the trigger registration exists and belongs to the user
583
579
  registration = await self.database.get_trigger_registration(
584
- registration_id, user_id
580
+ registration_id, user_id, tenant_id
585
581
  )
586
582
  if not registration:
587
583
  raise HTTPException(
@@ -751,11 +747,7 @@ class TriggerServer:
751
747
  # Ensure agent_id and user_id are strings for JSON serialization
752
748
  agent_id_str = str(agent_id)
753
749
  user_id_str = str(result.registration["user_id"])
754
- tenant_id_str = str(
755
- result.registration.get("metadata", {})
756
- .get("client_metadata", {})
757
- .get("tenant_id")
758
- )
750
+ tenant_id_str = str(result.registration["tenant_id"])
759
751
 
760
752
  agent_input = {"messages": [{"role": "human", "content": message}]}
761
753
 
@@ -97,12 +97,6 @@ class TriggerRegistrationResult(BaseModel):
97
97
  if self.create_registration and not self.metadata:
98
98
  self.metadata = {} # Allow empty metadata for create_registration=True
99
99
 
100
- if "client_metadata" in self.metadata:
101
- raise ValueError(
102
- "The 'client_metadata' key is reserved for client-provided metadata. "
103
- "Registration handlers must not use this key in their metadata."
104
- )
105
-
106
100
  if not self.create_registration and (
107
101
  not self.response_body or not self.status_code
108
102
  ):
@@ -285,9 +285,7 @@ class CronTriggerManager:
285
285
  user_id = registration["user_id"]
286
286
  template_id = registration.get("template_id")
287
287
  template_id = str(template_id) if template_id is not None else None
288
- tenant_id = (
289
- registration.get("metadata", {}).get("client_metadata", {}).get("tenant_id")
290
- )
288
+ tenant_id = str(registration.get("tenant_id"))
291
289
 
292
290
  # Get agent links
293
291
  agent_links = await self.trigger_server.database.get_agents_for_trigger(
@@ -1,16 +1,5 @@
1
1
  """Database module for trigger operations."""
2
2
 
3
3
  from .interface import TriggerDatabaseInterface
4
- from .supabase import SupabaseTriggerDatabase
5
4
 
6
-
7
- def create_database(database_type: str, **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"]
5
+ __all__ = ["TriggerDatabaseInterface"]
@@ -35,28 +35,33 @@ class TriggerDatabaseInterface(ABC):
35
35
 
36
36
  @abstractmethod
37
37
  async def create_trigger_registration(
38
- self, user_id: str, template_id: str, resource: dict, metadata: dict = None
38
+ self,
39
+ user_id: str,
40
+ tenant_id: str,
41
+ template_id: str,
42
+ resource: dict,
43
+ metadata: dict = None,
39
44
  ) -> dict[str, Any] | None:
40
45
  """Create a new trigger registration for a user."""
41
46
  pass
42
47
 
43
48
  @abstractmethod
44
49
  async def get_user_trigger_registrations(
45
- self, user_id: str
50
+ self, user_id: str, tenant_id: str
46
51
  ) -> list[dict[str, Any]]:
47
- """Get all trigger registrations for a user."""
52
+ """Get all trigger registrations for a user within a tenant."""
48
53
  pass
49
54
 
50
55
  @abstractmethod
51
56
  async def get_user_trigger_registrations_with_agents(
52
- self, user_id: str
57
+ self, user_id: str, tenant_id: str
53
58
  ) -> list[dict[str, Any]]:
54
- """Get all trigger registrations for a user with linked agents in a single query."""
59
+ """Get all trigger registrations for a user with linked agents in a single query within a tenant."""
55
60
  pass
56
61
 
57
62
  @abstractmethod
58
63
  async def get_trigger_registration(
59
- self, registration_id: str, user_id: str = None
64
+ self, registration_id: str, user_id: str, tenant_id: str
60
65
  ) -> dict[str, Any] | None:
61
66
  """Get a specific trigger registration."""
62
67
  pass
@@ -70,9 +75,13 @@ class TriggerDatabaseInterface(ABC):
70
75
 
71
76
  @abstractmethod
72
77
  async def find_user_registration_by_resource(
73
- self, user_id: str, template_id: str, resource_data: dict[str, Any]
78
+ self,
79
+ user_id: str,
80
+ tenant_id: str,
81
+ template_id: str,
82
+ resource_data: dict[str, Any],
74
83
  ) -> dict[str, Any] | None:
75
- """Find trigger registration by matching resource data for a specific user."""
84
+ """Find trigger registration by matching resource data for a specific user within a tenant."""
76
85
  pass
77
86
 
78
87
  @abstractmethod
@@ -80,16 +89,9 @@ class TriggerDatabaseInterface(ABC):
80
89
  """Get all registrations for a specific trigger template."""
81
90
  pass
82
91
 
83
- @abstractmethod
84
- async def update_trigger_metadata(
85
- self, registration_id: str, metadata_updates: dict, user_id: str = None
86
- ) -> bool:
87
- """Update metadata for a trigger registration."""
88
- pass
89
-
90
92
  @abstractmethod
91
93
  async def delete_trigger_registration(
92
- self, registration_id: str, user_id: str = None
94
+ self, registration_id: str, user_id: str, tenant_id: str
93
95
  ) -> bool:
94
96
  """Delete a trigger registration."""
95
97
  pass
@@ -120,20 +122,3 @@ class TriggerDatabaseInterface(ABC):
120
122
  ) -> list[dict[str, Any]]:
121
123
  """Get all agent links for a trigger registration with field_selection."""
122
124
  pass
123
-
124
- @abstractmethod
125
- async def get_triggers_for_agent(self, agent_id: str) -> list[dict[str, Any]]:
126
- """Get all trigger registrations linked to an agent."""
127
- pass
128
-
129
- # ========== Helper Methods ==========
130
-
131
- @abstractmethod
132
- async def get_user_from_token(self, token: str) -> str | None:
133
- """Extract user ID from authentication token."""
134
- pass
135
-
136
- @abstractmethod
137
- async def get_user_by_email(self, email: str) -> str | None:
138
- """Get user ID by email from trigger registrations."""
139
- pass
@@ -1,15 +0,0 @@
1
- langchain_triggers/__init__.py,sha256=WoW9LC_FJRs42mLWq2iuM-jjPow2Rue50q2zm56Oul0,536
2
- langchain_triggers/app.py,sha256=-VytRXkAPI8qm-fbMJMbQ52Qt8WC2nKBgL_8FGJaFTM,40468
3
- langchain_triggers/core.py,sha256=6r_91UWt7c26TVqkDxHc2xjmaQl42g-iFHGEWuDo0gs,3929
4
- langchain_triggers/cron_manager.py,sha256=PueW_z7gdkSza9J05W1VV2OxsfqvodMo7ZlAQJKMvpI,16408
5
- langchain_triggers/decorators.py,sha256=ZhWgUjbl2jUbvGSlqgThebd0tPgqXoQ02D7BcKZ9aIs,4675
6
- langchain_triggers/auth/__init__.py,sha256=RtDKuBoKYuyHzLNpKr74cmALO0PhHlWO9Ho7k3CUYFE,349
7
- langchain_triggers/auth/slack_hmac.py,sha256=kiwjhTXITgQvLAtEcOv8BnnWJRJcxaQ9dXkQm3JJDQ4,2948
8
- langchain_triggers/database/__init__.py,sha256=B1I1qmVr3U1CSf0VkjxsL4W5QGda5T7uB_CsJq6yBF4,535
9
- langchain_triggers/database/interface.py,sha256=jpADOOwcBQo1ZichgiZVaOvfZqEqVVo8Ea7ATWWTSBE,4283
10
- langchain_triggers/database/supabase.py,sha256=zi_75GbqRvzzlXd5EgfYof4h6vHWOLS4I1759wvY9kQ,17009
11
- langchain_triggers/triggers/__init__.py,sha256=Uw1544gxzN4XDRn2RzpZ5EAG6EAF38ZYQtVvlciEsMs,146
12
- langchain_triggers/triggers/cron_trigger.py,sha256=YTscULtB8JesnB24IWZl0LDMkf-A9Y6dksAk7GLFJiQ,3355
13
- langchain_trigger_server-0.3.4.dist-info/METADATA,sha256=FJuy7Q06X25o4JwhcS4yldyOAvsDBHdycR6utvhIgss,1438
14
- langchain_trigger_server-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- langchain_trigger_server-0.3.4.dist-info/RECORD,,
@@ -1,471 +0,0 @@
1
- """Supabase implementation of trigger database interface."""
2
-
3
- import base64
4
- import hashlib
5
- import logging
6
- import os
7
- from typing import Any
8
-
9
- from cryptography.hazmat.backends import default_backend
10
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
11
- from supabase import create_client
12
-
13
- from .interface import TriggerDatabaseInterface
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- class SupabaseTriggerDatabase(TriggerDatabaseInterface):
19
- """Supabase implementation of trigger database operations."""
20
-
21
- def __init__(self, supabase_url: str = None, supabase_key: str = None):
22
- self.supabase_url = supabase_url or os.getenv("SUPABASE_URL")
23
- self.supabase_key = supabase_key or os.getenv("SUPABASE_KEY")
24
-
25
- if not self.supabase_url or not self.supabase_key:
26
- raise ValueError(
27
- "SUPABASE_URL and SUPABASE_KEY environment variables are required"
28
- )
29
-
30
- self.client = create_client(self.supabase_url, self.supabase_key)
31
-
32
- # Get encryption key for API key decryption - required
33
- self.encryption_key = os.getenv("SECRETS_ENCRYPTION_KEY")
34
- if not self.encryption_key:
35
- raise ValueError("SECRETS_ENCRYPTION_KEY environment variable is required")
36
-
37
- logger.info("Initialized SupabaseTriggerDatabase")
38
-
39
- def _decrypt_secret(self, encrypted_secret: str) -> str:
40
- """Decrypt an encrypted secret using AES-256-GCM to match OAP Node.js implementation."""
41
- try:
42
- # Decode the base64 encoded encrypted data
43
- combined = base64.b64decode(encrypted_secret)
44
-
45
- # Constants from Node.js implementation
46
- IV_LENGTH = 12 # 96 bits
47
- TAG_LENGTH = 16 # 128 bits
48
-
49
- # Minimum length check
50
- if len(combined) < IV_LENGTH + TAG_LENGTH + 1:
51
- raise ValueError(
52
- "Invalid encrypted secret format: too short or malformed"
53
- )
54
-
55
- # Extract IV, encrypted data, and auth tag
56
- iv = combined[:IV_LENGTH]
57
- tag = combined[-TAG_LENGTH:]
58
- encrypted_data = combined[IV_LENGTH:-TAG_LENGTH]
59
-
60
- # Derive key using SHA-256 hash (same as Node.js deriveKey function)
61
- key = hashlib.sha256(self.encryption_key.encode()).digest()
62
-
63
- # Create AES-GCM cipher
64
- cipher = Cipher(
65
- algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend()
66
- )
67
- decryptor = cipher.decryptor()
68
-
69
- # Decrypt the data
70
- decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
71
-
72
- return decrypted_data.decode("utf-8")
73
- except Exception as e:
74
- logger.error(f"Error decrypting secret: {e}")
75
- raise ValueError("Failed to decrypt API key")
76
-
77
- # ========== Trigger Templates ==========
78
-
79
- async def create_trigger_template(
80
- self,
81
- id: str,
82
- provider: str,
83
- name: str,
84
- description: str = None,
85
- registration_schema: dict = None,
86
- ) -> dict[str, Any] | None:
87
- """Create a new trigger template."""
88
- try:
89
- data = {
90
- "id": id,
91
- "provider": provider,
92
- "name": name,
93
- "description": description,
94
- "registration_schema": registration_schema or {},
95
- }
96
-
97
- response = self.client.table("trigger_templates").insert(data).execute()
98
- return response.data[0] if response.data else None
99
-
100
- except Exception as e:
101
- logger.error(f"Error creating trigger template: {e}")
102
- return None
103
-
104
- async def get_trigger_templates(self) -> list[dict[str, Any]]:
105
- """Get all available trigger templates."""
106
- try:
107
- response = self.client.table("trigger_templates").select("*").execute()
108
- return response.data or []
109
- except Exception as e:
110
- logger.error(f"Error getting trigger templates: {e}")
111
- return []
112
-
113
- async def get_trigger_template(self, id: str) -> dict[str, Any] | None:
114
- """Get a specific trigger template by ID."""
115
- try:
116
- response = (
117
- self.client.table("trigger_templates")
118
- .select("*")
119
- .eq("id", id)
120
- .single()
121
- .execute()
122
- )
123
- return response.data if response.data else None
124
- except Exception as e:
125
- # Don't log as error if template just doesn't exist (expected on first startup)
126
- if (
127
- "no rows returned" in str(e).lower()
128
- or "multiple (or no) rows returned" in str(e).lower()
129
- ):
130
- logger.debug(f"Trigger template {id} not found in database")
131
- else:
132
- logger.error(f"Error getting trigger template {id}: {e}")
133
- return None
134
-
135
- # ========== Trigger Registrations ==========
136
-
137
- async def create_trigger_registration(
138
- self, user_id: str, template_id: str, resource: dict, metadata: dict = None
139
- ) -> dict[str, Any] | None:
140
- """Create a new trigger registration for a user."""
141
- try:
142
- # Verify template exists
143
- template = await self.get_trigger_template(template_id)
144
- if not template:
145
- logger.error(f"Template not found for ID: {template_id}")
146
- return None
147
-
148
- data = {
149
- "user_id": user_id,
150
- "template_id": template_id,
151
- "resource": resource,
152
- "metadata": metadata or {},
153
- "status": "active",
154
- }
155
-
156
- response = self.client.table("trigger_registrations").insert(data).execute()
157
- return response.data[0] if response.data else None
158
-
159
- except Exception as e:
160
- logger.exception(f"Error creating trigger registration: {e}")
161
- return None
162
-
163
- async def get_user_trigger_registrations(
164
- self, user_id: str
165
- ) -> list[dict[str, Any]]:
166
- """Get all trigger registrations for a user."""
167
- try:
168
- response = (
169
- self.client.table("trigger_registrations")
170
- .select("""
171
- *,
172
- trigger_templates(id, name, description)
173
- """)
174
- .eq("user_id", user_id)
175
- .order("created_at", desc=True)
176
- .execute()
177
- )
178
-
179
- return response.data or []
180
- except Exception as e:
181
- logger.error(f"Error getting user trigger registrations: {e}")
182
- return []
183
-
184
- async def get_user_trigger_registrations_with_agents(
185
- self, user_id: str
186
- ) -> list[dict[str, Any]]:
187
- """Get all trigger registrations for a user with linked agents in a single query."""
188
- try:
189
- response = (
190
- self.client.table("trigger_registrations")
191
- .select("""
192
- *,
193
- trigger_templates(id, name, description),
194
- agent_trigger_links(agent_id)
195
- """)
196
- .eq("user_id", user_id)
197
- .order("created_at", desc=True)
198
- .execute()
199
- )
200
-
201
- # Process the results to extract linked agent IDs
202
- if response.data:
203
- for registration in response.data:
204
- # Extract agent IDs from the agent_trigger_links
205
- agent_links = registration.get("agent_trigger_links", [])
206
- linked_agent_ids = [
207
- link.get("agent_id")
208
- for link in agent_links
209
- if link.get("agent_id")
210
- ]
211
- registration["linked_agent_ids"] = linked_agent_ids
212
-
213
- # Clean up the raw agent_trigger_links data as it's no longer needed
214
- registration.pop("agent_trigger_links", None)
215
-
216
- return response.data or []
217
- except Exception as e:
218
- logger.error(f"Error getting user trigger registrations with agents: {e}")
219
- return []
220
-
221
- async def get_trigger_registration(
222
- self, registration_id: str, user_id: str = None
223
- ) -> dict[str, Any] | None:
224
- """Get a specific trigger registration."""
225
- try:
226
- query = (
227
- self.client.table("trigger_registrations")
228
- .select("*")
229
- .eq("id", registration_id)
230
- )
231
- if user_id:
232
- query = query.eq("user_id", user_id)
233
-
234
- response = query.single().execute()
235
- return response.data if response.data else None
236
- except Exception as e:
237
- logger.error(f"Error getting trigger registration {registration_id}: {e}")
238
- return None
239
-
240
- async def update_trigger_metadata(
241
- self, registration_id: str, metadata_updates: dict, user_id: str = None
242
- ) -> bool:
243
- """Update metadata for a trigger registration."""
244
- try:
245
- # Get current registration to merge metadata
246
- current = await self.get_trigger_registration(registration_id, user_id)
247
- if not current:
248
- return False
249
-
250
- # Merge existing metadata with updates
251
- current_metadata = current.get("metadata", {})
252
- updated_metadata = {**current_metadata, **metadata_updates}
253
-
254
- query = (
255
- self.client.table("trigger_registrations")
256
- .update({"metadata": updated_metadata, "updated_at": "NOW()"})
257
- .eq("id", registration_id)
258
- )
259
-
260
- if user_id:
261
- query = query.eq("user_id", user_id)
262
-
263
- response = query.execute()
264
- return bool(response.data)
265
-
266
- except Exception as e:
267
- logger.error(f"Error updating trigger metadata: {e}")
268
- return False
269
-
270
- async def delete_trigger_registration(
271
- self, registration_id: str, user_id: str = None
272
- ) -> bool:
273
- """Delete a trigger registration."""
274
- try:
275
- query = (
276
- self.client.table("trigger_registrations")
277
- .delete()
278
- .eq("id", registration_id)
279
- )
280
- if user_id:
281
- query = query.eq("user_id", user_id)
282
-
283
- query.execute()
284
- return True # Delete operations don't return data
285
-
286
- except Exception as e:
287
- logger.error(f"Error deleting trigger registration: {e}")
288
- return False
289
-
290
- async def find_registration_by_resource(
291
- self, template_id: str, resource_data: dict[str, Any]
292
- ) -> dict[str, Any] | None:
293
- """Find trigger registration by matching resource data."""
294
- try:
295
- # Build query to match against trigger_registrations with template_id filter
296
- query = (
297
- self.client.table("trigger_registrations")
298
- .select("*, trigger_templates(id, name, description)")
299
- .eq("template_id", template_id)
300
- )
301
-
302
- # Add resource field matches
303
- for field, value in resource_data.items():
304
- query = query.eq(f"resource->>{field}", value)
305
-
306
- response = query.execute()
307
-
308
- if response.data:
309
- return response.data[0] # Return first match
310
- return None
311
-
312
- except Exception as e:
313
- logger.error(f"Error finding registration by resource: {e}")
314
- return None
315
-
316
- async def find_user_registration_by_resource(
317
- self, user_id: str, template_id: str, resource_data: dict[str, Any]
318
- ) -> dict[str, Any] | None:
319
- """Find trigger registration by matching resource data for a specific user."""
320
- try:
321
- # Build query to match against trigger_registrations with template_id and user_id filter
322
- query = (
323
- self.client.table("trigger_registrations")
324
- .select("*, trigger_templates(id, name, description)")
325
- .eq("template_id", template_id)
326
- .eq("user_id", user_id)
327
- )
328
-
329
- # Add resource field matches
330
- for field, value in resource_data.items():
331
- query = query.eq(f"resource->>{field}", value)
332
-
333
- response = query.execute()
334
-
335
- if response.data:
336
- return response.data[0] # Return first match
337
- return None
338
-
339
- except Exception as e:
340
- logger.error(f"Error finding user registration by resource: {e}")
341
- return None
342
-
343
- async def get_all_registrations(self, template_id: str) -> list[dict[str, Any]]:
344
- """Get all registrations for a specific trigger template."""
345
- try:
346
- response = (
347
- self.client.table("trigger_registrations")
348
- .select("*, trigger_templates(id, name, description)")
349
- .eq("template_id", template_id)
350
- .execute()
351
- )
352
-
353
- return response.data or []
354
- except Exception as e:
355
- logger.error(
356
- f"Error getting all registrations for template {template_id}: {e}"
357
- )
358
- return []
359
-
360
- # ========== Agent-Trigger Links ==========
361
-
362
- async def link_agent_to_trigger(
363
- self,
364
- agent_id: str,
365
- registration_id: str,
366
- created_by: str,
367
- field_selection: dict[str, bool] | None = None,
368
- ) -> bool:
369
- """Link an agent to a trigger registration with optional field selection."""
370
- try:
371
- data = {
372
- "agent_id": agent_id,
373
- "registration_id": registration_id,
374
- "created_by": created_by,
375
- "field_selection": field_selection,
376
- }
377
-
378
- response = self.client.table("agent_trigger_links").insert(data).execute()
379
- return bool(response.data)
380
-
381
- except Exception as e:
382
- logger.error(f"Error linking agent to trigger: {e}")
383
- return False
384
-
385
- async def unlink_agent_from_trigger(
386
- self, agent_id: str, registration_id: str
387
- ) -> bool:
388
- """Unlink an agent from a trigger registration."""
389
- try:
390
- (
391
- self.client.table("agent_trigger_links")
392
- .delete()
393
- .eq("agent_id", agent_id)
394
- .eq("registration_id", registration_id)
395
- .execute()
396
- )
397
-
398
- return True # Delete operations don't return data
399
-
400
- except Exception as e:
401
- logger.error(f"Error unlinking agent from trigger: {e}")
402
- return False
403
-
404
- async def get_agents_for_trigger(
405
- self, registration_id: str
406
- ) -> list[dict[str, Any]]:
407
- """Get all agent links for a trigger registration with field_selection."""
408
- try:
409
- response = (
410
- self.client.table("agent_trigger_links")
411
- .select("agent_id, field_selection")
412
- .eq("registration_id", registration_id)
413
- .execute()
414
- )
415
-
416
- return response.data or []
417
-
418
- except Exception as e:
419
- logger.error(f"Error getting agents for trigger: {e}")
420
- return []
421
-
422
- async def get_triggers_for_agent(self, agent_id: str) -> list[dict[str, Any]]:
423
- """Get all trigger registrations linked to an agent."""
424
- try:
425
- response = (
426
- self.client.table("agent_trigger_links")
427
- .select("""
428
- registration_id,
429
- trigger_registrations(
430
- *,
431
- trigger_templates(id, name, description)
432
- )
433
- """)
434
- .eq("agent_id", agent_id)
435
- .execute()
436
- )
437
-
438
- return [row["trigger_registrations"] for row in response.data or []]
439
-
440
- except Exception as e:
441
- logger.error(f"Error getting triggers for agent: {e}")
442
- return []
443
-
444
- # ========== Helper Methods ==========
445
-
446
- async def get_user_from_token(self, token: str) -> str | None:
447
- """Extract user ID from JWT token via Supabase auth."""
448
- try:
449
- client = self._create_user_client(token)
450
- response = client.auth.get_user(token)
451
- return response.user.id if response.user else None
452
- except Exception as e:
453
- logger.error(f"Error getting user from token: {e}")
454
- return None
455
-
456
- async def get_user_by_email(self, email: str) -> str | None:
457
- """Get user ID by email from trigger registrations."""
458
- try:
459
- response = (
460
- self.client.table("trigger_registrations")
461
- .select("user_id")
462
- .eq("resource->>email", email)
463
- .limit(1)
464
- .execute()
465
- )
466
-
467
- return response.data[0]["user_id"] if response.data else None
468
-
469
- except Exception as e:
470
- logger.error(f"Error getting user by email: {e}")
471
- return None