langchain-trigger-server 0.2.6rc8__py3-none-any.whl → 0.2.7__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,10 +1,10 @@
1
1
  """Authentication utilities for trigger webhooks."""
2
2
 
3
3
  from .slack_hmac import (
4
- verify_slack_signature,
5
- get_slack_signing_secret,
6
- extract_slack_headers,
7
4
  SlackSignatureVerificationError,
5
+ extract_slack_headers,
6
+ get_slack_signing_secret,
7
+ verify_slack_signature,
8
8
  )
9
9
 
10
10
  __all__ = [
@@ -13,4 +13,3 @@ __all__ = [
13
13
  "extract_slack_headers",
14
14
  "SlackSignatureVerificationError",
15
15
  ]
16
-
@@ -12,13 +12,13 @@ import hmac
12
12
  import logging
13
13
  import os
14
14
  import time
15
- from typing import Optional
16
15
 
17
16
  logger = logging.getLogger(__name__)
18
17
 
19
18
 
20
19
  class SlackSignatureVerificationError(Exception):
21
20
  """Exception raised when Slack signature verification fails."""
21
+
22
22
  pass
23
23
 
24
24
 
@@ -27,13 +27,13 @@ def verify_slack_signature(
27
27
  timestamp: str,
28
28
  body: str,
29
29
  signature: str,
30
- max_age_seconds: int = 300
30
+ max_age_seconds: int = 300,
31
31
  ) -> bool:
32
32
  try:
33
33
  # Verify timestamp to prevent replay attacks
34
34
  current_time = int(time.time())
35
35
  request_time = int(timestamp)
36
-
36
+
37
37
  if abs(current_time - request_time) > max_age_seconds:
38
38
  logger.error(
39
39
  f"Slack request timestamp too old. "
@@ -43,27 +43,27 @@ def verify_slack_signature(
43
43
  raise SlackSignatureVerificationError(
44
44
  f"Request timestamp is too old (>{max_age_seconds}s). Possible replay attack."
45
45
  )
46
-
46
+
47
47
  # Format: v0:{timestamp}:{body}
48
48
  sig_basestring = f"v0:{timestamp}:{body}"
49
-
49
+
50
50
  # Create HMAC-SHA256 hash
51
- my_signature = 'v0=' + hmac.new(
52
- signing_secret.encode(),
53
- sig_basestring.encode(),
54
- hashlib.sha256
55
- ).hexdigest()
56
-
51
+ my_signature = (
52
+ "v0="
53
+ + hmac.new(
54
+ signing_secret.encode(), sig_basestring.encode(), hashlib.sha256
55
+ ).hexdigest()
56
+ )
57
+
57
58
  if not hmac.compare_digest(my_signature, signature):
58
59
  logger.error(
59
- f"Slack signature mismatch. "
60
- f"Expected: {my_signature}, Got: {signature}"
60
+ f"Slack signature mismatch. Expected: {my_signature}, Got: {signature}"
61
61
  )
62
62
  raise SlackSignatureVerificationError("Signature verification failed")
63
-
63
+
64
64
  logger.info("Successfully verified Slack webhook signature")
65
65
  return True
66
-
66
+
67
67
  except ValueError as e:
68
68
  logger.error(f"Invalid timestamp format: {e}")
69
69
  raise SlackSignatureVerificationError(f"Invalid timestamp: {str(e)}")
@@ -72,7 +72,7 @@ def verify_slack_signature(
72
72
  raise SlackSignatureVerificationError(f"Verification error: {str(e)}")
73
73
 
74
74
 
75
- def get_slack_signing_secret() -> Optional[str]:
75
+ def get_slack_signing_secret() -> str | None:
76
76
  """Get Slack signing secret from SLACK_SIGNING_SECRET environment variable."""
77
77
  secret = os.getenv("SLACK_SIGNING_SECRET")
78
78
  if not secret:
@@ -80,16 +80,11 @@ def get_slack_signing_secret() -> Optional[str]:
80
80
  return secret
81
81
 
82
82
 
83
- def extract_slack_headers(headers: dict) -> tuple[Optional[str], Optional[str]]:
83
+ def extract_slack_headers(headers: dict) -> tuple[str | None, str | None]:
84
84
  """Extract Slack signature and timestamp from request headers."""
85
- signature = (
86
- headers.get('x-slack-signature') or
87
- headers.get('X-Slack-Signature')
88
- )
89
- timestamp = (
90
- headers.get('x-slack-request-timestamp') or
91
- headers.get('X-Slack-Request-Timestamp')
85
+ signature = headers.get("x-slack-signature") or headers.get("X-Slack-Signature")
86
+ timestamp = headers.get("x-slack-request-timestamp") or headers.get(
87
+ "X-Slack-Request-Timestamp"
92
88
  )
93
-
94
- return signature, timestamp
95
89
 
90
+ return signature, timestamp
@@ -3,51 +3,59 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from typing import Any, Dict, Optional
6
+ from typing import Any
7
+
7
8
  from pydantic import BaseModel, Field
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
11
12
 
12
-
13
13
  class ProviderAuthInfo(BaseModel):
14
14
  """Authentication info for a specific OAuth provider."""
15
-
16
- token: Optional[str] = None
15
+
16
+ token: str | None = None
17
17
  auth_required: bool = False
18
- auth_url: Optional[str] = None
19
- auth_id: Optional[str] = None
18
+ auth_url: str | None = None
19
+ auth_id: str | None = None
20
20
 
21
21
 
22
22
  class UserAuthInfo(BaseModel):
23
23
  """User authentication info containing OAuth tokens or auth requirements."""
24
-
24
+
25
25
  user_id: str
26
- providers: Dict[str, ProviderAuthInfo] = Field(default_factory=dict)
27
-
26
+ providers: dict[str, ProviderAuthInfo] = Field(default_factory=dict)
27
+
28
28
  class Config:
29
29
  arbitrary_types_allowed = True
30
30
 
31
31
 
32
-
33
-
34
32
  class AgentInvocationRequest(BaseModel):
35
33
  """Request to invoke an AI agent."""
36
-
34
+
37
35
  assistant_id: str
38
36
  user_id: str
39
37
  input_data: Any
40
- thread_id: Optional[str] = None
41
- metadata: Dict[str, Any] = Field(default_factory=dict)
38
+ thread_id: str | None = None
39
+ metadata: dict[str, Any] = Field(default_factory=dict)
42
40
 
43
41
 
44
42
  class TriggerHandlerResult(BaseModel):
45
43
  """Result returned by trigger handlers."""
46
- invoke_agent: bool = Field(default=True, description="Whether to invoke agents for this event")
47
- agent_messages: Optional[list[str]] = Field(default=None, description="List of messages to send to agents (one invocation per message)")
48
- response_body: Optional[Dict[str, Any]] = Field(default=None, description="Custom HTTP response body (when invoke_agent=False)")
49
- registration: Optional[Dict[str, Any]] = Field(default=None, description="Registration data (required when invoke_agent=True)")
50
-
44
+
45
+ invoke_agent: bool = Field(
46
+ default=True, description="Whether to invoke agents for this event"
47
+ )
48
+ agent_messages: list[str] | None = Field(
49
+ default=None,
50
+ description="List of messages to send to agents (one invocation per message)",
51
+ )
52
+ response_body: dict[str, Any] | None = Field(
53
+ default=None, description="Custom HTTP response body (when invoke_agent=False)"
54
+ )
55
+ registration: dict[str, Any] | None = Field(
56
+ default=None, description="Registration data (required when invoke_agent=True)"
57
+ )
58
+
51
59
  def model_post_init(self, __context) -> None:
52
60
  """Validate that required fields are provided based on invoke_agent."""
53
61
  if self.invoke_agent and not self.agent_messages:
@@ -60,19 +68,35 @@ class TriggerHandlerResult(BaseModel):
60
68
 
61
69
  class TriggerRegistrationResult(BaseModel):
62
70
  """Result returned by registration handlers."""
63
- create_registration: bool = Field(default=True, description="Whether to create database registration (False = return custom response)")
64
- metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata to store with the registration")
65
- response_body: Optional[Dict[str, Any]] = Field(default=None, description="Custom HTTP response body (when create_registration=False)")
66
- status_code: Optional[int] = Field(default=None, description="HTTP status code (when create_registration=False)")
67
-
71
+
72
+ create_registration: bool = Field(
73
+ default=True,
74
+ description="Whether to create database registration (False = return custom response)",
75
+ )
76
+ metadata: dict[str, Any] = Field(
77
+ default_factory=dict, description="Metadata to store with the registration"
78
+ )
79
+ response_body: dict[str, Any] | None = Field(
80
+ default=None,
81
+ description="Custom HTTP response body (when create_registration=False)",
82
+ )
83
+ status_code: int | None = Field(
84
+ default=None, description="HTTP status code (when create_registration=False)"
85
+ )
86
+
68
87
  def model_post_init(self, __context) -> None:
69
88
  """Validate that required fields are provided based on create_registration."""
70
89
  if self.create_registration and not self.metadata:
71
90
  self.metadata = {} # Allow empty metadata for create_registration=True
72
- if not self.create_registration and (not self.response_body or not self.status_code):
73
- raise ValueError("Both response_body and status_code are required when create_registration=False")
91
+ if not self.create_registration and (
92
+ not self.response_body or not self.status_code
93
+ ):
94
+ raise ValueError(
95
+ "Both response_body and status_code are required when create_registration=False"
96
+ )
74
97
 
75
98
 
76
99
  class TriggerRegistrationModel(BaseModel):
77
100
  """Base class for trigger resource models that define how webhooks are matched to registrations."""
78
- pass
101
+
102
+ pass
@@ -2,14 +2,12 @@
2
2
 
3
3
  import logging
4
4
  from datetime import datetime
5
- from typing import Dict, Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
8
8
  from apscheduler.triggers.cron import CronTrigger as APSCronTrigger
9
- from croniter import croniter
10
9
  from pydantic import BaseModel
11
10
 
12
- from langchain_triggers.core import TriggerHandlerResult
13
11
  from langchain_triggers.triggers.cron_trigger import CRON_TRIGGER_ID
14
12
 
15
13
  logger = logging.getLogger(__name__)
@@ -17,27 +15,27 @@ logger = logging.getLogger(__name__)
17
15
 
18
16
  class CronJobExecution(BaseModel):
19
17
  """Model for tracking cron job execution history."""
20
-
18
+
21
19
  registration_id: str
22
20
  cron_pattern: str
23
21
  scheduled_time: datetime
24
22
  actual_start_time: datetime
25
- completion_time: Optional[datetime] = None
23
+ completion_time: datetime | None = None
26
24
  status: str # "running", "completed", "failed"
27
- error_message: Optional[str] = None
25
+ error_message: str | None = None
28
26
  agents_invoked: int = 0
29
27
 
30
28
 
31
29
  class CronTriggerManager:
32
30
  """Manages dynamic cron job scheduling based on database registrations."""
33
-
31
+
34
32
  def __init__(self, trigger_server):
35
- self.scheduler = AsyncIOScheduler(timezone='UTC')
33
+ self.scheduler = AsyncIOScheduler(timezone="UTC")
36
34
  self.trigger_server = trigger_server
37
35
  self.active_jobs = {} # registration_id -> job_id mapping
38
36
  self.execution_history = [] # Keep recent execution history
39
37
  self.max_history = 1000
40
-
38
+
41
39
  async def start(self):
42
40
  """Start scheduler and load existing cron registrations."""
43
41
  try:
@@ -46,19 +44,21 @@ class CronTriggerManager:
46
44
  except Exception as e:
47
45
  logger.error(f"Failed to start CronTriggerManager: {e}")
48
46
  raise
49
-
47
+
50
48
  async def shutdown(self):
51
49
  """Shutdown scheduler gracefully."""
52
50
  try:
53
51
  self.scheduler.shutdown(wait=True)
54
52
  except Exception as e:
55
53
  logger.error(f"Error shutting down CronTriggerManager: {e}")
56
-
54
+
57
55
  async def _load_existing_registrations(self):
58
56
  """Load all existing cron registrations from database and schedule them."""
59
57
  try:
60
- registrations = await self.trigger_server.database.get_all_registrations(CRON_TRIGGER_ID)
61
-
58
+ registrations = await self.trigger_server.database.get_all_registrations(
59
+ CRON_TRIGGER_ID
60
+ )
61
+
62
62
  scheduled_count = 0
63
63
  for registration in registrations:
64
64
  if registration.get("status") == "active":
@@ -66,68 +66,74 @@ class CronTriggerManager:
66
66
  await self._schedule_cron_job(registration)
67
67
  scheduled_count += 1
68
68
  except Exception as e:
69
- logger.error(f"Failed to schedule existing cron job {registration.get('id')}: {e}")
70
-
69
+ logger.error(
70
+ f"Failed to schedule existing cron job {registration.get('id')}: {e}"
71
+ )
72
+
71
73
  except Exception as e:
72
74
  logger.error(f"Failed to load existing cron registrations: {e}")
73
-
75
+
74
76
  async def reload_from_database(self):
75
77
  """Reload all cron registrations from database, replacing current schedules."""
76
78
  try:
77
79
  # Clear all current jobs
78
80
  for registration_id in list(self.active_jobs.keys()):
79
81
  await self._unschedule_cron_job(registration_id)
80
-
82
+
81
83
  # Reload from database
82
84
  await self._load_existing_registrations()
83
85
 
84
86
  except Exception as e:
85
87
  logger.error(f"Failed to reload cron jobs from database: {e}")
86
88
  raise
87
-
88
- async def on_registration_created(self, registration: Dict[str, Any]):
89
+
90
+ async def on_registration_created(self, registration: dict[str, Any]):
89
91
  """Called when a new cron registration is created."""
90
92
  if registration.get("trigger_template_id") == CRON_TRIGGER_ID:
91
93
  try:
92
94
  await self._schedule_cron_job(registration)
93
95
  except Exception as e:
94
- logger.error(f"Failed to schedule new cron job {registration['id']}: {e}")
96
+ logger.error(
97
+ f"Failed to schedule new cron job {registration['id']}: {e}"
98
+ )
95
99
  raise
96
-
100
+
97
101
  async def on_registration_deleted(self, registration_id: str):
98
102
  """Called when a cron registration is deleted."""
99
103
  try:
100
104
  await self._unschedule_cron_job(registration_id)
101
105
  except Exception as e:
102
106
  logger.error(f"Failed to unschedule cron job {registration_id}: {e}")
103
-
104
- async def _schedule_cron_job(self, registration: Dict[str, Any]):
107
+
108
+ async def _schedule_cron_job(self, registration: dict[str, Any]):
105
109
  """Add a cron job to the scheduler."""
106
110
  registration_id = registration["id"]
107
111
  resource_data = registration.get("resource", {})
108
112
  crontab = resource_data.get("crontab", "")
109
-
113
+
110
114
  if not crontab:
111
- raise ValueError(f"No crontab pattern found in registration {registration_id}")
112
-
115
+ raise ValueError(
116
+ f"No crontab pattern found in registration {registration_id}"
117
+ )
118
+
113
119
  try:
114
120
  # Parse cron expression
115
121
  cron_parts = crontab.strip().split()
116
122
  if len(cron_parts) != 5:
117
123
  raise ValueError(f"Invalid cron format: {crontab} (expected 5 parts)")
118
-
124
+
119
125
  minute, hour, day, month, day_of_week = cron_parts
120
-
126
+
121
127
  # Create APScheduler cron trigger
122
128
  trigger = APSCronTrigger(
123
129
  minute=minute,
124
- hour=hour,
130
+ hour=hour,
125
131
  day=day,
126
132
  month=month,
127
133
  day_of_week=day_of_week,
128
- timezone='UTC'
134
+ timezone="UTC",
129
135
  )
130
-
136
+
131
137
  # Schedule the job
132
138
  job = self.scheduler.add_job(
133
139
  self._execute_cron_job_with_monitoring,
@@ -136,15 +142,17 @@ class CronTriggerManager:
136
142
  id=f"cron_{registration_id}",
137
143
  name=f"Cron job for registration {registration_id}",
138
144
  max_instances=1, # Prevent overlapping executions
139
- replace_existing=True
145
+ replace_existing=True,
140
146
  )
141
147
 
142
148
  self.active_jobs[registration_id] = job.id
143
149
 
144
150
  except Exception as e:
145
- logger.error(f"Failed to schedule cron job for registration {registration_id}: {e}")
151
+ logger.error(
152
+ f"Failed to schedule cron job for registration {registration_id}: {e}"
153
+ )
146
154
  raise
147
-
155
+
148
156
  async def _unschedule_cron_job(self, registration_id: str):
149
157
  """Remove a cron job from the scheduler."""
150
158
  if registration_id in self.active_jobs:
@@ -156,9 +164,11 @@ class CronTriggerManager:
156
164
  logger.error(f"Failed to unschedule cron job {job_id}: {e}")
157
165
  raise
158
166
  else:
159
- logger.warning(f"Attempted to unschedule non-existent cron job {registration_id}")
160
-
161
- async def _execute_cron_job_with_monitoring(self, registration: Dict[str, Any]):
167
+ logger.warning(
168
+ f"Attempted to unschedule non-existent cron job {registration_id}"
169
+ )
170
+
171
+ async def _execute_cron_job_with_monitoring(self, registration: dict[str, Any]):
162
172
  """Execute a scheduled cron job with full monitoring and error handling."""
163
173
  registration_id = registration["id"]
164
174
  cron_pattern = registration["resource"]["crontab"]
@@ -168,31 +178,35 @@ class CronTriggerManager:
168
178
  cron_pattern=cron_pattern,
169
179
  scheduled_time=datetime.utcnow(),
170
180
  actual_start_time=datetime.utcnow(),
171
- status="running"
181
+ status="running",
172
182
  )
173
183
 
174
184
  try:
175
185
  agents_invoked = await self.execute_cron_job(registration)
176
186
  execution.status = "completed"
177
187
  execution.agents_invoked = agents_invoked
178
- logger.info(f"✓ Cron job {registration_id} completed successfully - invoked {agents_invoked} agent(s)")
188
+ logger.info(
189
+ f"✓ Cron job {registration_id} completed successfully - invoked {agents_invoked} agent(s)"
190
+ )
179
191
 
180
192
  except Exception as e:
181
193
  execution.status = "failed"
182
194
  execution.error_message = str(e)
183
195
  logger.error(f"✗ Cron job {registration_id} failed: {e}")
184
-
196
+
185
197
  finally:
186
198
  execution.completion_time = datetime.utcnow()
187
199
  await self._record_execution(execution)
188
-
189
- async def execute_cron_job(self, registration: Dict[str, Any]) -> int:
200
+
201
+ async def execute_cron_job(self, registration: dict[str, Any]) -> int:
190
202
  """Execute a cron job - invoke agents. Can be called manually or by scheduler."""
191
203
  registration_id = registration["id"]
192
204
  user_id = registration["user_id"]
193
205
 
194
206
  # Get agent links
195
- agent_links = await self.trigger_server.database.get_agents_for_trigger(registration_id)
207
+ agent_links = await self.trigger_server.database.get_agents_for_trigger(
208
+ registration_id
209
+ )
196
210
 
197
211
  if not agent_links:
198
212
  logger.warning(f"No agents linked to cron job {registration_id}")
@@ -200,7 +214,11 @@ class CronTriggerManager:
200
214
 
201
215
  agents_invoked = 0
202
216
  for agent_link in agent_links:
203
- agent_id = agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
217
+ agent_id = (
218
+ agent_link
219
+ if isinstance(agent_link, str)
220
+ else agent_link.get("agent_id")
221
+ )
204
222
  # Ensure agent_id and user_id are strings for JSON serialization
205
223
  agent_id_str = str(agent_id)
206
224
  user_id_str = str(user_id)
@@ -210,7 +228,10 @@ class CronTriggerManager:
210
228
 
211
229
  agent_input = {
212
230
  "messages": [
213
- {"role": "human", "content": f"ACTION: triggering cron from langchain-trigger-server\nCURRENT TIME: {current_time_str}"}
231
+ {
232
+ "role": "human",
233
+ "content": f"ACTION: triggering cron from langchain-trigger-server\nCURRENT TIME: {current_time_str}",
234
+ }
214
235
  ]
215
236
  }
216
237
 
@@ -224,31 +245,33 @@ class CronTriggerManager:
224
245
  agents_invoked += 1
225
246
 
226
247
  except Exception as e:
227
- logger.error(f"✗ Error invoking agent {agent_id_str} for cron job {registration_id}: {e}")
248
+ logger.error(
249
+ f"✗ Error invoking agent {agent_id_str} for cron job {registration_id}: {e}"
250
+ )
228
251
 
229
252
  return agents_invoked
230
-
253
+
231
254
  async def _record_execution(self, execution: CronJobExecution):
232
255
  """Record execution history (in memory for now)."""
233
256
  self.execution_history.append(execution)
234
-
257
+
235
258
  # Keep only recent executions
236
259
  if len(self.execution_history) > self.max_history:
237
- self.execution_history = self.execution_history[-self.max_history:]
238
-
239
- def get_active_jobs(self) -> Dict[str, str]:
260
+ self.execution_history = self.execution_history[-self.max_history :]
261
+
262
+ def get_active_jobs(self) -> dict[str, str]:
240
263
  """Get currently active cron jobs."""
241
264
  return self.active_jobs.copy()
242
-
265
+
243
266
  def get_execution_history(self, limit: int = 100) -> list[CronJobExecution]:
244
267
  """Get recent execution history."""
245
268
  return self.execution_history[-limit:]
246
-
247
- def get_job_status(self) -> Dict[str, Any]:
269
+
270
+ def get_job_status(self) -> dict[str, Any]:
248
271
  """Get status information about the cron manager."""
249
272
  return {
250
273
  "active_jobs": len(self.active_jobs),
251
274
  "scheduler_running": self.scheduler.running,
252
275
  "total_executions": len(self.execution_history),
253
- "active_job_ids": list(self.active_jobs.keys())
254
- }
276
+ "active_job_ids": list(self.active_jobs.keys()),
277
+ }
@@ -6,11 +6,11 @@ from .supabase import SupabaseTriggerDatabase
6
6
 
7
7
  def create_database(database_type: str, **kwargs) -> TriggerDatabaseInterface:
8
8
  """Factory function to create database implementation."""
9
-
9
+
10
10
  if database_type == "supabase":
11
11
  return SupabaseTriggerDatabase(**kwargs)
12
12
  else:
13
13
  raise ValueError(f"Unknown database type: {database_type}")
14
14
 
15
15
 
16
- __all__ = ["TriggerDatabaseInterface", "SupabaseTriggerDatabase", "create_database"]
16
+ __all__ = ["TriggerDatabaseInterface", "SupabaseTriggerDatabase", "create_database"]