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_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,132 @@ 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
+ )
326
+ client_metadata = payload.pop("metadata", None)
296
327
 
297
328
  # Parse payload into registration model first
298
329
  try:
299
330
  registration_instance = trigger.registration_model(**payload)
300
331
  except Exception as e:
301
332
  raise HTTPException(
302
- status_code=400,
303
- detail=f"Invalid payload for trigger: {str(e)}"
333
+ status_code=400, detail=f"Invalid payload for trigger: {str(e)}"
304
334
  )
305
-
335
+
306
336
  # Check for duplicate registration based on resource data within this user's scope
307
337
  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
338
+ existing_registration = (
339
+ await self.database.find_user_registration_by_resource(
340
+ user_id=user_id,
341
+ template_id=trigger.id,
342
+ resource_data=resource_dict,
343
+ )
312
344
  )
313
345
 
314
346
  if existing_registration:
315
347
  raise HTTPException(
316
348
  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')}"
349
+ detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}",
318
350
  )
319
- result = await trigger.registration_handler(user_id, self.langchain_auth_client, registration_instance)
320
-
351
+ result = await trigger.registration_handler(
352
+ user_id, self.langchain_auth_client, registration_instance
353
+ )
354
+
321
355
  # Check if handler requested to skip registration (e.g., for OAuth or URL verification)
322
356
  if not result.create_registration:
323
- logger.info(f"Registration handler requested to skip database creation")
324
- from fastapi import Response
357
+ logger.info(
358
+ "Registration handler requested to skip database creation"
359
+ )
325
360
  import json
361
+
362
+ from fastapi import Response
363
+
326
364
  return Response(
327
365
  content=json.dumps(result.response_body),
328
366
  status_code=result.status_code,
329
- media_type="application/json"
367
+ media_type="application/json",
330
368
  )
331
-
369
+
332
370
  resource_dict = registration_instance.model_dump()
333
371
 
372
+ merged_metadata = {}
373
+ if client_metadata:
374
+ merged_metadata["client_metadata"] = client_metadata
375
+ merged_metadata.update(result.metadata)
376
+
334
377
  registration = await self.database.create_trigger_registration(
335
378
  user_id=user_id,
336
379
  template_id=trigger.id,
337
380
  resource=resource_dict,
338
- metadata=result.metadata
381
+ metadata=merged_metadata,
339
382
  )
340
-
383
+
341
384
  if not registration:
342
- raise HTTPException(status_code=500, detail="Failed to create trigger registration")
343
-
385
+ raise HTTPException(
386
+ status_code=500, detail="Failed to create trigger registration"
387
+ )
388
+
344
389
  # Reload cron manager to pick up any new cron registrations
345
390
  await self.cron_manager.reload_from_database()
346
-
391
+
347
392
  # Return registration result
348
393
  return {
349
394
  "success": True,
350
395
  "data": registration,
351
- "metadata": result.metadata
396
+ "metadata": result.metadata,
352
397
  }
353
-
398
+
354
399
  except HTTPException:
355
400
  raise
356
401
  except Exception as e:
357
402
  logger.exception(f"Error creating trigger registration: {e}")
358
403
  raise HTTPException(status_code=500, detail=str(e))
359
-
404
+
360
405
  @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]:
406
+ async def api_list_registration_agents(
407
+ registration_id: str,
408
+ current_user: dict[str, Any] = Depends(get_current_user),
409
+ ) -> dict[str, Any]:
362
410
  """List agents linked to this registration."""
363
411
  try:
364
412
  user_id = current_user["identity"]
365
-
413
+
366
414
  # Get the specific trigger registration
367
- trigger = await self.database.get_trigger_registration(registration_id, user_id)
415
+ trigger = await self.database.get_trigger_registration(
416
+ registration_id, user_id
417
+ )
368
418
  if not trigger:
369
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
370
-
419
+ raise HTTPException(
420
+ status_code=404,
421
+ detail="Trigger registration not found or access denied",
422
+ )
423
+
371
424
  # Return the linked agent IDs
372
425
  return {
373
426
  "success": True,
374
- "data": trigger.get("linked_assistant_ids", [])
427
+ "data": trigger.get("linked_assistant_ids", []),
375
428
  }
376
-
429
+
377
430
  except HTTPException:
378
431
  raise
379
432
  except Exception as e:
380
433
  logger.error(f"Error getting registration agents: {e}")
381
434
  raise HTTPException(status_code=500, detail=str(e))
382
-
435
+
383
436
  @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]:
437
+ async def api_add_agent_to_trigger(
438
+ registration_id: str,
439
+ agent_id: str,
440
+ request: Request,
441
+ current_user: dict[str, Any] = Depends(get_current_user),
442
+ ) -> dict[str, Any]:
385
443
  """Add an agent to a trigger registration."""
386
444
  try:
387
445
  # Parse request body for field selection
@@ -390,67 +448,80 @@ class TriggerServer:
390
448
  field_selection = body.get("field_selection")
391
449
  except:
392
450
  field_selection = None
393
-
451
+
394
452
  user_id = current_user["identity"]
395
-
453
+
396
454
  # Verify the trigger registration exists and belongs to the user
397
- registration = await self.database.get_trigger_registration(registration_id, user_id)
455
+ registration = await self.database.get_trigger_registration(
456
+ registration_id, user_id
457
+ )
398
458
  if not registration:
399
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
400
-
459
+ raise HTTPException(
460
+ status_code=404,
461
+ detail="Trigger registration not found or access denied",
462
+ )
463
+
401
464
  # Link the agent to the trigger
402
465
  success = await self.database.link_agent_to_trigger(
403
466
  agent_id=agent_id,
404
467
  registration_id=registration_id,
405
468
  created_by=user_id,
406
- field_selection=field_selection
469
+ field_selection=field_selection,
407
470
  )
408
-
471
+
409
472
  if not success:
410
- raise HTTPException(status_code=500, detail="Failed to link agent to trigger")
411
-
473
+ raise HTTPException(
474
+ status_code=500, detail="Failed to link agent to trigger"
475
+ )
476
+
412
477
  return {
413
478
  "success": True,
414
479
  "message": f"Successfully linked agent {agent_id} to trigger {registration_id}",
415
- "data": {
416
- "registration_id": registration_id,
417
- "agent_id": agent_id
418
- }
480
+ "data": {"registration_id": registration_id, "agent_id": agent_id},
419
481
  }
420
-
482
+
421
483
  except HTTPException:
422
484
  raise
423
485
  except Exception as e:
424
486
  logger.error(f"Error linking agent to trigger: {e}")
425
487
  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]:
488
+
489
+ @self.app.delete(
490
+ "/v1/triggers/registrations/{registration_id}/agents/{agent_id}"
491
+ )
492
+ async def api_remove_agent_from_trigger(
493
+ registration_id: str,
494
+ agent_id: str,
495
+ current_user: dict[str, Any] = Depends(get_current_user),
496
+ ) -> dict[str, Any]:
429
497
  """Remove an agent from a trigger registration."""
430
498
  try:
431
499
  user_id = current_user["identity"]
432
500
 
433
501
  # Verify the trigger registration exists and belongs to the user
434
- registration = await self.database.get_trigger_registration(registration_id, user_id)
502
+ registration = await self.database.get_trigger_registration(
503
+ registration_id, user_id
504
+ )
435
505
  if not registration:
436
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
506
+ raise HTTPException(
507
+ status_code=404,
508
+ detail="Trigger registration not found or access denied",
509
+ )
437
510
 
438
511
  # Unlink the agent from the trigger
439
512
  success = await self.database.unlink_agent_from_trigger(
440
- agent_id=agent_id,
441
- registration_id=registration_id
513
+ agent_id=agent_id, registration_id=registration_id
442
514
  )
443
515
 
444
516
  if not success:
445
- raise HTTPException(status_code=500, detail="Failed to unlink agent from trigger")
517
+ raise HTTPException(
518
+ status_code=500, detail="Failed to unlink agent from trigger"
519
+ )
446
520
 
447
521
  return {
448
522
  "success": True,
449
523
  "message": f"Successfully unlinked agent {agent_id} from trigger {registration_id}",
450
- "data": {
451
- "registration_id": registration_id,
452
- "agent_id": agent_id
453
- }
524
+ "data": {"registration_id": registration_id, "agent_id": agent_id},
454
525
  }
455
526
 
456
527
  except HTTPException:
@@ -460,20 +531,31 @@ class TriggerServer:
460
531
  raise HTTPException(status_code=500, detail=str(e))
461
532
 
462
533
  @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]:
534
+ async def api_execute_trigger_now(
535
+ registration_id: str,
536
+ current_user: dict[str, Any] = Depends(get_current_user),
537
+ ) -> dict[str, Any]:
464
538
  """Manually execute a cron trigger registration immediately."""
465
539
  try:
466
540
  user_id = current_user["identity"]
467
541
 
468
542
  # Verify the trigger registration exists and belongs to the user
469
- registration = await self.database.get_trigger_registration(registration_id, user_id)
543
+ registration = await self.database.get_trigger_registration(
544
+ registration_id, user_id
545
+ )
470
546
  if not registration:
471
- raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
547
+ raise HTTPException(
548
+ status_code=404,
549
+ detail="Trigger registration not found or access denied",
550
+ )
472
551
 
473
552
  # Get the template to check if it's a cron trigger
474
553
  template_id = registration.get("template_id")
475
554
  if template_id != CRON_TRIGGER_ID:
476
- raise HTTPException(status_code=400, detail="Manual execution is only supported for cron triggers")
555
+ raise HTTPException(
556
+ status_code=400,
557
+ detail="Manual execution is only supported for cron triggers",
558
+ )
477
559
 
478
560
  # Execute the cron trigger using the cron manager
479
561
  agents_invoked = await self.cron_manager.execute_cron_job(registration)
@@ -481,7 +563,7 @@ class TriggerServer:
481
563
  return {
482
564
  "success": True,
483
565
  "message": f"Manually executed cron trigger {registration_id}",
484
- "agents_invoked": agents_invoked
566
+ "agents_invoked": agents_invoked,
485
567
  }
486
568
 
487
569
  except HTTPException:
@@ -489,29 +571,34 @@ class TriggerServer:
489
571
  except Exception as e:
490
572
  logger.error(f"Error executing trigger: {e}")
491
573
  raise HTTPException(status_code=500, detail=str(e))
492
-
493
-
574
+
494
575
  async def _handle_request(
495
- self,
496
- trigger: TriggerTemplate,
497
- request: Request
498
- ) -> Dict[str, Any]:
576
+ self, trigger: TriggerTemplate, request: Request
577
+ ) -> dict[str, Any]:
499
578
  """Handle an incoming request with a handler function."""
500
579
  try:
501
580
  if request.method == "POST":
502
- if request.headers.get("content-type", "").startswith("application/json"):
581
+ if request.headers.get("content-type", "").startswith(
582
+ "application/json"
583
+ ):
503
584
  # Read body once for both auth and parsing
504
585
  body_bytes = await request.body()
505
586
  body_str = body_bytes.decode("utf-8")
506
-
587
+
507
588
  if self._is_slack_trigger(trigger):
508
- await self._verify_slack_webhook_auth_with_body(request, body_str)
509
-
589
+ await self._verify_slack_webhook_auth_with_body(
590
+ request, body_str
591
+ )
592
+
510
593
  import json
594
+
511
595
  payload = json.loads(body_str)
512
596
 
513
- if payload.get("type") == "url_verification" and "challenge" in payload:
514
- logger.info(f"Responding to Slack URL verification challenge")
597
+ if (
598
+ payload.get("type") == "url_verification"
599
+ and "challenge" in payload
600
+ ):
601
+ logger.info("Responding to Slack URL verification challenge")
515
602
  return {"challenge": payload["challenge"]}
516
603
  else:
517
604
  # Handle form data or other content types
@@ -521,7 +608,9 @@ class TriggerServer:
521
608
  payload = dict(request.query_params)
522
609
 
523
610
  query_params = dict(request.query_params)
524
- result = await trigger.trigger_handler(payload, query_params, self.database, self.langchain_auth_client)
611
+ result = await trigger.trigger_handler(
612
+ payload, query_params, self.database, self.langchain_auth_client
613
+ )
525
614
  if not result.invoke_agent:
526
615
  return result.response_body
527
616
 
@@ -532,16 +621,16 @@ class TriggerServer:
532
621
  # Iterate through each message and invoke agents for each
533
622
  for message in result.agent_messages:
534
623
  for agent_link in agent_links:
535
- agent_id = agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
624
+ agent_id = (
625
+ agent_link
626
+ if isinstance(agent_link, str)
627
+ else agent_link.get("agent_id")
628
+ )
536
629
  # Ensure agent_id and user_id are strings for JSON serialization
537
630
  agent_id_str = str(agent_id)
538
631
  user_id_str = str(result.registration["user_id"])
539
632
 
540
- agent_input = {
541
- "messages": [
542
- {"role": "human", "content": message}
543
- ]
544
- }
633
+ agent_input = {"messages": [{"role": "human", "content": message}]}
545
634
 
546
635
  try:
547
636
  success = await self._invoke_agent(
@@ -552,45 +641,46 @@ class TriggerServer:
552
641
  if success:
553
642
  agents_invoked += 1
554
643
  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
-
644
+ logger.error(
645
+ f"Error invoking agent {agent_id_str}: {e}", exc_info=True
646
+ )
647
+ logger.info(
648
+ f"Processed trigger handler with {len(result.agent_messages)} messages, invoked {agents_invoked} agents"
649
+ )
650
+
651
+ return {"success": True, "agents_invoked": agents_invoked}
652
+
563
653
  except HTTPException:
564
654
  raise
565
655
  except Exception as e:
566
656
  logger.error(f"Error in trigger handler: {e}", exc_info=True)
567
657
  raise HTTPException(
568
- status_code=500,
569
- detail=f"Trigger processing failed: {str(e)}"
658
+ status_code=500, detail=f"Trigger processing failed: {str(e)}"
570
659
  )
571
-
572
-
660
+
573
661
  async def _invoke_agent(
574
662
  self,
575
663
  agent_id: str,
576
664
  user_id: str,
577
- input_data: Dict[str, Any],
665
+ input_data: dict[str, Any],
578
666
  ) -> bool:
579
667
  """Invoke LangGraph agent using the SDK."""
580
668
  # Ensure user_id is a string for JSON serialization
581
669
  user_id_str = str(user_id)
582
670
  logger.info(f"Invoking LangGraph agent {agent_id} for user {user_id_str}")
583
-
671
+
584
672
  try:
585
673
  headers = {
586
674
  "x-auth-scheme": "agent-builder-trigger",
587
675
  "x-user-id": user_id_str,
588
676
  }
589
-
677
+
590
678
  # Note: API key is already set in client initialization, no need to add to headers
591
679
  if not self.langsmith_api_key:
592
- logger.warning("No LANGSMITH_API_KEY available - authentication may fail")
593
-
680
+ logger.warning(
681
+ "No LANGSMITH_API_KEY available - authentication may fail"
682
+ )
683
+
594
684
  thread = await self.langgraph_client.threads.create(
595
685
  metadata={
596
686
  "triggered_by": "langchain-triggers",
@@ -601,7 +691,7 @@ class TriggerServer:
601
691
  logger.info(f"Created thread {thread['thread_id']} for agent {agent_id}")
602
692
 
603
693
  run = await self.langgraph_client.runs.create(
604
- thread_id=thread['thread_id'],
694
+ thread_id=thread["thread_id"],
605
695
  assistant_id=agent_id,
606
696
  input=input_data,
607
697
  metadata={
@@ -610,35 +700,39 @@ class TriggerServer:
610
700
  },
611
701
  headers=headers,
612
702
  )
613
-
614
- logger.info(f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}")
703
+
704
+ logger.info(
705
+ f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}"
706
+ )
615
707
  return True
616
-
708
+
617
709
  except Exception as e:
618
710
  # 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")
711
+ if (
712
+ hasattr(e, "response")
713
+ and getattr(e.response, "status_code", None) == 404
714
+ ):
715
+ logger.warning(
716
+ f"Agent {agent_id} not found (404) - agent may have been deleted or moved"
717
+ )
621
718
  return False
622
719
  else:
623
720
  logger.error(f"Error invoking agent {agent_id}: {e}")
624
721
  raise
625
-
722
+
626
723
  def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
627
724
  """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
-
725
+ return trigger.provider.lower() == "slack" or "slack" in trigger.id.lower()
726
+
633
727
  async def _verify_slack_webhook_auth(self, request: Request) -> None:
634
728
  """Verify Slack HMAC signature for webhook requests.
635
-
729
+
636
730
  Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
637
731
  The signature is computed from the timestamp, body, and signing secret.
638
-
732
+
639
733
  Args:
640
734
  request: The FastAPI request object
641
-
735
+
642
736
  Raises:
643
737
  HTTPException: If authentication fails
644
738
  """
@@ -648,67 +742,70 @@ class TriggerServer:
648
742
  logger.error("SLACK_SIGNING_SECRET environment variable not set")
649
743
  raise HTTPException(
650
744
  status_code=500,
651
- detail="Slack signing secret not configured on server"
745
+ detail="Slack signing secret not configured on server",
652
746
  )
653
-
747
+
654
748
  headers_dict = dict(request.headers)
655
749
  signature, timestamp = extract_slack_headers(headers_dict)
656
-
750
+
657
751
  if not signature:
658
752
  logger.error("Missing X-Slack-Signature header")
659
753
  raise HTTPException(
660
754
  status_code=401,
661
- detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
755
+ detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
662
756
  )
663
-
757
+
664
758
  if not timestamp:
665
759
  logger.error("Missing X-Slack-Request-Timestamp header")
666
760
  raise HTTPException(
667
761
  status_code=401,
668
- detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
762
+ detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
669
763
  )
670
-
764
+
671
765
  body = await request.body()
672
- body_str = body.decode('utf-8')
673
-
766
+ body_str = body.decode("utf-8")
767
+
674
768
  try:
675
769
  verify_slack_signature(
676
770
  signing_secret=signing_secret,
677
771
  timestamp=timestamp,
678
772
  body=body_str,
679
- signature=signature
773
+ signature=signature,
774
+ )
775
+ logger.info(
776
+ f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
680
777
  )
681
- logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
682
778
  except SlackSignatureVerificationError as e:
683
779
  logger.error(f"Slack signature verification failed: {e}")
684
780
  raise HTTPException(
685
781
  status_code=401,
686
- detail=f"Slack signature verification failed: {str(e)}"
782
+ detail=f"Slack signature verification failed: {str(e)}",
687
783
  )
688
-
784
+
689
785
  # Store verification info in request state
690
786
  request.state.slack_verified = True
691
787
  request.state.slack_timestamp = timestamp
692
-
788
+
693
789
  except HTTPException:
694
790
  raise
695
791
  except Exception as e:
696
792
  logger.error(f"Unexpected error during Slack webhook authentication: {e}")
697
793
  raise HTTPException(
698
- status_code=500,
699
- detail=f"Authentication error: {str(e)}"
794
+ status_code=500, detail=f"Authentication error: {str(e)}"
700
795
  )
701
-
702
- async def _verify_slack_webhook_auth_with_body(self, request: Request, body_str: str) -> None:
796
+
797
+ async def _verify_slack_webhook_auth_with_body(
798
+ self, request: Request, body_str: str
799
+ ) -> None:
703
800
  """Verify Slack HMAC signature for webhook requests using pre-read body.
704
-
801
+
705
802
  Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
706
803
  The signature is computed from the timestamp, body, and signing secret.
707
-
804
+
708
805
  Args:
709
806
  request: The FastAPI request object
710
807
  body_str: The request body as a string (already read)
711
-
808
+
712
809
  Raises:
713
810
  HTTPException: If authentication fails
714
811
  """
@@ -718,54 +815,55 @@ class TriggerServer:
718
815
  logger.error("SLACK_SIGNING_SECRET environment variable not set")
719
816
  raise HTTPException(
720
817
  status_code=500,
721
- detail="Slack signing secret not configured on server"
818
+ detail="Slack signing secret not configured on server",
722
819
  )
723
-
820
+
724
821
  headers_dict = dict(request.headers)
725
822
  signature, timestamp = extract_slack_headers(headers_dict)
726
-
823
+
727
824
  if not signature:
728
825
  logger.error("Missing X-Slack-Signature header")
729
826
  raise HTTPException(
730
827
  status_code=401,
731
- detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
828
+ detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
732
829
  )
733
-
830
+
734
831
  if not timestamp:
735
832
  logger.error("Missing X-Slack-Request-Timestamp header")
736
833
  raise HTTPException(
737
834
  status_code=401,
738
- detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
835
+ detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
739
836
  )
740
-
837
+
741
838
  try:
742
839
  verify_slack_signature(
743
840
  signing_secret=signing_secret,
744
841
  timestamp=timestamp,
745
842
  body=body_str,
746
- signature=signature
843
+ signature=signature,
844
+ )
845
+ logger.info(
846
+ f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
747
847
  )
748
- logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
749
848
  except SlackSignatureVerificationError as e:
750
849
  logger.error(f"Slack signature verification failed: {e}")
751
850
  raise HTTPException(
752
851
  status_code=401,
753
- detail=f"Slack signature verification failed: {str(e)}"
852
+ detail=f"Slack signature verification failed: {str(e)}",
754
853
  )
755
-
854
+
756
855
  # Store verification info in request state
757
856
  request.state.slack_verified = True
758
857
  request.state.slack_timestamp = timestamp
759
-
858
+
760
859
  except HTTPException:
761
860
  raise
762
861
  except Exception as e:
763
862
  logger.error(f"Unexpected error during Slack webhook authentication: {e}")
764
863
  raise HTTPException(
765
- status_code=500,
766
- detail=f"Authentication error: {str(e)}"
864
+ status_code=500, detail=f"Authentication error: {str(e)}"
767
865
  )
768
-
866
+
769
867
  def get_app(self) -> FastAPI:
770
868
  """Get the FastAPI app instance."""
771
869
  return self.app