langchain-trigger-server 0.2.6rc8__py3-none-any.whl → 0.2.8__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.2.6rc8.dist-info → langchain_trigger_server-0.2.8.dist-info}/METADATA +4 -5
- langchain_trigger_server-0.2.8.dist-info/RECORD +15 -0
- langchain_triggers/__init__.py +8 -3
- langchain_triggers/app.py +351 -253
- langchain_triggers/auth/__init__.py +3 -4
- langchain_triggers/auth/slack_hmac.py +21 -26
- langchain_triggers/core.py +58 -27
- langchain_triggers/cron_manager.py +79 -56
- langchain_triggers/database/__init__.py +2 -2
- langchain_triggers/database/interface.py +55 -68
- langchain_triggers/database/supabase.py +217 -159
- langchain_triggers/decorators.py +52 -25
- langchain_triggers/triggers/__init__.py +1 -1
- langchain_triggers/triggers/cron_trigger.py +11 -11
- langchain_trigger_server-0.2.6rc8.dist-info/RECORD +0 -15
- {langchain_trigger_server-0.2.6rc8.dist-info → langchain_trigger_server-0.2.8.dist-info}/WHEEL +0 -0
|
@@ -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 =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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() ->
|
|
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[
|
|
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
|
-
|
|
87
|
-
|
|
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
|
langchain_triggers/core.py
CHANGED
|
@@ -3,51 +3,59 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import Any
|
|
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:
|
|
15
|
+
|
|
16
|
+
token: str | None = None
|
|
17
17
|
auth_required: bool = False
|
|
18
|
-
auth_url:
|
|
19
|
-
auth_id:
|
|
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:
|
|
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:
|
|
41
|
-
metadata:
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,42 @@ class TriggerHandlerResult(BaseModel):
|
|
|
60
68
|
|
|
61
69
|
class TriggerRegistrationResult(BaseModel):
|
|
62
70
|
"""Result returned by registration handlers."""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
91
|
+
|
|
92
|
+
if "client_metadata" in self.metadata:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"The 'client_metadata' key is reserved for client-provided metadata. "
|
|
95
|
+
"Registration handlers must not use this key in their metadata."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not self.create_registration and (
|
|
99
|
+
not self.response_body or not self.status_code
|
|
100
|
+
):
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"Both response_body and status_code are required when create_registration=False"
|
|
103
|
+
)
|
|
74
104
|
|
|
75
105
|
|
|
76
106
|
class TriggerRegistrationModel(BaseModel):
|
|
77
107
|
"""Base class for trigger resource models that define how webhooks are matched to registrations."""
|
|
78
|
-
|
|
108
|
+
|
|
109
|
+
pass
|
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import
|
|
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:
|
|
23
|
+
completion_time: datetime | None = None
|
|
26
24
|
status: str # "running", "completed", "failed"
|
|
27
|
-
error_message:
|
|
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=
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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=
|
|
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(
|
|
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(
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
-
{
|
|
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(
|
|
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) ->
|
|
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) ->
|
|
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"]
|