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.

langchain_triggers/app.py CHANGED
@@ -4,68 +4,71 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import os
7
- from typing import Any, Callable, Dict, List, Optional
7
+ from collections.abc import Callable
8
+ from typing import Any
8
9
 
9
- from fastapi import FastAPI, HTTPException, Request, Depends
10
- from langgraph_sdk import get_client
10
+ from fastapi import Depends, FastAPI, HTTPException, Request
11
11
  from langchain_auth.client import Client
12
+ from langgraph_sdk import get_client
12
13
  from starlette.middleware.base import BaseHTTPMiddleware
13
14
  from starlette.responses import Response
14
15
 
15
- from .decorators import TriggerTemplate
16
- from .database import create_database, TriggerDatabaseInterface
17
- from .cron_manager import CronTriggerManager
18
- from .triggers.cron_trigger import CRON_TRIGGER_ID
19
16
  from .auth.slack_hmac import (
20
- verify_slack_signature,
21
- get_slack_signing_secret,
22
- extract_slack_headers,
23
17
  SlackSignatureVerificationError,
18
+ extract_slack_headers,
19
+ get_slack_signing_secret,
20
+ verify_slack_signature,
24
21
  )
22
+ from .cron_manager import CronTriggerManager
23
+ from .database import TriggerDatabaseInterface, create_database
24
+ from .decorators import TriggerTemplate
25
+ from .triggers.cron_trigger import CRON_TRIGGER_ID
25
26
 
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
 
29
30
  class AuthenticationMiddleware(BaseHTTPMiddleware):
30
31
  """Middleware to handle authentication for API endpoints."""
31
-
32
+
32
33
  def __init__(self, app, auth_handler: Callable):
33
34
  super().__init__(app)
34
35
  self.auth_handler = auth_handler
35
-
36
+
36
37
  async def dispatch(self, request: Request, call_next):
37
38
  # Skip auth for webhooks, health/root endpoints, and OPTIONS requests
38
- if (request.url.path.startswith("/v1/triggers/webhooks/") or
39
- request.url.path in ["/", "/health"] or
40
- request.method == "OPTIONS"):
39
+ if (
40
+ request.url.path.startswith("/v1/triggers/webhooks/")
41
+ or request.url.path in ["/", "/health"]
42
+ or request.method == "OPTIONS"
43
+ ):
41
44
  return await call_next(request)
42
-
45
+
43
46
  try:
44
47
  # Run mandatory custom authentication
45
48
  identity = await self.auth_handler({}, dict(request.headers))
46
-
49
+
47
50
  if not identity or not identity.get("identity"):
48
51
  return Response(
49
52
  content='{"detail": "Authentication required"}',
50
53
  status_code=401,
51
- media_type="application/json"
54
+ media_type="application/json",
52
55
  )
53
-
56
+
54
57
  # Store identity in request state for endpoints to access
55
58
  request.state.current_user = identity
56
-
59
+
57
60
  except Exception as e:
58
61
  logger.error(f"Authentication middleware error: {e}")
59
62
  return Response(
60
63
  content='{"detail": "Authentication failed"}',
61
64
  status_code=401,
62
- media_type="application/json"
65
+ media_type="application/json",
63
66
  )
64
-
67
+
65
68
  return await call_next(request)
66
69
 
67
70
 
68
- def get_current_user(request: Request) -> Dict[str, Any]:
71
+ def get_current_user(request: Request) -> dict[str, Any]:
69
72
  """FastAPI dependency to get the current authenticated user."""
70
73
  if not hasattr(request.state, "current_user"):
71
74
  raise HTTPException(status_code=401, detail="Authentication required")
@@ -74,12 +77,12 @@ def get_current_user(request: Request) -> Dict[str, Any]:
74
77
 
75
78
  class TriggerServer:
76
79
  """FastAPI application for trigger webhooks."""
77
-
80
+
78
81
  def __init__(
79
82
  self,
80
83
  auth_handler: Callable,
81
- database: Optional[TriggerDatabaseInterface] = None,
82
- database_type: Optional[str] = "supabase",
84
+ database: TriggerDatabaseInterface | None = None,
85
+ database_type: str | None = "supabase",
83
86
  **database_kwargs: Any,
84
87
  ):
85
88
  # Configure uvicorn logging to use consistent formatting
@@ -88,7 +91,7 @@ class TriggerServer:
88
91
  self.app = FastAPI(
89
92
  title="Triggers Server",
90
93
  description="Event-driven triggers framework",
91
- version="0.1.0"
94
+ version="0.1.0",
92
95
  )
93
96
 
94
97
  # Configure database: allow either instance injection or factory creation
@@ -100,104 +103,117 @@ class TriggerServer:
100
103
  else:
101
104
  self.database = create_database(database_type, **database_kwargs)
102
105
  self.auth_handler = auth_handler
103
-
106
+
104
107
  # LangGraph configuration
105
108
  self.langgraph_api_url = os.getenv("LANGGRAPH_API_URL")
106
109
  self.langsmith_api_key = os.getenv("LANGCHAIN_API_KEY")
107
110
  self.trigger_server_auth_api_url = os.getenv("TRIGGER_SERVER_HOST_API_URL")
108
-
111
+
109
112
  if not self.langgraph_api_url:
110
113
  raise ValueError("LANGGRAPH_API_URL environment variable is required")
111
-
114
+
112
115
  self.langgraph_api_url = self.langgraph_api_url.rstrip("/")
113
116
 
114
117
  # Initialize LangGraph SDK client
115
- self.langgraph_client = get_client(url=self.langgraph_api_url, api_key=self.langsmith_api_key)
116
- logger.info(f"✓ LangGraph client initialized with URL: {self.langgraph_api_url}")
118
+ self.langgraph_client = get_client(
119
+ url=self.langgraph_api_url, api_key=self.langsmith_api_key
120
+ )
121
+ logger.info(
122
+ f"✓ LangGraph client initialized with URL: {self.langgraph_api_url}"
123
+ )
117
124
  if self.langsmith_api_key:
118
- logger.info(f"✓ LangGraph client initialized with API key.")
125
+ logger.info("✓ LangGraph client initialized with API key.")
119
126
  else:
120
127
  logger.warning("⚠ LangGraph client initialized without API key")
121
128
 
122
129
  # Initialize LangChain auth client
123
130
  langchain_api_key = os.getenv("LANGCHAIN_API_KEY")
124
131
  if langchain_api_key:
125
- self.langchain_auth_client = Client(api_key=langchain_api_key, api_url=self.trigger_server_auth_api_url)
132
+ self.langchain_auth_client = Client(
133
+ api_key=langchain_api_key, api_url=self.trigger_server_auth_api_url
134
+ )
126
135
  logger.info("✓ LangChain auth client initialized")
127
136
  else:
128
137
  self.langchain_auth_client = None
129
- logger.warning("LANGCHAIN_API_KEY not found - OAuth token injection disabled")
130
-
131
- self.triggers: List[TriggerTemplate] = []
132
-
138
+ logger.warning(
139
+ "LANGCHAIN_API_KEY not found - OAuth token injection disabled"
140
+ )
141
+
142
+ self.triggers: list[TriggerTemplate] = []
143
+
133
144
  # Initialize CronTriggerManager
134
145
  self.cron_manager = CronTriggerManager(self)
135
-
146
+
136
147
  # Setup authentication middleware
137
148
  self.app.add_middleware(AuthenticationMiddleware, auth_handler=auth_handler)
138
-
149
+
139
150
  # Setup routes
140
151
  self._setup_routes()
141
-
152
+
142
153
  # Add startup and shutdown events
143
154
  @self.app.on_event("startup")
144
155
  async def startup_event():
145
156
  await self.ensure_trigger_templates()
146
157
  await self.cron_manager.start()
147
-
158
+
148
159
  @self.app.on_event("shutdown")
149
160
  async def shutdown_event():
150
161
  await self.cron_manager.shutdown()
151
-
162
+
152
163
  def _configure_uvicorn_logging(self) -> None:
153
164
  """Configure uvicorn loggers to use consistent formatting for production deployments."""
154
165
  formatter = logging.Formatter("%(levelname)s: %(name)s - %(message)s")
155
-
166
+
156
167
  # Configure uvicorn access logger
157
168
  uvicorn_access_logger = logging.getLogger("uvicorn.access")
158
169
  uvicorn_access_logger.handlers.clear()
159
170
  access_handler = logging.StreamHandler()
160
171
  access_handler.setFormatter(formatter)
161
172
  uvicorn_access_logger.addHandler(access_handler)
162
-
163
- # Configure uvicorn error logger
173
+
174
+ # Configure uvicorn error logger
164
175
  uvicorn_error_logger = logging.getLogger("uvicorn.error")
165
176
  uvicorn_error_logger.handlers.clear()
166
177
  error_handler = logging.StreamHandler()
167
178
  error_handler.setFormatter(formatter)
168
179
  uvicorn_error_logger.addHandler(error_handler)
169
-
180
+
170
181
  # Configure uvicorn main logger
171
182
  uvicorn_logger = logging.getLogger("uvicorn")
172
183
  uvicorn_logger.handlers.clear()
173
184
  main_handler = logging.StreamHandler()
174
185
  main_handler.setFormatter(formatter)
175
186
  uvicorn_logger.addHandler(main_handler)
176
-
187
+
177
188
  def add_trigger(self, trigger: TriggerTemplate) -> None:
178
189
  """Add a trigger template to the app."""
179
190
  # Check for duplicate IDs
180
191
  if any(t.id == trigger.id for t in self.triggers):
181
192
  raise ValueError(f"Trigger with id '{trigger.id}' already exists")
182
-
193
+
183
194
  self.triggers.append(trigger)
184
195
 
185
196
  if trigger.trigger_handler:
186
- async def handler_endpoint(request: Request) -> Dict[str, Any]:
197
+
198
+ async def handler_endpoint(request: Request) -> dict[str, Any]:
187
199
  return await self._handle_request(trigger, request)
188
-
200
+
189
201
  handler_path = f"/v1/triggers/webhooks/{trigger.id}"
190
202
  self.app.post(handler_path)(handler_endpoint)
191
203
  logger.info(f"Added handler route: POST {handler_path}")
192
-
193
- logger.info(f"Registered trigger template in memory: {trigger.name} ({trigger.id})")
194
-
204
+
205
+ logger.info(
206
+ f"Registered trigger template in memory: {trigger.name} ({trigger.id})"
207
+ )
208
+
195
209
  async def ensure_trigger_templates(self) -> None:
196
210
  """Ensure all registered trigger templates exist in the database."""
197
211
  for trigger in self.triggers:
198
212
  existing = await self.database.get_trigger_template(trigger.id)
199
213
  if not existing:
200
- logger.info(f"Creating new trigger template in database: {trigger.name} ({trigger.id})")
214
+ logger.info(
215
+ f"Creating new trigger template in database: {trigger.name} ({trigger.id})"
216
+ )
201
217
  await self.database.create_trigger_template(
202
218
  id=trigger.id,
203
219
  provider=trigger.provider,
@@ -205,81 +221,91 @@ class TriggerServer:
205
221
  description=trigger.description,
206
222
  registration_schema=trigger.registration_model.model_json_schema(),
207
223
  )
208
- logger.info(f"✓ Successfully created trigger template: {trigger.name} ({trigger.id})")
224
+ logger.info(
225
+ f"✓ Successfully created trigger template: {trigger.name} ({trigger.id})"
226
+ )
209
227
  else:
210
- logger.info(f"✓ Trigger template already exists in database: {trigger.name} ({trigger.id})")
211
-
212
- def add_triggers(self, triggers: List[TriggerTemplate]) -> None:
228
+ logger.info(
229
+ f"✓ Trigger template already exists in database: {trigger.name} ({trigger.id})"
230
+ )
231
+
232
+ def add_triggers(self, triggers: list[TriggerTemplate]) -> None:
213
233
  """Add multiple triggers."""
214
234
  for trigger in triggers:
215
235
  self.add_trigger(trigger)
216
-
236
+
217
237
  def _setup_routes(self) -> None:
218
238
  """Setup built-in API routes."""
219
-
239
+
220
240
  @self.app.get("/")
221
- async def root() -> Dict[str, str]:
241
+ async def root() -> dict[str, str]:
222
242
  return {"message": "Triggers Server", "version": "0.1.0"}
223
-
243
+
224
244
  @self.app.get("/health")
225
- async def health() -> Dict[str, str]:
245
+ async def health() -> dict[str, str]:
226
246
  return {"status": "healthy"}
227
-
247
+
228
248
  @self.app.get("/v1/triggers")
229
- async def api_list_triggers() -> Dict[str, Any]:
249
+ async def api_list_triggers() -> dict[str, Any]:
230
250
  """List available trigger templates."""
231
251
  templates = await self.database.get_trigger_templates()
232
252
  trigger_list = []
233
253
  for template in templates:
234
- trigger_list.append({
235
- "id": template["id"],
236
- "provider": template["provider"],
237
- "displayName": template["name"],
238
- "description": template["description"],
239
- "path": "/v1/triggers/registrations",
240
- "method": "POST",
241
- "payloadSchema": template.get("registration_schema", {}),
242
- })
243
-
244
- return {
245
- "success": True,
246
- "data": trigger_list
247
- }
248
-
254
+ trigger_list.append(
255
+ {
256
+ "id": template["id"],
257
+ "provider": template["provider"],
258
+ "displayName": template["name"],
259
+ "description": template["description"],
260
+ "path": "/v1/triggers/registrations",
261
+ "method": "POST",
262
+ "payloadSchema": template.get("registration_schema", {}),
263
+ }
264
+ )
265
+
266
+ return {"success": True, "data": trigger_list}
267
+
249
268
  @self.app.get("/v1/triggers/registrations")
250
- async def api_list_registrations(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
269
+ async def api_list_registrations(
270
+ current_user: dict[str, Any] = Depends(get_current_user),
271
+ ) -> dict[str, Any]:
251
272
  """List user's trigger registrations (user-scoped)."""
252
273
  try:
253
274
  user_id = current_user["identity"]
254
-
275
+
255
276
  # Get user's trigger registrations with linked agents in a single query
256
- user_registrations = await self.database.get_user_trigger_registrations_with_agents(user_id)
257
-
277
+ user_registrations = (
278
+ await self.database.get_user_trigger_registrations_with_agents(
279
+ user_id
280
+ )
281
+ )
282
+
258
283
  # Format response to match expected structure
259
284
  registrations = []
260
285
  for reg in user_registrations:
261
- registrations.append({
262
- "id": reg["id"],
263
- "user_id": reg["user_id"],
264
- "template_id": reg.get("trigger_templates", {}).get("id"),
265
- "resource": reg["resource"],
266
- "linked_agent_ids": reg.get("linked_agent_ids", []),
267
- "created_at": reg["created_at"]
268
- })
269
-
270
- return {
271
- "success": True,
272
- "data": registrations
273
- }
274
-
286
+ registrations.append(
287
+ {
288
+ "id": reg["id"],
289
+ "user_id": reg["user_id"],
290
+ "template_id": reg.get("trigger_templates", {}).get("id"),
291
+ "resource": reg["resource"],
292
+ "linked_agent_ids": reg.get("linked_agent_ids", []),
293
+ "created_at": reg["created_at"],
294
+ }
295
+ )
296
+
297
+ return {"success": True, "data": registrations}
298
+
275
299
  except HTTPException:
276
300
  raise
277
301
  except Exception as e:
278
302
  logger.error(f"Error listing registrations: {e}")
279
303
  raise HTTPException(status_code=500, detail=str(e))
280
-
304
+
281
305
  @self.app.post("/v1/triggers/registrations")
282
- async def api_create_registration(request: Request, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
306
+ async def api_create_registration(
307
+ request: Request, current_user: dict[str, Any] = Depends(get_current_user)
308
+ ) -> dict[str, Any]:
283
309
  """Create a new trigger registration."""
284
310
  try:
285
311
  payload = await request.json()
@@ -288,100 +314,126 @@ class TriggerServer:
288
314
  user_id = current_user["identity"]
289
315
  trigger_id = payload.get("type")
290
316
  if not trigger_id:
291
- raise HTTPException(status_code=400, detail="Missing required field: type")
292
-
317
+ raise HTTPException(
318
+ status_code=400, detail="Missing required field: type"
319
+ )
320
+
293
321
  trigger = next((t for t in self.triggers if t.id == trigger_id), None)
294
322
  if not trigger:
295
- raise HTTPException(status_code=400, detail=f"Unknown trigger type: {trigger_id}")
323
+ raise HTTPException(
324
+ status_code=400, detail=f"Unknown trigger type: {trigger_id}"
325
+ )
296
326
 
297
327
  # Parse payload into registration model first
298
328
  try:
299
329
  registration_instance = trigger.registration_model(**payload)
300
330
  except Exception as e:
301
331
  raise HTTPException(
302
- status_code=400,
303
- detail=f"Invalid payload for trigger: {str(e)}"
332
+ status_code=400, detail=f"Invalid payload for trigger: {str(e)}"
304
333
  )
305
-
334
+
306
335
  # Check for duplicate registration based on resource data within this user's scope
307
336
  resource_dict = registration_instance.model_dump()
308
- existing_registration = await self.database.find_user_registration_by_resource(
309
- user_id=user_id,
310
- template_id=trigger.id,
311
- resource_data=resource_dict
337
+ existing_registration = (
338
+ await self.database.find_user_registration_by_resource(
339
+ user_id=user_id,
340
+ template_id=trigger.id,
341
+ resource_data=resource_dict,
342
+ )
312
343
  )
313
344
 
314
345
  if existing_registration:
315
346
  raise HTTPException(
316
347
  status_code=400,
317
- detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}"
348
+ detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}",
318
349
  )
319
- result = await trigger.registration_handler(user_id, self.langchain_auth_client, registration_instance)
320
-
350
+ result = await trigger.registration_handler(
351
+ user_id, self.langchain_auth_client, registration_instance
352
+ )
353
+
321
354
  # Check if handler requested to skip registration (e.g., for OAuth or URL verification)
322
355
  if not result.create_registration:
323
- logger.info(f"Registration handler requested to skip database creation")
324
- from fastapi import Response
356
+ logger.info(
357
+ "Registration handler requested to skip database creation"
358
+ )
325
359
  import json
360
+
361
+ from fastapi import Response
362
+
326
363
  return Response(
327
364
  content=json.dumps(result.response_body),
328
365
  status_code=result.status_code,
329
- media_type="application/json"
366
+ media_type="application/json",
330
367
  )
331
-
368
+
332
369
  resource_dict = registration_instance.model_dump()
333
370
 
334
371
  registration = await self.database.create_trigger_registration(
335
372
  user_id=user_id,
336
373
  template_id=trigger.id,
337
374
  resource=resource_dict,
338
- metadata=result.metadata
375
+ metadata=result.metadata,
339
376
  )
340
-
377
+
341
378
  if not registration:
342
- raise HTTPException(status_code=500, detail="Failed to create trigger registration")
343
-
379
+ raise HTTPException(
380
+ status_code=500, detail="Failed to create trigger registration"
381
+ )
382
+
344
383
  # Reload cron manager to pick up any new cron registrations
345
384
  await self.cron_manager.reload_from_database()
346
-
385
+
347
386
  # Return registration result
348
387
  return {
349
388
  "success": True,
350
389
  "data": registration,
351
- "metadata": result.metadata
390
+ "metadata": result.metadata,
352
391
  }
353
-
392
+
354
393
  except HTTPException:
355
394
  raise
356
395
  except Exception as e:
357
396
  logger.exception(f"Error creating trigger registration: {e}")
358
397
  raise HTTPException(status_code=500, detail=str(e))
359
-
398
+
360
399
  @self.app.get("/v1/triggers/registrations/{registration_id}/agents")
361
- async def api_list_registration_agents(registration_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
400
+ async def api_list_registration_agents(
401
+ registration_id: str,
402
+ current_user: dict[str, Any] = Depends(get_current_user),
403
+ ) -> dict[str, Any]:
362
404
  """List agents linked to this registration."""
363
405
  try:
364
406
  user_id = current_user["identity"]
365
-
407
+
366
408
  # Get the specific trigger registration
367
- trigger = await self.database.get_trigger_registration(registration_id, user_id)
409
+ trigger = await self.database.get_trigger_registration(
410
+ registration_id, user_id
411
+ )
368
412
  if not trigger:
369
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
370
-
413
+ raise HTTPException(
414
+ status_code=404,
415
+ detail="Trigger registration not found or access denied",
416
+ )
417
+
371
418
  # Return the linked agent IDs
372
419
  return {
373
420
  "success": True,
374
- "data": trigger.get("linked_assistant_ids", [])
421
+ "data": trigger.get("linked_assistant_ids", []),
375
422
  }
376
-
423
+
377
424
  except HTTPException:
378
425
  raise
379
426
  except Exception as e:
380
427
  logger.error(f"Error getting registration agents: {e}")
381
428
  raise HTTPException(status_code=500, detail=str(e))
382
-
429
+
383
430
  @self.app.post("/v1/triggers/registrations/{registration_id}/agents/{agent_id}")
384
- async def api_add_agent_to_trigger(registration_id: str, agent_id: str, request: Request, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
431
+ async def api_add_agent_to_trigger(
432
+ registration_id: str,
433
+ agent_id: str,
434
+ request: Request,
435
+ current_user: dict[str, Any] = Depends(get_current_user),
436
+ ) -> dict[str, Any]:
385
437
  """Add an agent to a trigger registration."""
386
438
  try:
387
439
  # Parse request body for field selection
@@ -390,67 +442,80 @@ class TriggerServer:
390
442
  field_selection = body.get("field_selection")
391
443
  except:
392
444
  field_selection = None
393
-
445
+
394
446
  user_id = current_user["identity"]
395
-
447
+
396
448
  # Verify the trigger registration exists and belongs to the user
397
- registration = await self.database.get_trigger_registration(registration_id, user_id)
449
+ registration = await self.database.get_trigger_registration(
450
+ registration_id, user_id
451
+ )
398
452
  if not registration:
399
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
400
-
453
+ raise HTTPException(
454
+ status_code=404,
455
+ detail="Trigger registration not found or access denied",
456
+ )
457
+
401
458
  # Link the agent to the trigger
402
459
  success = await self.database.link_agent_to_trigger(
403
460
  agent_id=agent_id,
404
461
  registration_id=registration_id,
405
462
  created_by=user_id,
406
- field_selection=field_selection
463
+ field_selection=field_selection,
407
464
  )
408
-
465
+
409
466
  if not success:
410
- raise HTTPException(status_code=500, detail="Failed to link agent to trigger")
411
-
467
+ raise HTTPException(
468
+ status_code=500, detail="Failed to link agent to trigger"
469
+ )
470
+
412
471
  return {
413
472
  "success": True,
414
473
  "message": f"Successfully linked agent {agent_id} to trigger {registration_id}",
415
- "data": {
416
- "registration_id": registration_id,
417
- "agent_id": agent_id
418
- }
474
+ "data": {"registration_id": registration_id, "agent_id": agent_id},
419
475
  }
420
-
476
+
421
477
  except HTTPException:
422
478
  raise
423
479
  except Exception as e:
424
480
  logger.error(f"Error linking agent to trigger: {e}")
425
481
  raise HTTPException(status_code=500, detail=str(e))
426
-
427
- @self.app.delete("/v1/triggers/registrations/{registration_id}/agents/{agent_id}")
428
- async def api_remove_agent_from_trigger(registration_id: str, agent_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
482
+
483
+ @self.app.delete(
484
+ "/v1/triggers/registrations/{registration_id}/agents/{agent_id}"
485
+ )
486
+ async def api_remove_agent_from_trigger(
487
+ registration_id: str,
488
+ agent_id: str,
489
+ current_user: dict[str, Any] = Depends(get_current_user),
490
+ ) -> dict[str, Any]:
429
491
  """Remove an agent from a trigger registration."""
430
492
  try:
431
493
  user_id = current_user["identity"]
432
494
 
433
495
  # Verify the trigger registration exists and belongs to the user
434
- registration = await self.database.get_trigger_registration(registration_id, user_id)
496
+ registration = await self.database.get_trigger_registration(
497
+ registration_id, user_id
498
+ )
435
499
  if not registration:
436
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
500
+ raise HTTPException(
501
+ status_code=404,
502
+ detail="Trigger registration not found or access denied",
503
+ )
437
504
 
438
505
  # Unlink the agent from the trigger
439
506
  success = await self.database.unlink_agent_from_trigger(
440
- agent_id=agent_id,
441
- registration_id=registration_id
507
+ agent_id=agent_id, registration_id=registration_id
442
508
  )
443
509
 
444
510
  if not success:
445
- raise HTTPException(status_code=500, detail="Failed to unlink agent from trigger")
511
+ raise HTTPException(
512
+ status_code=500, detail="Failed to unlink agent from trigger"
513
+ )
446
514
 
447
515
  return {
448
516
  "success": True,
449
517
  "message": f"Successfully unlinked agent {agent_id} from trigger {registration_id}",
450
- "data": {
451
- "registration_id": registration_id,
452
- "agent_id": agent_id
453
- }
518
+ "data": {"registration_id": registration_id, "agent_id": agent_id},
454
519
  }
455
520
 
456
521
  except HTTPException:
@@ -460,20 +525,31 @@ class TriggerServer:
460
525
  raise HTTPException(status_code=500, detail=str(e))
461
526
 
462
527
  @self.app.post("/v1/triggers/registrations/{registration_id}/execute")
463
- async def api_execute_trigger_now(registration_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
528
+ async def api_execute_trigger_now(
529
+ registration_id: str,
530
+ current_user: dict[str, Any] = Depends(get_current_user),
531
+ ) -> dict[str, Any]:
464
532
  """Manually execute a cron trigger registration immediately."""
465
533
  try:
466
534
  user_id = current_user["identity"]
467
535
 
468
536
  # Verify the trigger registration exists and belongs to the user
469
- registration = await self.database.get_trigger_registration(registration_id, user_id)
537
+ registration = await self.database.get_trigger_registration(
538
+ registration_id, user_id
539
+ )
470
540
  if not registration:
471
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
541
+ raise HTTPException(
542
+ status_code=404,
543
+ detail="Trigger registration not found or access denied",
544
+ )
472
545
 
473
546
  # Get the template to check if it's a cron trigger
474
547
  template_id = registration.get("template_id")
475
548
  if template_id != CRON_TRIGGER_ID:
476
- raise HTTPException(status_code=400, detail="Manual execution is only supported for cron triggers")
549
+ raise HTTPException(
550
+ status_code=400,
551
+ detail="Manual execution is only supported for cron triggers",
552
+ )
477
553
 
478
554
  # Execute the cron trigger using the cron manager
479
555
  agents_invoked = await self.cron_manager.execute_cron_job(registration)
@@ -481,7 +557,7 @@ class TriggerServer:
481
557
  return {
482
558
  "success": True,
483
559
  "message": f"Manually executed cron trigger {registration_id}",
484
- "agents_invoked": agents_invoked
560
+ "agents_invoked": agents_invoked,
485
561
  }
486
562
 
487
563
  except HTTPException:
@@ -489,29 +565,34 @@ class TriggerServer:
489
565
  except Exception as e:
490
566
  logger.error(f"Error executing trigger: {e}")
491
567
  raise HTTPException(status_code=500, detail=str(e))
492
-
493
-
568
+
494
569
  async def _handle_request(
495
- self,
496
- trigger: TriggerTemplate,
497
- request: Request
498
- ) -> Dict[str, Any]:
570
+ self, trigger: TriggerTemplate, request: Request
571
+ ) -> dict[str, Any]:
499
572
  """Handle an incoming request with a handler function."""
500
573
  try:
501
574
  if request.method == "POST":
502
- if request.headers.get("content-type", "").startswith("application/json"):
575
+ if request.headers.get("content-type", "").startswith(
576
+ "application/json"
577
+ ):
503
578
  # Read body once for both auth and parsing
504
579
  body_bytes = await request.body()
505
580
  body_str = body_bytes.decode("utf-8")
506
-
581
+
507
582
  if self._is_slack_trigger(trigger):
508
- await self._verify_slack_webhook_auth_with_body(request, body_str)
509
-
583
+ await self._verify_slack_webhook_auth_with_body(
584
+ request, body_str
585
+ )
586
+
510
587
  import json
588
+
511
589
  payload = json.loads(body_str)
512
590
 
513
- if payload.get("type") == "url_verification" and "challenge" in payload:
514
- logger.info(f"Responding to Slack URL verification challenge")
591
+ if (
592
+ payload.get("type") == "url_verification"
593
+ and "challenge" in payload
594
+ ):
595
+ logger.info("Responding to Slack URL verification challenge")
515
596
  return {"challenge": payload["challenge"]}
516
597
  else:
517
598
  # Handle form data or other content types
@@ -521,7 +602,9 @@ class TriggerServer:
521
602
  payload = dict(request.query_params)
522
603
 
523
604
  query_params = dict(request.query_params)
524
- result = await trigger.trigger_handler(payload, query_params, self.database, self.langchain_auth_client)
605
+ result = await trigger.trigger_handler(
606
+ payload, query_params, self.database, self.langchain_auth_client
607
+ )
525
608
  if not result.invoke_agent:
526
609
  return result.response_body
527
610
 
@@ -532,16 +615,16 @@ class TriggerServer:
532
615
  # Iterate through each message and invoke agents for each
533
616
  for message in result.agent_messages:
534
617
  for agent_link in agent_links:
535
- agent_id = agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
618
+ agent_id = (
619
+ agent_link
620
+ if isinstance(agent_link, str)
621
+ else agent_link.get("agent_id")
622
+ )
536
623
  # Ensure agent_id and user_id are strings for JSON serialization
537
624
  agent_id_str = str(agent_id)
538
625
  user_id_str = str(result.registration["user_id"])
539
626
 
540
- agent_input = {
541
- "messages": [
542
- {"role": "human", "content": message}
543
- ]
544
- }
627
+ agent_input = {"messages": [{"role": "human", "content": message}]}
545
628
 
546
629
  try:
547
630
  success = await self._invoke_agent(
@@ -552,45 +635,46 @@ class TriggerServer:
552
635
  if success:
553
636
  agents_invoked += 1
554
637
  except Exception as e:
555
- logger.error(f"Error invoking agent {agent_id_str}: {e}", exc_info=True)
556
- logger.info(f"Processed trigger handler with {len(result.agent_messages)} messages, invoked {agents_invoked} agents")
557
-
558
- return {
559
- "success": True,
560
- "agents_invoked": agents_invoked
561
- }
562
-
638
+ logger.error(
639
+ f"Error invoking agent {agent_id_str}: {e}", exc_info=True
640
+ )
641
+ logger.info(
642
+ f"Processed trigger handler with {len(result.agent_messages)} messages, invoked {agents_invoked} agents"
643
+ )
644
+
645
+ return {"success": True, "agents_invoked": agents_invoked}
646
+
563
647
  except HTTPException:
564
648
  raise
565
649
  except Exception as e:
566
650
  logger.error(f"Error in trigger handler: {e}", exc_info=True)
567
651
  raise HTTPException(
568
- status_code=500,
569
- detail=f"Trigger processing failed: {str(e)}"
652
+ status_code=500, detail=f"Trigger processing failed: {str(e)}"
570
653
  )
571
-
572
-
654
+
573
655
  async def _invoke_agent(
574
656
  self,
575
657
  agent_id: str,
576
658
  user_id: str,
577
- input_data: Dict[str, Any],
659
+ input_data: dict[str, Any],
578
660
  ) -> bool:
579
661
  """Invoke LangGraph agent using the SDK."""
580
662
  # Ensure user_id is a string for JSON serialization
581
663
  user_id_str = str(user_id)
582
664
  logger.info(f"Invoking LangGraph agent {agent_id} for user {user_id_str}")
583
-
665
+
584
666
  try:
585
667
  headers = {
586
668
  "x-auth-scheme": "agent-builder-trigger",
587
669
  "x-user-id": user_id_str,
588
670
  }
589
-
671
+
590
672
  # Note: API key is already set in client initialization, no need to add to headers
591
673
  if not self.langsmith_api_key:
592
- logger.warning("No LANGSMITH_API_KEY available - authentication may fail")
593
-
674
+ logger.warning(
675
+ "No LANGSMITH_API_KEY available - authentication may fail"
676
+ )
677
+
594
678
  thread = await self.langgraph_client.threads.create(
595
679
  metadata={
596
680
  "triggered_by": "langchain-triggers",
@@ -601,7 +685,7 @@ class TriggerServer:
601
685
  logger.info(f"Created thread {thread['thread_id']} for agent {agent_id}")
602
686
 
603
687
  run = await self.langgraph_client.runs.create(
604
- thread_id=thread['thread_id'],
688
+ thread_id=thread["thread_id"],
605
689
  assistant_id=agent_id,
606
690
  input=input_data,
607
691
  metadata={
@@ -610,35 +694,39 @@ class TriggerServer:
610
694
  },
611
695
  headers=headers,
612
696
  )
613
-
614
- logger.info(f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}")
697
+
698
+ logger.info(
699
+ f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}"
700
+ )
615
701
  return True
616
-
702
+
617
703
  except Exception as e:
618
704
  # Handle 404s (agent not found) as warnings, not errors
619
- if hasattr(e, 'response') and getattr(e.response, 'status_code', None) == 404:
620
- logger.warning(f"Agent {agent_id} not found (404) - agent may have been deleted or moved")
705
+ if (
706
+ hasattr(e, "response")
707
+ and getattr(e.response, "status_code", None) == 404
708
+ ):
709
+ logger.warning(
710
+ f"Agent {agent_id} not found (404) - agent may have been deleted or moved"
711
+ )
621
712
  return False
622
713
  else:
623
714
  logger.error(f"Error invoking agent {agent_id}: {e}")
624
715
  raise
625
-
716
+
626
717
  def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
627
718
  """Check if a trigger is from Slack and requires HMAC signature verification."""
628
- return (
629
- trigger.provider.lower() == "slack" or
630
- "slack" in trigger.id.lower()
631
- )
632
-
719
+ return trigger.provider.lower() == "slack" or "slack" in trigger.id.lower()
720
+
633
721
  async def _verify_slack_webhook_auth(self, request: Request) -> None:
634
722
  """Verify Slack HMAC signature for webhook requests.
635
-
723
+
636
724
  Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
637
725
  The signature is computed from the timestamp, body, and signing secret.
638
-
726
+
639
727
  Args:
640
728
  request: The FastAPI request object
641
-
729
+
642
730
  Raises:
643
731
  HTTPException: If authentication fails
644
732
  """
@@ -648,67 +736,70 @@ class TriggerServer:
648
736
  logger.error("SLACK_SIGNING_SECRET environment variable not set")
649
737
  raise HTTPException(
650
738
  status_code=500,
651
- detail="Slack signing secret not configured on server"
739
+ detail="Slack signing secret not configured on server",
652
740
  )
653
-
741
+
654
742
  headers_dict = dict(request.headers)
655
743
  signature, timestamp = extract_slack_headers(headers_dict)
656
-
744
+
657
745
  if not signature:
658
746
  logger.error("Missing X-Slack-Signature header")
659
747
  raise HTTPException(
660
748
  status_code=401,
661
- detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
749
+ detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
662
750
  )
663
-
751
+
664
752
  if not timestamp:
665
753
  logger.error("Missing X-Slack-Request-Timestamp header")
666
754
  raise HTTPException(
667
755
  status_code=401,
668
- detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
756
+ detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
669
757
  )
670
-
758
+
671
759
  body = await request.body()
672
- body_str = body.decode('utf-8')
673
-
760
+ body_str = body.decode("utf-8")
761
+
674
762
  try:
675
763
  verify_slack_signature(
676
764
  signing_secret=signing_secret,
677
765
  timestamp=timestamp,
678
766
  body=body_str,
679
- signature=signature
767
+ signature=signature,
768
+ )
769
+ logger.info(
770
+ f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
680
771
  )
681
- logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
682
772
  except SlackSignatureVerificationError as e:
683
773
  logger.error(f"Slack signature verification failed: {e}")
684
774
  raise HTTPException(
685
775
  status_code=401,
686
- detail=f"Slack signature verification failed: {str(e)}"
776
+ detail=f"Slack signature verification failed: {str(e)}",
687
777
  )
688
-
778
+
689
779
  # Store verification info in request state
690
780
  request.state.slack_verified = True
691
781
  request.state.slack_timestamp = timestamp
692
-
782
+
693
783
  except HTTPException:
694
784
  raise
695
785
  except Exception as e:
696
786
  logger.error(f"Unexpected error during Slack webhook authentication: {e}")
697
787
  raise HTTPException(
698
- status_code=500,
699
- detail=f"Authentication error: {str(e)}"
788
+ status_code=500, detail=f"Authentication error: {str(e)}"
700
789
  )
701
-
702
- async def _verify_slack_webhook_auth_with_body(self, request: Request, body_str: str) -> None:
790
+
791
+ async def _verify_slack_webhook_auth_with_body(
792
+ self, request: Request, body_str: str
793
+ ) -> None:
703
794
  """Verify Slack HMAC signature for webhook requests using pre-read body.
704
-
795
+
705
796
  Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
706
797
  The signature is computed from the timestamp, body, and signing secret.
707
-
798
+
708
799
  Args:
709
800
  request: The FastAPI request object
710
801
  body_str: The request body as a string (already read)
711
-
802
+
712
803
  Raises:
713
804
  HTTPException: If authentication fails
714
805
  """
@@ -718,54 +809,55 @@ class TriggerServer:
718
809
  logger.error("SLACK_SIGNING_SECRET environment variable not set")
719
810
  raise HTTPException(
720
811
  status_code=500,
721
- detail="Slack signing secret not configured on server"
812
+ detail="Slack signing secret not configured on server",
722
813
  )
723
-
814
+
724
815
  headers_dict = dict(request.headers)
725
816
  signature, timestamp = extract_slack_headers(headers_dict)
726
-
817
+
727
818
  if not signature:
728
819
  logger.error("Missing X-Slack-Signature header")
729
820
  raise HTTPException(
730
821
  status_code=401,
731
- detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
822
+ detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
732
823
  )
733
-
824
+
734
825
  if not timestamp:
735
826
  logger.error("Missing X-Slack-Request-Timestamp header")
736
827
  raise HTTPException(
737
828
  status_code=401,
738
- detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
829
+ detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
739
830
  )
740
-
831
+
741
832
  try:
742
833
  verify_slack_signature(
743
834
  signing_secret=signing_secret,
744
835
  timestamp=timestamp,
745
836
  body=body_str,
746
- signature=signature
837
+ signature=signature,
838
+ )
839
+ logger.info(
840
+ f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
747
841
  )
748
- logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
749
842
  except SlackSignatureVerificationError as e:
750
843
  logger.error(f"Slack signature verification failed: {e}")
751
844
  raise HTTPException(
752
845
  status_code=401,
753
- detail=f"Slack signature verification failed: {str(e)}"
846
+ detail=f"Slack signature verification failed: {str(e)}",
754
847
  )
755
-
848
+
756
849
  # Store verification info in request state
757
850
  request.state.slack_verified = True
758
851
  request.state.slack_timestamp = timestamp
759
-
852
+
760
853
  except HTTPException:
761
854
  raise
762
855
  except Exception as e:
763
856
  logger.error(f"Unexpected error during Slack webhook authentication: {e}")
764
857
  raise HTTPException(
765
- status_code=500,
766
- detail=f"Authentication error: {str(e)}"
858
+ status_code=500, detail=f"Authentication error: {str(e)}"
767
859
  )
768
-
860
+
769
861
  def get_app(self) -> FastAPI:
770
862
  """Get the FastAPI app instance."""
771
863
  return self.app