langchain-trigger-server 0.3.22__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.
@@ -0,0 +1,177 @@
1
+ """Cron-based trigger for scheduled agent execution."""
2
+
3
+ import logging
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from croniter import croniter
9
+ from fastapi import Request
10
+ from langgraph_sdk import get_client
11
+ from pydantic import Field
12
+
13
+ from langchain_triggers.core import (
14
+ TriggerRegistrationModel,
15
+ TriggerRegistrationResult,
16
+ TriggerType,
17
+ )
18
+ from langchain_triggers.decorators import TriggerTemplate
19
+ from langchain_triggers.util import (
20
+ create_service_auth_headers,
21
+ get_langgraph_url,
22
+ is_assistant_triggers_paused,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Global constant for cron trigger ID (UUID format to match database schema)
28
+ CRON_TRIGGER_ID = "c809e66e-0000-4000-8000-000000000001"
29
+
30
+
31
+ class CronRegistration(TriggerRegistrationModel):
32
+ """Registration model for cron triggers - just a crontab pattern."""
33
+
34
+ crontab: str = Field(
35
+ ...,
36
+ description="Cron pattern (e.g., '0 9 * * MON-FRI', '*/15 * * * *')",
37
+ examples=["0 9 * * MON-FRI", "*/15 * * * *", "0 2 * * SUN"],
38
+ )
39
+
40
+
41
+ async def cron_registration_handler(
42
+ request: Request, user_id: str, registration: CronRegistration
43
+ ) -> TriggerRegistrationResult:
44
+ """Handle cron trigger registration - validates cron pattern and prepares for scheduling."""
45
+ logger.info(f"Cron registration request: {registration}")
46
+
47
+ cron_pattern = registration.crontab.strip()
48
+
49
+ # Validate cron pattern
50
+ try:
51
+ if not croniter.is_valid(cron_pattern):
52
+ return TriggerRegistrationResult(
53
+ create_registration=False,
54
+ response_body={
55
+ "success": False,
56
+ "error": "invalid_cron_pattern",
57
+ "message": f"Invalid cron pattern: '{cron_pattern}'",
58
+ },
59
+ status_code=400,
60
+ )
61
+ except Exception as e:
62
+ return TriggerRegistrationResult(
63
+ create_registration=False,
64
+ response_body={
65
+ "success": False,
66
+ "error": "cron_validation_failed",
67
+ "message": f"Failed to validate cron pattern: {str(e)}",
68
+ },
69
+ status_code=400,
70
+ )
71
+
72
+ logger.info(f"Successfully validated cron pattern: {cron_pattern}")
73
+ return TriggerRegistrationResult(
74
+ metadata={
75
+ "cron_pattern": cron_pattern,
76
+ "timezone": "UTC",
77
+ "created_at": datetime.utcnow().isoformat(),
78
+ "validated": True,
79
+ }
80
+ )
81
+
82
+
83
+ async def cron_poll_handler(
84
+ registration: dict[str, Any],
85
+ database,
86
+ ) -> dict[str, Any]:
87
+ """Polling handler for generic cron - invokes agents directly."""
88
+ registration_id = registration["id"]
89
+ user_id = str(registration["user_id"])
90
+ tenant_id = str(registration.get("tenant_id", ""))
91
+ organization_id = str(registration.get("organization_id", ""))
92
+ langgraph_url = get_langgraph_url(registration)
93
+
94
+ agent_links = await database.get_agents_for_trigger(registration_id)
95
+
96
+ if not agent_links:
97
+ logger.info(f"cron_no_linked_agents registration_id={registration_id}")
98
+ return {"success": True, "message": "No linked agents", "agents_invoked": 0}
99
+
100
+ client = get_client(url=langgraph_url, api_key=None)
101
+ headers = create_service_auth_headers(user_id, tenant_id, organization_id)
102
+
103
+ current_time = datetime.utcnow()
104
+ current_time_str = current_time.strftime("%A, %B %d, %Y at %H:%M UTC")
105
+
106
+ agents_invoked = 0
107
+ for agent_link in agent_links:
108
+ agent_id = str(
109
+ agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
110
+ )
111
+
112
+ paused = await is_assistant_triggers_paused(client, agent_id, headers)
113
+ if paused:
114
+ logger.info(
115
+ f"cron_triggers_paused_skip agent_id={agent_id} registration_id={registration_id}"
116
+ )
117
+ continue
118
+
119
+ try:
120
+ thread_id = str(uuid.uuid4())
121
+
122
+ try:
123
+ thread_result = await client.threads.create(
124
+ thread_id=thread_id,
125
+ if_exists="do_nothing",
126
+ metadata={
127
+ "triggered_by": "cron-trigger",
128
+ "user_id": user_id,
129
+ "tenant_id": tenant_id,
130
+ "registration_id": str(registration_id),
131
+ },
132
+ headers=headers,
133
+ )
134
+ logger.info(
135
+ f"cron_thread_create_success thread_id={thread_id} result={thread_result}"
136
+ )
137
+ except Exception as thread_err:
138
+ logger.exception(
139
+ f"cron_thread_create_failed thread_id={thread_id} error={str(thread_err)}"
140
+ )
141
+
142
+ await client.runs.create(
143
+ thread_id,
144
+ agent_id,
145
+ input={
146
+ "messages": [
147
+ {
148
+ "role": "human",
149
+ "content": f"Cron trigger fired at {current_time_str}",
150
+ }
151
+ ]
152
+ },
153
+ headers=headers,
154
+ metadata={"source": "trigger"},
155
+ )
156
+ logger.info(
157
+ f"cron_run_ok registration_id={registration_id} agent_id={agent_id} thread_id={thread_id}"
158
+ )
159
+ agents_invoked += 1
160
+ except Exception as e:
161
+ logger.exception(
162
+ f"cron_run_err registration_id={registration_id} agent_id={agent_id} error={str(e)}"
163
+ )
164
+
165
+ return {"success": True, "agents_invoked": agents_invoked}
166
+
167
+
168
+ cron_trigger = TriggerTemplate(
169
+ id=CRON_TRIGGER_ID,
170
+ description="Triggers agents on a predetermined schedule",
171
+ registration_model=CronRegistration,
172
+ registration_handler=cron_registration_handler,
173
+ trigger_type=TriggerType.POLLING,
174
+ poll_handler=cron_poll_handler,
175
+ display_name="Cron",
176
+ integration=None,
177
+ )
@@ -0,0 +1,122 @@
1
+ """Utility functions for trigger handlers."""
2
+
3
+ import logging
4
+ import os
5
+ from datetime import UTC, datetime, timedelta
6
+ from typing import Any
7
+
8
+ import jwt
9
+ from langgraph_sdk.client import LangGraphClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def get_x_service_jwt_token(
15
+ payload: dict[str, Any] | None = None, expiration_seconds: int = 60 * 60
16
+ ) -> str:
17
+ """Create X-Service-Key JWT token for service-to-service authentication.
18
+
19
+ Args:
20
+ payload: Optional payload to include in JWT
21
+ expiration_seconds: Token expiration time in seconds (default 1 hour)
22
+
23
+ Returns:
24
+ JWT token string
25
+ """
26
+ exp_datetime = datetime.now(tz=UTC) + timedelta(seconds=expiration_seconds)
27
+ exp = int(exp_datetime.timestamp())
28
+
29
+ payload = payload or {}
30
+ payload = {
31
+ "sub": "unspecified",
32
+ "exp": exp,
33
+ **payload,
34
+ }
35
+
36
+ secret = os.environ["X_SERVICE_AUTH_JWT_SECRET"]
37
+
38
+ return jwt.encode(
39
+ payload,
40
+ secret,
41
+ algorithm="HS256",
42
+ )
43
+
44
+
45
+ def create_service_auth_headers(
46
+ user_id: str,
47
+ tenant_id: str,
48
+ organization_id: str,
49
+ ) -> dict[str, str]:
50
+ """Create authentication headers with X-Service-Key JWT token.
51
+
52
+ Args:
53
+ user_id: User ID for the request
54
+ tenant_id: Tenant ID for the request
55
+
56
+ Returns:
57
+ Dictionary of authentication headers
58
+ """
59
+ headers = {
60
+ "x-api-key": "",
61
+ "x-auth-scheme": "langsmith-agent",
62
+ "x-user-id": user_id,
63
+ "x-tenant-id": tenant_id,
64
+ "x-organization-id": organization_id,
65
+ "x-service-key": get_x_service_jwt_token(
66
+ payload={
67
+ "tenant_id": tenant_id,
68
+ "user_id": user_id,
69
+ }
70
+ ),
71
+ }
72
+
73
+ return headers
74
+
75
+
76
+ def get_langgraph_url(registration: dict[str, Any]) -> str:
77
+ """Get the LangGraph API URL for a given registration
78
+ by comparing the registration's organization ID to the
79
+ LANGCHAIN_ORGANIZATION_ID setting."""
80
+ reg_organization_id = str(registration.get("organization_id"))
81
+
82
+ langgraph_api_url = os.getenv("LANGGRAPH_API_URL", "http://localhost:2024")
83
+ langgraph_api_url_public = os.getenv(
84
+ "LANGGRAPH_API_URL_PUBLIC", "http://localhost:2024"
85
+ )
86
+ langchain_organization_id = os.getenv(
87
+ "LANGCHAIN_ORGANIZATION_ID", "f5c798a2-2155-4999-ad27-6d466bd26e1c"
88
+ )
89
+
90
+ return (
91
+ langgraph_api_url
92
+ if reg_organization_id == langchain_organization_id
93
+ else langgraph_api_url_public
94
+ )
95
+
96
+
97
+ async def is_assistant_triggers_paused(
98
+ client: LangGraphClient, agent_id: str, headers: dict[str, str] | None = None
99
+ ) -> bool:
100
+ """Check if triggers are paused for the given assistant.
101
+
102
+ Looks for a boolean `triggers_paused` under `assistant.config.configurable`.
103
+ If the assistant cannot be fetched or the field is absent, returns False.
104
+ """
105
+ try:
106
+ assistant = await client.assistants.get(agent_id, headers=headers)
107
+ if not assistant:
108
+ logger.warning(
109
+ f"assistant_triggers_paused_check_failed agent_id={agent_id} error=assistant_not_found"
110
+ )
111
+ return False
112
+ config = assistant.get("config") or {}
113
+ configurable = config.get("configurable") or {}
114
+ paused = bool(configurable.get("triggers_paused", False))
115
+ if paused:
116
+ logger.debug(f"assistant_triggers_paused agent_id={agent_id} paused=True")
117
+ return paused
118
+ except Exception as e:
119
+ logger.warning(
120
+ f"assistant_triggers_paused_check_failed agent_id={agent_id} error={str(e)}"
121
+ )
122
+ return False