langchain-trigger-server 0.1.9__tar.gz → 0.1.11__tar.gz

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.1.9
3
+ Version: 0.1.11
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
@@ -16,6 +16,8 @@ Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Requires-Python: >=3.9
19
+ Requires-Dist: apscheduler>=3.10.0
20
+ Requires-Dist: croniter>=1.4.0
19
21
  Requires-Dist: cryptography>=3.0.0
20
22
  Requires-Dist: fastapi>=0.100.0
21
23
  Requires-Dist: httpx>=0.24.0
@@ -3,6 +3,7 @@
3
3
  from .core import UserAuthInfo, TriggerRegistrationModel, TriggerHandlerResult, TriggerRegistrationResult
4
4
  from .decorators import TriggerTemplate
5
5
  from .app import TriggerServer
6
+ from .triggers.cron_trigger import cron_trigger
6
7
 
7
8
  __version__ = "0.1.0"
8
9
 
@@ -13,4 +14,5 @@ __all__ = [
13
14
  "TriggerRegistrationResult",
14
15
  "TriggerTemplate",
15
16
  "TriggerServer",
17
+ "cron_trigger",
16
18
  ]
@@ -14,6 +14,7 @@ from starlette.responses import Response
14
14
 
15
15
  from .decorators import TriggerTemplate
16
16
  from .database import create_database, TriggerDatabaseInterface
17
+ from .cron_manager import CronTriggerManager
17
18
 
18
19
  logger = logging.getLogger(__name__)
19
20
 
@@ -103,16 +104,24 @@ class TriggerServer:
103
104
 
104
105
  self.triggers: List[TriggerTemplate] = []
105
106
 
107
+ # Initialize CronTriggerManager
108
+ self.cron_manager = CronTriggerManager(self)
109
+
106
110
  # Setup authentication middleware
107
111
  self.app.add_middleware(AuthenticationMiddleware, auth_handler=auth_handler)
108
112
 
109
113
  # Setup routes
110
114
  self._setup_routes()
111
115
 
112
- # Add startup event to ensure trigger templates exist in database
116
+ # Add startup and shutdown events
113
117
  @self.app.on_event("startup")
114
118
  async def startup_event():
115
119
  await self.ensure_trigger_templates()
120
+ await self.cron_manager.start()
121
+
122
+ @self.app.on_event("shutdown")
123
+ async def shutdown_event():
124
+ await self.cron_manager.shutdown()
116
125
 
117
126
  def add_trigger(self, trigger: TriggerTemplate) -> None:
118
127
  """Add a trigger template to the app."""
@@ -285,6 +294,9 @@ class TriggerServer:
285
294
  if not registration:
286
295
  raise HTTPException(status_code=500, detail="Failed to create trigger registration")
287
296
 
297
+ # Reload cron manager to pick up any new cron registrations
298
+ await self.cron_manager.reload_from_database()
299
+
288
300
  # Return registration result
289
301
  return {
290
302
  "success": True,
@@ -0,0 +1,259 @@
1
+ """Dynamic Cron Trigger Manager for scheduled agent execution."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Dict, Any, Optional
6
+
7
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
8
+ from apscheduler.triggers.cron import CronTrigger as APSCronTrigger
9
+ from croniter import croniter
10
+ from pydantic import BaseModel
11
+
12
+ from langchain_triggers.core import TriggerHandlerResult
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CronJobExecution(BaseModel):
18
+ """Model for tracking cron job execution history."""
19
+
20
+ registration_id: str
21
+ cron_pattern: str
22
+ scheduled_time: datetime
23
+ actual_start_time: datetime
24
+ completion_time: Optional[datetime] = None
25
+ status: str # "running", "completed", "failed"
26
+ error_message: Optional[str] = None
27
+ agents_invoked: int = 0
28
+
29
+
30
+ class CronTriggerManager:
31
+ """Manages dynamic cron job scheduling based on database registrations."""
32
+
33
+ def __init__(self, trigger_server):
34
+ self.scheduler = AsyncIOScheduler(timezone='UTC')
35
+ self.trigger_server = trigger_server
36
+ self.active_jobs = {} # registration_id -> job_id mapping
37
+ self.execution_history = [] # Keep recent execution history
38
+ self.max_history = 1000
39
+
40
+ async def start(self):
41
+ """Start scheduler and load existing cron registrations."""
42
+ try:
43
+ self.scheduler.start()
44
+ await self._load_existing_registrations()
45
+ logger.info("✓ CronTriggerManager started")
46
+ except Exception as e:
47
+ logger.error(f"Failed to start CronTriggerManager: {e}")
48
+ raise
49
+
50
+ async def shutdown(self):
51
+ """Shutdown scheduler gracefully."""
52
+ try:
53
+ self.scheduler.shutdown(wait=True)
54
+ logger.info("✓ CronTriggerManager stopped")
55
+ except Exception as e:
56
+ logger.error(f"Error shutting down CronTriggerManager: {e}")
57
+
58
+ async def _load_existing_registrations(self):
59
+ """Load all existing cron registrations from database and schedule them."""
60
+ try:
61
+ registrations = await self.trigger_server.database.get_all_registrations("cron-trigger")
62
+
63
+ scheduled_count = 0
64
+ for registration in registrations:
65
+ if registration.get("status") == "active":
66
+ try:
67
+ await self._schedule_cron_job(registration)
68
+ scheduled_count += 1
69
+ except Exception as e:
70
+ logger.error(f"Failed to schedule existing cron job {registration.get('id')}: {e}")
71
+
72
+ logger.info(f"Loaded {scheduled_count} existing cron registrations from {len(registrations)} total")
73
+
74
+ except Exception as e:
75
+ logger.error(f"Failed to load existing cron registrations: {e}")
76
+
77
+ async def reload_from_database(self):
78
+ """Reload all cron registrations from database, replacing current schedules."""
79
+ try:
80
+ # Clear all current jobs
81
+ for registration_id in list(self.active_jobs.keys()):
82
+ await self._unschedule_cron_job(registration_id)
83
+
84
+ # Reload from database
85
+ await self._load_existing_registrations()
86
+ logger.info("✓ Reloaded cron jobs from database")
87
+
88
+ except Exception as e:
89
+ logger.error(f"Failed to reload cron jobs from database: {e}")
90
+ raise
91
+
92
+ async def on_registration_created(self, registration: Dict[str, Any]):
93
+ """Called when a new cron registration is created."""
94
+ if registration.get("trigger_template_id") == "cron-trigger":
95
+ try:
96
+ await self._schedule_cron_job(registration)
97
+ logger.info(f"Scheduled new cron job for registration {registration['id']}")
98
+ except Exception as e:
99
+ logger.error(f"Failed to schedule new cron job {registration['id']}: {e}")
100
+ raise
101
+
102
+ async def on_registration_deleted(self, registration_id: str):
103
+ """Called when a cron registration is deleted."""
104
+ try:
105
+ await self._unschedule_cron_job(registration_id)
106
+ logger.info(f"Unscheduled cron job for deleted registration {registration_id}")
107
+ except Exception as e:
108
+ logger.error(f"Failed to unschedule cron job {registration_id}: {e}")
109
+
110
+ async def _schedule_cron_job(self, registration: Dict[str, Any]):
111
+ """Add a cron job to the scheduler."""
112
+ registration_id = registration["id"]
113
+ resource_data = registration.get("resource", {})
114
+ crontab = resource_data.get("crontab", "")
115
+
116
+ if not crontab:
117
+ raise ValueError(f"No crontab pattern found in registration {registration_id}")
118
+
119
+ try:
120
+ # Parse cron expression
121
+ cron_parts = crontab.strip().split()
122
+ if len(cron_parts) != 5:
123
+ raise ValueError(f"Invalid cron format: {crontab} (expected 5 parts)")
124
+
125
+ minute, hour, day, month, day_of_week = cron_parts
126
+
127
+ # Create APScheduler cron trigger
128
+ trigger = APSCronTrigger(
129
+ minute=minute,
130
+ hour=hour,
131
+ day=day,
132
+ month=month,
133
+ day_of_week=day_of_week,
134
+ timezone='UTC'
135
+ )
136
+
137
+ # Schedule the job
138
+ job = self.scheduler.add_job(
139
+ self._execute_cron_job_with_monitoring,
140
+ trigger=trigger,
141
+ args=[registration],
142
+ id=f"cron_{registration_id}",
143
+ name=f"Cron job for registration {registration_id}",
144
+ max_instances=1, # Prevent overlapping executions
145
+ replace_existing=True
146
+ )
147
+
148
+ self.active_jobs[registration_id] = job.id
149
+ logger.info(f"✓ Scheduled cron job: '{crontab}' for registration {registration_id}")
150
+
151
+ except Exception as e:
152
+ logger.error(f"Failed to schedule cron job for registration {registration_id}: {e}")
153
+ raise
154
+
155
+ async def _unschedule_cron_job(self, registration_id: str):
156
+ """Remove a cron job from the scheduler."""
157
+ if registration_id in self.active_jobs:
158
+ job_id = self.active_jobs[registration_id]
159
+ try:
160
+ self.scheduler.remove_job(job_id)
161
+ del self.active_jobs[registration_id]
162
+ logger.info(f"✓ Unscheduled cron job for registration {registration_id}")
163
+ except Exception as e:
164
+ logger.error(f"Failed to unschedule cron job {job_id}: {e}")
165
+ raise
166
+ else:
167
+ logger.warning(f"Attempted to unschedule non-existent cron job {registration_id}")
168
+
169
+ async def _execute_cron_job_with_monitoring(self, registration: Dict[str, Any]):
170
+ """Execute a scheduled cron job with full monitoring and error handling."""
171
+ registration_id = registration["id"]
172
+ cron_pattern = registration["resource"]["crontab"]
173
+
174
+ execution = CronJobExecution(
175
+ registration_id=registration_id,
176
+ cron_pattern=cron_pattern,
177
+ scheduled_time=datetime.utcnow(),
178
+ actual_start_time=datetime.utcnow(),
179
+ status="running"
180
+ )
181
+
182
+ logger.info(f"🕐 Executing cron job {registration_id} with pattern '{cron_pattern}'")
183
+
184
+ try:
185
+ agents_invoked = await self._execute_cron_job(registration)
186
+ execution.status = "completed"
187
+ execution.agents_invoked = agents_invoked
188
+ logger.info(f"✓ Cron job {registration_id} completed - invoked {agents_invoked} agents")
189
+
190
+ except Exception as e:
191
+ execution.status = "failed"
192
+ execution.error_message = str(e)
193
+ logger.error(f"✗ Cron job {registration_id} failed: {e}")
194
+
195
+ finally:
196
+ execution.completion_time = datetime.utcnow()
197
+ await self._record_execution(execution)
198
+
199
+ async def _execute_cron_job(self, registration: Dict[str, Any]) -> int:
200
+ """Execute a scheduled cron job - invoke agents."""
201
+ registration_id = registration["id"]
202
+ user_id = registration["user_id"]
203
+
204
+ # Get agent links
205
+ agent_links = await self.trigger_server.database.get_agents_for_trigger(registration_id)
206
+
207
+ if not agent_links:
208
+ logger.warning(f"No agents linked to cron job {registration_id}")
209
+ return 0
210
+
211
+ agents_invoked = 0
212
+ for agent_link in agent_links:
213
+ agent_id = agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
214
+
215
+ agent_input = {
216
+ "messages": [
217
+ {"role": "human", "content": ""}
218
+ ]
219
+ }
220
+
221
+ try:
222
+ success = await self.trigger_server._invoke_agent(
223
+ agent_id=agent_id,
224
+ user_id=user_id,
225
+ input_data=agent_input,
226
+ )
227
+ if success:
228
+ agents_invoked += 1
229
+ logger.info(f"✓ Invoked agent {agent_id} for cron job {registration_id}")
230
+
231
+ except Exception as e:
232
+ logger.error(f"✗ Error invoking agent {agent_id} for cron job {registration_id}: {e}")
233
+
234
+ return agents_invoked
235
+
236
+ async def _record_execution(self, execution: CronJobExecution):
237
+ """Record execution history (in memory for now)."""
238
+ self.execution_history.append(execution)
239
+
240
+ # Keep only recent executions
241
+ if len(self.execution_history) > self.max_history:
242
+ self.execution_history = self.execution_history[-self.max_history:]
243
+
244
+ def get_active_jobs(self) -> Dict[str, str]:
245
+ """Get currently active cron jobs."""
246
+ return self.active_jobs.copy()
247
+
248
+ def get_execution_history(self, limit: int = 100) -> list[CronJobExecution]:
249
+ """Get recent execution history."""
250
+ return self.execution_history[-limit:]
251
+
252
+ def get_job_status(self) -> Dict[str, Any]:
253
+ """Get status information about the cron manager."""
254
+ return {
255
+ "active_jobs": len(self.active_jobs),
256
+ "scheduler_running": self.scheduler.running,
257
+ "total_executions": len(self.execution_history),
258
+ "active_job_ids": list(self.active_jobs.keys())
259
+ }
@@ -253,7 +253,7 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
253
253
  try:
254
254
  response = self.client.table("trigger_registrations").select(
255
255
  "*, trigger_templates(id, name, description)"
256
- ).eq("trigger_templates.id", template_id).execute()
256
+ ).eq("template_id", template_id).execute()
257
257
 
258
258
  return response.data or []
259
259
  except Exception as e:
@@ -0,0 +1,7 @@
1
+ """Built-in trigger templates for the LangChain Triggers Framework."""
2
+
3
+ from .cron_trigger import cron_trigger
4
+
5
+ __all__ = [
6
+ "cron_trigger",
7
+ ]
@@ -0,0 +1,97 @@
1
+ """Cron-based trigger for scheduled agent execution."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Dict, Any
6
+
7
+ from croniter import croniter
8
+ from langchain_auth.client import Client
9
+ from pydantic import Field
10
+
11
+ from langchain_triggers.core import (
12
+ TriggerRegistrationModel,
13
+ TriggerHandlerResult,
14
+ TriggerRegistrationResult,
15
+ )
16
+ from langchain_triggers.decorators import TriggerTemplate
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class CronRegistration(TriggerRegistrationModel):
22
+ """Registration model for cron triggers - just a crontab pattern."""
23
+
24
+ crontab: str = Field(
25
+ ...,
26
+ description="Cron pattern (e.g., '0 9 * * MON-FRI', '*/15 * * * *')",
27
+ examples=["0 9 * * MON-FRI", "*/15 * * * *", "0 2 * * SUN"],
28
+ )
29
+
30
+
31
+ async def cron_registration_handler(
32
+ user_id: str, auth_client: Client, registration: CronRegistration
33
+ ) -> TriggerRegistrationResult:
34
+ """Handle cron trigger registration - validates cron pattern and prepares for scheduling."""
35
+ logger.info(f"Cron registration request: {registration}")
36
+
37
+ cron_pattern = registration.crontab.strip()
38
+
39
+ # Validate cron pattern
40
+ try:
41
+ if not croniter.is_valid(cron_pattern):
42
+ return TriggerRegistrationResult(
43
+ create_registration=False,
44
+ response_body={
45
+ "success": False,
46
+ "error": "invalid_cron_pattern",
47
+ "message": f"Invalid cron pattern: '{cron_pattern}'"
48
+ },
49
+ status_code=400
50
+ )
51
+ except Exception as e:
52
+ return TriggerRegistrationResult(
53
+ create_registration=False,
54
+ response_body={
55
+ "success": False,
56
+ "error": "cron_validation_failed",
57
+ "message": f"Failed to validate cron pattern: {str(e)}"
58
+ },
59
+ status_code=400
60
+ )
61
+
62
+ logger.info(f"Successfully validated cron pattern: {cron_pattern}")
63
+ return TriggerRegistrationResult(
64
+ metadata={
65
+ "cron_pattern": cron_pattern,
66
+ "timezone": "UTC",
67
+ "created_at": datetime.utcnow().isoformat(),
68
+ "validated": True
69
+ }
70
+ )
71
+
72
+
73
+ async def cron_trigger_handler(
74
+ payload: Dict[str, Any],
75
+ query_params: Dict[str, str],
76
+ database,
77
+ auth_client: Client,
78
+ ) -> TriggerHandlerResult:
79
+ """Cron trigger handler - this should never be called via HTTP."""
80
+ logger.warning("Cron trigger handler called via HTTP - this shouldn't happen")
81
+ return TriggerHandlerResult(
82
+ invoke_agent=False,
83
+ response_body={
84
+ "success": False,
85
+ "message": "Cron triggers are executed by scheduler, not HTTP requests"
86
+ }
87
+ )
88
+
89
+
90
+ cron_trigger = TriggerTemplate(
91
+ id="cron-trigger",
92
+ name="Cron Scheduler",
93
+ description="Triggers agents on a cron schedule",
94
+ registration_model=CronRegistration,
95
+ registration_handler=cron_registration_handler,
96
+ trigger_handler=cron_trigger_handler,
97
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langchain-trigger-server"
7
- version = "0.1.9"
7
+ version = "0.1.11"
8
8
  description = "Generic event-driven triggers framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -33,6 +33,8 @@ dependencies = [
33
33
  "langgraph-sdk>=0.2.6",
34
34
  "supabase>=2.0.0",
35
35
  "cryptography>=3.0.0",
36
+ "APScheduler>=3.10.0",
37
+ "croniter>=1.4.0",
36
38
  ]
37
39
 
38
40
  [project.optional-dependencies]