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.
- langchain_trigger_server-0.1.0.dist-info/METADATA +35 -0
- langchain_trigger_server-0.1.0.dist-info/RECORD +10 -0
- langchain_trigger_server-0.1.0.dist-info/WHEEL +4 -0
- langchain_triggers/__init__.py +17 -0
- langchain_triggers/app.py +657 -0
- langchain_triggers/core.py +83 -0
- langchain_triggers/database/__init__.py +16 -0
- langchain_triggers/database/interface.py +150 -0
- langchain_triggers/database/supabase.py +365 -0
- langchain_triggers/decorators.py +75 -0
|
@@ -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}")
|