langchain-trigger-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langchain-trigger-server might be problematic. Click here for more details.
- langchain_trigger_server-0.1.0.dist-info/METADATA +35 -0
- langchain_trigger_server-0.1.0.dist-info/RECORD +10 -0
- langchain_trigger_server-0.1.0.dist-info/WHEEL +4 -0
- langchain_triggers/__init__.py +17 -0
- langchain_triggers/app.py +657 -0
- langchain_triggers/core.py +83 -0
- langchain_triggers/database/__init__.py +16 -0
- langchain_triggers/database/interface.py +150 -0
- langchain_triggers/database/supabase.py +365 -0
- langchain_triggers/decorators.py +75 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"""FastAPI application for trigger server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI, HTTPException, Request, Depends
|
|
12
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
13
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
|
+
from starlette.responses import Response
|
|
15
|
+
|
|
16
|
+
from .core import UserAuthInfo, ProviderAuthInfo, MetadataManager
|
|
17
|
+
from .decorators import TriggerTemplate
|
|
18
|
+
from .database import create_database, TriggerDatabaseInterface
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
24
|
+
"""Middleware to handle authentication for API endpoints."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, app, auth_handler: Callable):
|
|
27
|
+
super().__init__(app)
|
|
28
|
+
self.auth_handler = auth_handler
|
|
29
|
+
|
|
30
|
+
async def dispatch(self, request: Request, call_next):
|
|
31
|
+
# Skip auth for webhooks, health/root endpoints, and OPTIONS requests
|
|
32
|
+
if (request.url.path.startswith("/webhooks/") or
|
|
33
|
+
request.url.path in ["/", "/health"] or
|
|
34
|
+
request.method == "OPTIONS"):
|
|
35
|
+
return await call_next(request)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# Run mandatory custom authentication
|
|
39
|
+
identity = await self.auth_handler({}, dict(request.headers))
|
|
40
|
+
|
|
41
|
+
if not identity or not identity.get("identity"):
|
|
42
|
+
return Response(
|
|
43
|
+
content='{"detail": "Authentication required"}',
|
|
44
|
+
status_code=401,
|
|
45
|
+
media_type="application/json"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Store identity in request state for endpoints to access
|
|
49
|
+
request.state.current_user = identity
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"Authentication middleware error: {e}")
|
|
53
|
+
return Response(
|
|
54
|
+
content='{"detail": "Authentication failed"}',
|
|
55
|
+
status_code=401,
|
|
56
|
+
media_type="application/json"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return await call_next(request)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_current_user(request: Request) -> Dict[str, Any]:
|
|
63
|
+
"""FastAPI dependency to get the current authenticated user."""
|
|
64
|
+
if not hasattr(request.state, "current_user"):
|
|
65
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
66
|
+
return request.state.current_user
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TriggerServer:
|
|
70
|
+
"""FastAPI application for trigger webhooks."""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
auth_handler: Callable,
|
|
75
|
+
cors_origins: Optional[List[str]] = None,
|
|
76
|
+
database: Optional[TriggerDatabaseInterface] = None,
|
|
77
|
+
):
|
|
78
|
+
self.app = FastAPI(
|
|
79
|
+
title="Triggers Server",
|
|
80
|
+
description="Event-driven triggers framework",
|
|
81
|
+
version="0.1.0"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.database = database or create_database() # Default to Supabase
|
|
85
|
+
self.auth_handler = auth_handler
|
|
86
|
+
|
|
87
|
+
# LangGraph configuration
|
|
88
|
+
self.langgraph_api_url = os.getenv("LANGGRAPH_API_URL")
|
|
89
|
+
self.langgraph_api_key = os.getenv("LANGCHAIN_API_KEY")
|
|
90
|
+
|
|
91
|
+
if not self.langgraph_api_url:
|
|
92
|
+
raise ValueError("LANGGRAPH_API_URL environment variable is required")
|
|
93
|
+
|
|
94
|
+
self.langgraph_api_url = self.langgraph_api_url.rstrip("/")
|
|
95
|
+
|
|
96
|
+
self.langchain_auth_client = None
|
|
97
|
+
try:
|
|
98
|
+
from langchain_auth import Client
|
|
99
|
+
langchain_api_key = os.getenv("LANGCHAIN_API_KEY")
|
|
100
|
+
if langchain_api_key:
|
|
101
|
+
self.langchain_auth_client = Client(api_key=langchain_api_key)
|
|
102
|
+
logger.info("Initialized LangChain Auth client for OAuth token injection")
|
|
103
|
+
else:
|
|
104
|
+
logger.warning("LANGCHAIN_API_KEY not found - OAuth token injection disabled")
|
|
105
|
+
except ImportError:
|
|
106
|
+
logger.warning("langchain_auth not installed - OAuth token injection disabled")
|
|
107
|
+
|
|
108
|
+
self.triggers: List[TriggerTemplate] = []
|
|
109
|
+
|
|
110
|
+
# Setup CORS
|
|
111
|
+
if cors_origins is None:
|
|
112
|
+
cors_origins = ["*"] # Allow all origins by default
|
|
113
|
+
|
|
114
|
+
self.app.add_middleware(
|
|
115
|
+
CORSMiddleware,
|
|
116
|
+
allow_origins=cors_origins,
|
|
117
|
+
allow_credentials=True,
|
|
118
|
+
allow_methods=["*"],
|
|
119
|
+
allow_headers=["*"],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Setup authentication middleware
|
|
123
|
+
self.app.add_middleware(AuthenticationMiddleware, auth_handler=auth_handler)
|
|
124
|
+
|
|
125
|
+
# Setup routes
|
|
126
|
+
self._setup_routes()
|
|
127
|
+
|
|
128
|
+
# Add startup event to ensure trigger templates exist in database
|
|
129
|
+
@self.app.on_event("startup")
|
|
130
|
+
async def startup_event():
|
|
131
|
+
await self.ensure_trigger_templates()
|
|
132
|
+
|
|
133
|
+
def add_trigger(self, trigger: TriggerTemplate) -> None:
|
|
134
|
+
"""Add a trigger template to the app."""
|
|
135
|
+
# Check for duplicate IDs
|
|
136
|
+
if any(t.id == trigger.id for t in self.triggers):
|
|
137
|
+
raise ValueError(f"Trigger with id '{trigger.id}' already exists")
|
|
138
|
+
|
|
139
|
+
self.triggers.append(trigger)
|
|
140
|
+
|
|
141
|
+
if trigger.trigger_handler:
|
|
142
|
+
async def handler_endpoint(request: Request) -> Dict[str, Any]:
|
|
143
|
+
return await self._handle_request(trigger, request)
|
|
144
|
+
|
|
145
|
+
handler_path = f"/webhooks/{trigger.id}"
|
|
146
|
+
self.app.post(handler_path)(handler_endpoint)
|
|
147
|
+
logger.info(f"Added handler route: POST {handler_path}")
|
|
148
|
+
|
|
149
|
+
logger.info(f"Registered trigger template in memory: {trigger.name} ({trigger.id})")
|
|
150
|
+
|
|
151
|
+
async def ensure_trigger_templates(self) -> None:
|
|
152
|
+
"""Ensure all registered trigger templates exist in the database."""
|
|
153
|
+
for trigger in self.triggers:
|
|
154
|
+
existing = await self.database.get_trigger_template(trigger.id)
|
|
155
|
+
if not existing:
|
|
156
|
+
logger.info(f"Creating new trigger template in database: {trigger.name} ({trigger.id})")
|
|
157
|
+
await self.database.create_trigger_template(
|
|
158
|
+
id=trigger.id,
|
|
159
|
+
name=trigger.name,
|
|
160
|
+
description=trigger.description,
|
|
161
|
+
registration_schema=trigger.registration_model.model_json_schema()
|
|
162
|
+
)
|
|
163
|
+
logger.info(f"✓ Successfully created trigger template: {trigger.name} ({trigger.id})")
|
|
164
|
+
else:
|
|
165
|
+
logger.info(f"✓ Trigger template already exists in database: {trigger.name} ({trigger.id})")
|
|
166
|
+
|
|
167
|
+
def add_triggers(self, triggers: List[TriggerTemplate]) -> None:
|
|
168
|
+
"""Add multiple triggers."""
|
|
169
|
+
for trigger in triggers:
|
|
170
|
+
self.add_trigger(trigger)
|
|
171
|
+
|
|
172
|
+
def _setup_routes(self) -> None:
|
|
173
|
+
"""Setup built-in API routes."""
|
|
174
|
+
|
|
175
|
+
@self.app.get("/")
|
|
176
|
+
async def root() -> Dict[str, str]:
|
|
177
|
+
return {"message": "Triggers Server", "version": "0.1.0"}
|
|
178
|
+
|
|
179
|
+
@self.app.get("/health")
|
|
180
|
+
async def health() -> Dict[str, str]:
|
|
181
|
+
return {"status": "healthy"}
|
|
182
|
+
|
|
183
|
+
@self.app.get("/api/triggers")
|
|
184
|
+
async def api_list_triggers() -> Dict[str, Any]:
|
|
185
|
+
"""List available trigger templates."""
|
|
186
|
+
templates = await self.database.get_trigger_templates()
|
|
187
|
+
trigger_list = []
|
|
188
|
+
for template in templates:
|
|
189
|
+
trigger_list.append({
|
|
190
|
+
"id": template["id"],
|
|
191
|
+
"displayName": template["name"],
|
|
192
|
+
"description": template["description"],
|
|
193
|
+
"path": "/api/triggers/registrations",
|
|
194
|
+
"method": "POST",
|
|
195
|
+
"payloadSchema": template.get("registration_schema", {}),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
"success": True,
|
|
200
|
+
"data": trigger_list
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@self.app.get("/api/triggers/registrations")
|
|
204
|
+
async def api_list_registrations(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
205
|
+
"""List user's trigger registrations (user-scoped)."""
|
|
206
|
+
try:
|
|
207
|
+
user_id = current_user["identity"]
|
|
208
|
+
|
|
209
|
+
# Get user's trigger registrations using new schema
|
|
210
|
+
user_registrations = await self.database.get_user_trigger_registrations(user_id)
|
|
211
|
+
|
|
212
|
+
# Format response to match expected structure
|
|
213
|
+
registrations = []
|
|
214
|
+
for reg in user_registrations:
|
|
215
|
+
# Get linked agent IDs
|
|
216
|
+
linked_agent_ids = await self.database.get_agents_for_trigger(reg["id"])
|
|
217
|
+
|
|
218
|
+
registrations.append({
|
|
219
|
+
"id": reg["id"],
|
|
220
|
+
"user_id": reg["user_id"],
|
|
221
|
+
"template_id": reg.get("trigger_templates", {}).get("id"),
|
|
222
|
+
"resource": reg["resource"],
|
|
223
|
+
"linked_assistant_ids": linked_agent_ids, # For backward compatibility
|
|
224
|
+
"created_at": reg["created_at"]
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"success": True,
|
|
229
|
+
"data": registrations
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
except HTTPException:
|
|
233
|
+
raise
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(f"Error listing registrations: {e}")
|
|
236
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
237
|
+
|
|
238
|
+
@self.app.post("/api/triggers/registrations")
|
|
239
|
+
async def api_create_registration(request: Request, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
240
|
+
"""Create a new trigger registration."""
|
|
241
|
+
try:
|
|
242
|
+
payload = await request.json()
|
|
243
|
+
logger.info(f"Registration payload received: {payload}")
|
|
244
|
+
|
|
245
|
+
user_id = current_user["identity"]
|
|
246
|
+
trigger_id = payload.get("type")
|
|
247
|
+
if not trigger_id:
|
|
248
|
+
raise HTTPException(status_code=400, detail="Missing required field: type")
|
|
249
|
+
|
|
250
|
+
trigger = next((t for t in self.triggers if t.id == trigger_id), None)
|
|
251
|
+
if not trigger:
|
|
252
|
+
raise HTTPException(status_code=400, detail=f"Unknown trigger type: {trigger_id}")
|
|
253
|
+
|
|
254
|
+
# Inject OAuth tokens if needed for registration
|
|
255
|
+
auth_user = None
|
|
256
|
+
if trigger.oauth_providers:
|
|
257
|
+
try:
|
|
258
|
+
auth_user = await self._get_authenticated_user(trigger, user_id)
|
|
259
|
+
|
|
260
|
+
# Check if any provider requires authentication - return early if so
|
|
261
|
+
for provider in trigger.oauth_providers.keys():
|
|
262
|
+
provider_info = auth_user.providers.get(provider)
|
|
263
|
+
if provider_info and provider_info.auth_required:
|
|
264
|
+
logger.info(f"User {user_id} needs to authenticate for {provider} - returning auth URL")
|
|
265
|
+
return {
|
|
266
|
+
"success": True,
|
|
267
|
+
"registered": False,
|
|
268
|
+
"auth_required": True,
|
|
269
|
+
"auth_url": provider_info.auth_url,
|
|
270
|
+
"auth_id": provider_info.auth_id,
|
|
271
|
+
"provider": provider
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"OAuth authentication failed during registration: {e}")
|
|
276
|
+
raise HTTPException(status_code=500, detail="OAuth authentication failed")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Parse payload into registration model first
|
|
280
|
+
try:
|
|
281
|
+
registration_instance = trigger.registration_model(**payload)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
raise HTTPException(
|
|
284
|
+
status_code=400,
|
|
285
|
+
detail=f"Invalid payload for trigger {trigger_type}: {str(e)}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Call the trigger's registration handler with parsed registration model
|
|
289
|
+
result = await trigger.registration_handler(registration_instance, auth_user)
|
|
290
|
+
|
|
291
|
+
resource_dict = registration_instance.model_dump()
|
|
292
|
+
registration = await self.database.create_trigger_registration(
|
|
293
|
+
user_id=user_id,
|
|
294
|
+
template_id=trigger.id,
|
|
295
|
+
resource=resource_dict,
|
|
296
|
+
metadata=result.metadata
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if not registration:
|
|
300
|
+
raise HTTPException(status_code=500, detail="Failed to create trigger registration")
|
|
301
|
+
|
|
302
|
+
# Return registration result
|
|
303
|
+
return {
|
|
304
|
+
"success": True,
|
|
305
|
+
"data": registration,
|
|
306
|
+
"metadata": result.metadata
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
except HTTPException:
|
|
310
|
+
raise
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Error creating trigger registration: {e}")
|
|
313
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
314
|
+
|
|
315
|
+
@self.app.get("/api/triggers/registrations/{registration_id}/agents")
|
|
316
|
+
async def api_list_registration_agents(registration_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
317
|
+
"""List agents linked to this registration."""
|
|
318
|
+
try:
|
|
319
|
+
user_id = current_user["identity"]
|
|
320
|
+
|
|
321
|
+
# Get the specific trigger registration
|
|
322
|
+
trigger = await self.database.get_user_trigger(user_id, registration_id, token)
|
|
323
|
+
if not trigger:
|
|
324
|
+
raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
|
|
325
|
+
|
|
326
|
+
# Return the linked agent IDs
|
|
327
|
+
return {
|
|
328
|
+
"success": True,
|
|
329
|
+
"data": trigger.get("linked_assistant_ids", [])
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
except HTTPException:
|
|
333
|
+
raise
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Error getting registration agents: {e}")
|
|
336
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
337
|
+
|
|
338
|
+
@self.app.post("/api/triggers/registrations/{registration_id}/agents/{agent_id}")
|
|
339
|
+
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]:
|
|
340
|
+
"""Add an agent to a trigger registration."""
|
|
341
|
+
try:
|
|
342
|
+
# Parse request body for field selection
|
|
343
|
+
try:
|
|
344
|
+
body = await request.json()
|
|
345
|
+
field_selection = body.get("field_selection")
|
|
346
|
+
except:
|
|
347
|
+
field_selection = None
|
|
348
|
+
|
|
349
|
+
user_id = current_user["identity"]
|
|
350
|
+
|
|
351
|
+
# Verify the trigger registration exists and belongs to the user
|
|
352
|
+
registration = await self.database.get_trigger_registration(registration_id, user_id)
|
|
353
|
+
if not registration:
|
|
354
|
+
raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
|
|
355
|
+
|
|
356
|
+
# Link the agent to the trigger
|
|
357
|
+
success = await self.database.link_agent_to_trigger(
|
|
358
|
+
agent_id=agent_id,
|
|
359
|
+
registration_id=registration_id,
|
|
360
|
+
created_by=user_id,
|
|
361
|
+
field_selection=field_selection
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if not success:
|
|
365
|
+
raise HTTPException(status_code=500, detail="Failed to link agent to trigger")
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
"success": True,
|
|
369
|
+
"message": f"Successfully linked agent {agent_id} to trigger {registration_id}",
|
|
370
|
+
"data": {
|
|
371
|
+
"registration_id": registration_id,
|
|
372
|
+
"agent_id": agent_id
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
except HTTPException:
|
|
377
|
+
raise
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error(f"Error linking agent to trigger: {e}")
|
|
380
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
381
|
+
|
|
382
|
+
@self.app.delete("/api/triggers/registrations/{registration_id}/agents/{agent_id}")
|
|
383
|
+
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]:
|
|
384
|
+
"""Remove an agent from a trigger registration."""
|
|
385
|
+
try:
|
|
386
|
+
user_id = current_user["identity"]
|
|
387
|
+
|
|
388
|
+
# Verify the trigger registration exists and belongs to the user
|
|
389
|
+
registration = await self.database.get_trigger_registration(registration_id, user_id)
|
|
390
|
+
if not registration:
|
|
391
|
+
raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
|
|
392
|
+
|
|
393
|
+
# Unlink the agent from the trigger
|
|
394
|
+
success = await self.database.unlink_agent_from_trigger(
|
|
395
|
+
agent_id=agent_id,
|
|
396
|
+
registration_id=registration_id
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if not success:
|
|
400
|
+
raise HTTPException(status_code=500, detail="Failed to unlink agent from trigger")
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
"success": True,
|
|
404
|
+
"message": f"Successfully unlinked agent {agent_id} from trigger {registration_id}",
|
|
405
|
+
"data": {
|
|
406
|
+
"registration_id": registration_id,
|
|
407
|
+
"agent_id": agent_id
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
except HTTPException:
|
|
412
|
+
raise
|
|
413
|
+
except Exception as e:
|
|
414
|
+
logger.error(f"Error unlinking agent from trigger: {e}")
|
|
415
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
416
|
+
|
|
417
|
+
@self.app.get("/events/subscriptions")
|
|
418
|
+
async def list_event_subscriptions() -> Dict[str, Any]:
|
|
419
|
+
"""List event bus subscriptions."""
|
|
420
|
+
if hasattr(self.event_bus, "list_subscriptions"):
|
|
421
|
+
subscriptions = self.event_bus.list_subscriptions()
|
|
422
|
+
else:
|
|
423
|
+
subscriptions = {}
|
|
424
|
+
|
|
425
|
+
return {"subscriptions": subscriptions}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def _handle_request(
|
|
429
|
+
self,
|
|
430
|
+
trigger: TriggerTemplate,
|
|
431
|
+
request: Request
|
|
432
|
+
) -> Dict[str, Any]:
|
|
433
|
+
"""Handle an incoming request with a handler function."""
|
|
434
|
+
try:
|
|
435
|
+
# Parse request data
|
|
436
|
+
if request.method == "POST":
|
|
437
|
+
if request.headers.get("content-type", "").startswith("application/json"):
|
|
438
|
+
payload = await request.json()
|
|
439
|
+
else:
|
|
440
|
+
# Handle form data or other content types
|
|
441
|
+
body = await request.body()
|
|
442
|
+
payload = {"raw_body": body.decode("utf-8") if body else ""}
|
|
443
|
+
else:
|
|
444
|
+
payload = dict(request.query_params)
|
|
445
|
+
|
|
446
|
+
# Step 1: Registration resolution
|
|
447
|
+
if not trigger.registration_resolver:
|
|
448
|
+
raise HTTPException(
|
|
449
|
+
status_code=500,
|
|
450
|
+
detail=f"Trigger {trigger.id} missing required registration_resolver"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Extract resource identifiers from webhook payload
|
|
454
|
+
resource_data = await trigger.registration_resolver(payload)
|
|
455
|
+
|
|
456
|
+
# Find matching registration
|
|
457
|
+
# Convert Pydantic model to dict for database lookup
|
|
458
|
+
resource_dict = resource_data.model_dump()
|
|
459
|
+
registration = await self.database.find_registration_by_resource(
|
|
460
|
+
trigger.id,
|
|
461
|
+
resource_dict
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if not registration:
|
|
465
|
+
logger.warning(f"No registration found for trigger_id={trigger.id} with resource={resource_data}, returning 400")
|
|
466
|
+
raise HTTPException(
|
|
467
|
+
status_code=400,
|
|
468
|
+
detail=f"No registration found for {trigger.id} with resource {resource_data}"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Step 2: Get user_id from registration (webhooks don't use API auth)
|
|
472
|
+
user_id = registration["user_id"]
|
|
473
|
+
|
|
474
|
+
# Step 3: Inject OAuth tokens if needed
|
|
475
|
+
auth_user = None
|
|
476
|
+
if trigger.oauth_providers and self.langchain_auth_client:
|
|
477
|
+
try:
|
|
478
|
+
auth_user = await self._get_authenticated_user(trigger, user_id)
|
|
479
|
+
|
|
480
|
+
# Check if any provider requires re-authentication - this shouldn't happen in webhooks
|
|
481
|
+
for provider in trigger.oauth_providers.keys():
|
|
482
|
+
provider_info = auth_user.providers.get(provider)
|
|
483
|
+
if provider_info and provider_info.auth_required:
|
|
484
|
+
logger.error(f"User {user_id} needs to re-authenticate for {provider} - this should have been handled during registration")
|
|
485
|
+
return {
|
|
486
|
+
"success": False,
|
|
487
|
+
"error": f"Authentication required for {provider}",
|
|
488
|
+
"message": "User needs to re-authenticate this trigger"
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(f"OAuth authentication failed: {e}")
|
|
493
|
+
# Continue without auth - triggers can handle missing tokens
|
|
494
|
+
|
|
495
|
+
# Step 4: Create metadata manager
|
|
496
|
+
metadata_manager = MetadataManager(
|
|
497
|
+
database=self.database,
|
|
498
|
+
registration_id=registration["id"],
|
|
499
|
+
initial_metadata=registration.get("metadata", {})
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Step 5: Call handler with parsed registration data
|
|
503
|
+
result = await trigger.trigger_handler(payload, resource_data, auth_user, metadata_manager)
|
|
504
|
+
registration_id = registration["id"]
|
|
505
|
+
|
|
506
|
+
# Check if we should invoke agents
|
|
507
|
+
if not result.invoke_agent:
|
|
508
|
+
logger.info(f"Handler requested no agent invocation for registration {registration_id}")
|
|
509
|
+
return {
|
|
510
|
+
"success": True,
|
|
511
|
+
"agents_invoked": 0
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Get agents linked to this trigger registration
|
|
515
|
+
agent_links = await self.database.get_agents_for_trigger(registration_id)
|
|
516
|
+
|
|
517
|
+
if not agent_links:
|
|
518
|
+
logger.info(f"No agents linked to registration {registration_id}")
|
|
519
|
+
return {
|
|
520
|
+
"success": True,
|
|
521
|
+
"agents_invoked": 0
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
logger.info(f"Processing trigger result for registration {registration_id} with {len(agent_links)} linked agents")
|
|
525
|
+
|
|
526
|
+
# Invoke each linked agent
|
|
527
|
+
agents_invoked = 0
|
|
528
|
+
for agent_link in agent_links:
|
|
529
|
+
agent_id = agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
|
|
530
|
+
|
|
531
|
+
# Use the data string from TriggerHandlerResult directly
|
|
532
|
+
agent_input = {
|
|
533
|
+
"messages": [
|
|
534
|
+
{"role": "human", "content": result.data}
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
await self._invoke_agent(
|
|
540
|
+
agent_id=agent_id,
|
|
541
|
+
user_id=registration["user_id"],
|
|
542
|
+
input_data=agent_input,
|
|
543
|
+
)
|
|
544
|
+
agents_invoked += 1
|
|
545
|
+
except Exception as e:
|
|
546
|
+
logger.error(f"Error invoking agent {agent_id}: {e}", exc_info=True)
|
|
547
|
+
|
|
548
|
+
logger.info(f"Processed trigger handler, invoked {agents_invoked} agents")
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
"success": True,
|
|
552
|
+
"agents_invoked": agents_invoked
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
except HTTPException:
|
|
556
|
+
raise
|
|
557
|
+
except Exception as e:
|
|
558
|
+
logger.error(f"Error in trigger handler: {e}", exc_info=True)
|
|
559
|
+
raise HTTPException(
|
|
560
|
+
status_code=500,
|
|
561
|
+
detail=f"Trigger processing failed: {str(e)}"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
async def _invoke_agent(
|
|
566
|
+
self,
|
|
567
|
+
agent_id: str,
|
|
568
|
+
user_id: str,
|
|
569
|
+
input_data: Dict[str, Any],
|
|
570
|
+
) -> Dict[str, Any]:
|
|
571
|
+
"""Invoke LangGraph agent directly."""
|
|
572
|
+
headers = {
|
|
573
|
+
"Content-Type": "application/json",
|
|
574
|
+
"x-auth-scheme": "langchain-trigger-server-event",
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# Add API key if provided
|
|
578
|
+
if self.langgraph_api_key:
|
|
579
|
+
headers["Authorization"] = f"Bearer {self.langgraph_api_key}"
|
|
580
|
+
|
|
581
|
+
# Add user-specific headers
|
|
582
|
+
if user_id:
|
|
583
|
+
headers["x-supabase-user-id"] = user_id
|
|
584
|
+
|
|
585
|
+
payload = {
|
|
586
|
+
"input": input_data,
|
|
587
|
+
"assistant_id": agent_id,
|
|
588
|
+
"metadata": {
|
|
589
|
+
"triggered_by": "langchain-triggers",
|
|
590
|
+
"user_id": user_id,
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
# Let LangGraph create a new thread automatically
|
|
595
|
+
url = f"{self.langgraph_api_url}/runs"
|
|
596
|
+
|
|
597
|
+
logger.info(f"Invoking LangGraph agent {agent_id} for user {user_id}")
|
|
598
|
+
|
|
599
|
+
async with httpx.AsyncClient() as client:
|
|
600
|
+
try:
|
|
601
|
+
response = await client.post(
|
|
602
|
+
url,
|
|
603
|
+
json=payload,
|
|
604
|
+
headers=headers,
|
|
605
|
+
timeout=30.0,
|
|
606
|
+
)
|
|
607
|
+
response.raise_for_status()
|
|
608
|
+
|
|
609
|
+
result = response.json()
|
|
610
|
+
logger.info(f"Successfully invoked agent {agent_id}")
|
|
611
|
+
return result
|
|
612
|
+
|
|
613
|
+
except httpx.HTTPStatusError as e:
|
|
614
|
+
logger.error(f"HTTP error invoking agent: {e.response.status_code} - {e.response.text}")
|
|
615
|
+
raise
|
|
616
|
+
except Exception as e:
|
|
617
|
+
logger.error(f"Error invoking agent {agent_id}: {e}")
|
|
618
|
+
raise
|
|
619
|
+
|
|
620
|
+
async def _get_authenticated_user(self, trigger: TriggerTemplate, user_id: str) -> UserAuthInfo:
|
|
621
|
+
"""Get authenticated user with OAuth tokens for the trigger's required providers."""
|
|
622
|
+
providers = {}
|
|
623
|
+
|
|
624
|
+
# Get tokens for each required OAuth provider
|
|
625
|
+
for provider, scopes in trigger.oauth_providers.items():
|
|
626
|
+
try:
|
|
627
|
+
auth_result = await self.langchain_auth_client.authenticate(
|
|
628
|
+
provider=provider,
|
|
629
|
+
scopes=scopes,
|
|
630
|
+
user_id=user_id
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Debug logging
|
|
634
|
+
logger.info(f"Auth result for {provider}: {vars(auth_result) if hasattr(auth_result, '__dict__') else 'Not available'}")
|
|
635
|
+
|
|
636
|
+
if hasattr(auth_result, 'token') and auth_result.token:
|
|
637
|
+
providers[provider] = ProviderAuthInfo(token=auth_result.token)
|
|
638
|
+
logger.debug(f"Successfully got {provider} token for user {user_id}")
|
|
639
|
+
elif hasattr(auth_result, 'auth_required') and auth_result.auth_required:
|
|
640
|
+
logger.info(f"User {user_id} needs to authenticate for {provider}")
|
|
641
|
+
providers[provider] = ProviderAuthInfo(
|
|
642
|
+
auth_required=True,
|
|
643
|
+
auth_url=getattr(auth_result, 'auth_url', None),
|
|
644
|
+
auth_id=getattr(auth_result, 'auth_id', None)
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
logger.warning(f"No token returned for {provider} provider")
|
|
648
|
+
|
|
649
|
+
except Exception as e:
|
|
650
|
+
logger.error(f"Failed to get {provider} token: {e}")
|
|
651
|
+
# Continue with other providers
|
|
652
|
+
|
|
653
|
+
return UserAuthInfo(user_id=user_id, providers=providers)
|
|
654
|
+
|
|
655
|
+
def get_app(self) -> FastAPI:
|
|
656
|
+
"""Get the FastAPI app instance."""
|
|
657
|
+
return self.app
|