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