langchain-trigger-server 0.1.8__tar.gz → 0.1.9__tar.gz
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.8 → langchain_trigger_server-0.1.9}/PKG-INFO +1 -1
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/__init__.py +1 -2
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/app.py +40 -197
- langchain_trigger_server-0.1.9/langchain_triggers/core.py +78 -0
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/database/interface.py +7 -7
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/database/supabase.py +17 -33
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/decorators.py +7 -13
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/pyproject.toml +1 -1
- langchain_trigger_server-0.1.8/langchain_triggers/core.py +0 -83
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/.github/workflows/release.yml +0 -0
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/README.md +0 -0
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/database/__init__.py +0 -0
- {langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/test_framework.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-trigger-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Generic event-driven triggers framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/langchain-ai/open-agent-platform
|
|
6
6
|
Project-URL: Repository, https://github.com/langchain-ai/open-agent-platform
|
{langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/__init__.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""LangChain Triggers Framework - Event-driven triggers for AI agents."""
|
|
2
2
|
|
|
3
|
-
from .core import UserAuthInfo, TriggerRegistrationModel, TriggerHandlerResult, TriggerRegistrationResult
|
|
3
|
+
from .core import UserAuthInfo, TriggerRegistrationModel, TriggerHandlerResult, TriggerRegistrationResult
|
|
4
4
|
from .decorators import TriggerTemplate
|
|
5
5
|
from .app import TriggerServer
|
|
6
6
|
|
|
@@ -11,7 +11,6 @@ __all__ = [
|
|
|
11
11
|
"TriggerRegistrationModel",
|
|
12
12
|
"TriggerHandlerResult",
|
|
13
13
|
"TriggerRegistrationResult",
|
|
14
|
-
"MetadataManager",
|
|
15
14
|
"TriggerTemplate",
|
|
16
15
|
"TriggerServer",
|
|
17
16
|
]
|
|
@@ -6,14 +6,12 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
from typing import Any, Callable, Dict, List, Optional
|
|
8
8
|
|
|
9
|
-
import httpx
|
|
10
|
-
|
|
11
9
|
from fastapi import FastAPI, HTTPException, Request, Depends
|
|
12
10
|
from langgraph_sdk import get_client
|
|
11
|
+
from langchain_auth.client import Client
|
|
13
12
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
13
|
from starlette.responses import Response
|
|
15
14
|
|
|
16
|
-
from .core import UserAuthInfo, ProviderAuthInfo, MetadataManager
|
|
17
15
|
from .decorators import TriggerTemplate
|
|
18
16
|
from .database import create_database, TriggerDatabaseInterface
|
|
19
17
|
|
|
@@ -72,7 +70,6 @@ class TriggerServer:
|
|
|
72
70
|
def __init__(
|
|
73
71
|
self,
|
|
74
72
|
auth_handler: Callable,
|
|
75
|
-
langgraph_headers_builder: Callable,
|
|
76
73
|
):
|
|
77
74
|
self.app = FastAPI(
|
|
78
75
|
title="Triggers Server",
|
|
@@ -82,11 +79,10 @@ class TriggerServer:
|
|
|
82
79
|
|
|
83
80
|
self.database = create_database()
|
|
84
81
|
self.auth_handler = auth_handler
|
|
85
|
-
self.langgraph_headers_builder = langgraph_headers_builder
|
|
86
82
|
|
|
87
83
|
# LangGraph configuration
|
|
88
84
|
self.langgraph_api_url = os.getenv("LANGGRAPH_API_URL")
|
|
89
|
-
self.
|
|
85
|
+
self.langsmith_api_key = os.getenv("LANGSMITH_API_KEY")
|
|
90
86
|
|
|
91
87
|
if not self.langgraph_api_url:
|
|
92
88
|
raise ValueError("LANGGRAPH_API_URL environment variable is required")
|
|
@@ -94,19 +90,16 @@ class TriggerServer:
|
|
|
94
90
|
self.langgraph_api_url = self.langgraph_api_url.rstrip("/")
|
|
95
91
|
|
|
96
92
|
# Initialize LangGraph SDK client
|
|
97
|
-
self.langgraph_client = get_client(url=self.langgraph_api_url)
|
|
93
|
+
self.langgraph_client = get_client(url=self.langgraph_api_url, api_key=self.langsmith_api_key)
|
|
98
94
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
logger.warning("LANGCHAIN_API_KEY not found - OAuth token injection disabled")
|
|
108
|
-
except ImportError:
|
|
109
|
-
logger.warning("langchain_auth not installed - OAuth token injection disabled")
|
|
95
|
+
# Initialize LangChain auth client
|
|
96
|
+
langchain_api_key = os.getenv("LANGCHAIN_API_KEY")
|
|
97
|
+
if langchain_api_key:
|
|
98
|
+
self.langchain_auth_client = Client(api_key=langchain_api_key)
|
|
99
|
+
logger.info("✓ LangChain auth client initialized")
|
|
100
|
+
else:
|
|
101
|
+
self.langchain_auth_client = None
|
|
102
|
+
logger.warning("LANGCHAIN_API_KEY not found - OAuth token injection disabled")
|
|
110
103
|
|
|
111
104
|
self.triggers: List[TriggerTemplate] = []
|
|
112
105
|
|
|
@@ -255,43 +248,31 @@ class TriggerServer:
|
|
|
255
248
|
resource_dict = registration_instance.model_dump()
|
|
256
249
|
existing_registration = await self.database.find_registration_by_resource(
|
|
257
250
|
template_id=trigger.id,
|
|
258
|
-
resource_data=resource_dict
|
|
259
|
-
user_id=user_id
|
|
251
|
+
resource_data=resource_dict
|
|
260
252
|
)
|
|
261
253
|
|
|
254
|
+
# TODO(sam) figure out how to allow duplicates across users.....very unnatural constraint to have
|
|
262
255
|
if existing_registration:
|
|
263
256
|
raise HTTPException(
|
|
264
257
|
status_code=400,
|
|
265
258
|
detail=f"A registration with this configuration already exists for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}"
|
|
266
259
|
)
|
|
267
260
|
|
|
268
|
-
# Inject OAuth tokens if needed for registration
|
|
269
|
-
auth_user = None
|
|
270
|
-
if trigger.oauth_providers:
|
|
271
|
-
try:
|
|
272
|
-
auth_user = await self._get_authenticated_user(trigger, user_id)
|
|
273
|
-
|
|
274
|
-
# Check if any provider requires authentication - return early if so
|
|
275
|
-
for provider in trigger.oauth_providers.keys():
|
|
276
|
-
provider_info = auth_user.providers.get(provider)
|
|
277
|
-
if provider_info and provider_info.auth_required:
|
|
278
|
-
logger.info(f"User {user_id} needs to authenticate for {provider} - returning auth URL")
|
|
279
|
-
return {
|
|
280
|
-
"success": True,
|
|
281
|
-
"registered": False,
|
|
282
|
-
"auth_required": True,
|
|
283
|
-
"auth_url": provider_info.auth_url,
|
|
284
|
-
"auth_id": provider_info.auth_id,
|
|
285
|
-
"provider": provider
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
except Exception as e:
|
|
289
|
-
logger.error(f"OAuth authentication failed during registration: {e}")
|
|
290
|
-
raise HTTPException(status_code=500, detail="OAuth authentication failed")
|
|
291
|
-
|
|
292
261
|
|
|
293
262
|
# Call the trigger's registration handler with parsed registration model
|
|
294
|
-
result = await trigger.registration_handler(
|
|
263
|
+
result = await trigger.registration_handler(user_id, self.langchain_auth_client, registration_instance)
|
|
264
|
+
|
|
265
|
+
# Check if handler requested to skip registration (e.g., for OAuth or URL verification)
|
|
266
|
+
if not result.create_registration:
|
|
267
|
+
logger.info(f"Registration handler requested to skip database creation")
|
|
268
|
+
from fastapi import Response
|
|
269
|
+
import json
|
|
270
|
+
return Response(
|
|
271
|
+
content=json.dumps(result.response_body),
|
|
272
|
+
status_code=result.status_code,
|
|
273
|
+
media_type="application/json"
|
|
274
|
+
)
|
|
275
|
+
|
|
295
276
|
resource_dict = registration_instance.model_dump()
|
|
296
277
|
|
|
297
278
|
registration = await self.database.create_trigger_registration(
|
|
@@ -314,7 +295,7 @@ class TriggerServer:
|
|
|
314
295
|
except HTTPException:
|
|
315
296
|
raise
|
|
316
297
|
except Exception as e:
|
|
317
|
-
logger.
|
|
298
|
+
logger.exception(f"Error creating trigger registration: {e}")
|
|
318
299
|
raise HTTPException(status_code=500, detail=str(e))
|
|
319
300
|
|
|
320
301
|
@self.app.get("/api/triggers/registrations/{registration_id}/agents")
|
|
@@ -418,16 +399,6 @@ class TriggerServer:
|
|
|
418
399
|
except Exception as e:
|
|
419
400
|
logger.error(f"Error unlinking agent from trigger: {e}")
|
|
420
401
|
raise HTTPException(status_code=500, detail=str(e))
|
|
421
|
-
|
|
422
|
-
@self.app.get("/events/subscriptions")
|
|
423
|
-
async def list_event_subscriptions() -> Dict[str, Any]:
|
|
424
|
-
"""List event bus subscriptions."""
|
|
425
|
-
if hasattr(self.event_bus, "list_subscriptions"):
|
|
426
|
-
subscriptions = self.event_bus.list_subscriptions()
|
|
427
|
-
else:
|
|
428
|
-
subscriptions = {}
|
|
429
|
-
|
|
430
|
-
return {"subscriptions": subscriptions}
|
|
431
402
|
|
|
432
403
|
|
|
433
404
|
async def _handle_request(
|
|
@@ -437,24 +408,6 @@ class TriggerServer:
|
|
|
437
408
|
) -> Dict[str, Any]:
|
|
438
409
|
"""Handle an incoming request with a handler function."""
|
|
439
410
|
try:
|
|
440
|
-
# Step 1: API Key Authentication (required for webhooks)
|
|
441
|
-
# Check for API key in header first, then query params (for Pub/Sub compatibility)
|
|
442
|
-
api_key = request.headers.get("x-api-key") or request.query_params.get("api_key")
|
|
443
|
-
if not api_key:
|
|
444
|
-
logger.warning("Webhook request missing x-api-key header or api_key query parameter")
|
|
445
|
-
raise HTTPException(
|
|
446
|
-
status_code=401,
|
|
447
|
-
detail="Missing x-api-key header or api_key query parameter"
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
# Validate API key and get user_id
|
|
451
|
-
user_id = await self.database.validate_api_key(api_key)
|
|
452
|
-
if not user_id:
|
|
453
|
-
logger.warning("Invalid API key provided to webhook")
|
|
454
|
-
raise HTTPException(
|
|
455
|
-
status_code=401,
|
|
456
|
-
detail="Invalid API key"
|
|
457
|
-
)
|
|
458
411
|
|
|
459
412
|
# Parse request data
|
|
460
413
|
if request.method == "POST":
|
|
@@ -466,108 +419,35 @@ class TriggerServer:
|
|
|
466
419
|
payload = {"raw_body": body.decode("utf-8") if body else ""}
|
|
467
420
|
else:
|
|
468
421
|
payload = dict(request.query_params)
|
|
469
|
-
|
|
470
|
-
# Step 2: Registration resolution
|
|
471
|
-
if not trigger.registration_resolver:
|
|
472
|
-
raise HTTPException(
|
|
473
|
-
status_code=500,
|
|
474
|
-
detail=f"Trigger {trigger.id} missing required registration_resolver"
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
# Extract resource identifiers using resolver (gets both query params and payload)
|
|
478
|
-
resource_data = await trigger.registration_resolver(payload, dict(request.query_params))
|
|
479
|
-
|
|
480
|
-
# Find matching registration for the authenticated user
|
|
481
|
-
# Convert Pydantic model to dict for database lookup
|
|
482
|
-
resource_dict = resource_data.model_dump()
|
|
483
|
-
registration = await self.database.find_registration_by_resource(
|
|
484
|
-
trigger.id,
|
|
485
|
-
resource_dict,
|
|
486
|
-
user_id
|
|
487
|
-
)
|
|
488
422
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
raise HTTPException(
|
|
492
|
-
status_code=400,
|
|
493
|
-
detail=f"No registration found for {trigger.id} with resource {resource_dict}"
|
|
494
|
-
)
|
|
495
|
-
|
|
496
|
-
# Step 3: Inject OAuth tokens if needed
|
|
497
|
-
auth_user = None
|
|
498
|
-
if trigger.oauth_providers and self.langchain_auth_client:
|
|
499
|
-
try:
|
|
500
|
-
auth_user = await self._get_authenticated_user(trigger, user_id)
|
|
501
|
-
|
|
502
|
-
# Check if any provider requires re-authentication - this shouldn't happen in webhooks
|
|
503
|
-
for provider in trigger.oauth_providers.keys():
|
|
504
|
-
provider_info = auth_user.providers.get(provider)
|
|
505
|
-
if provider_info and provider_info.auth_required:
|
|
506
|
-
logger.error(f"User {user_id} needs to re-authenticate for {provider} - this should have been handled during registration")
|
|
507
|
-
return {
|
|
508
|
-
"success": False,
|
|
509
|
-
"error": f"Authentication required for {provider}",
|
|
510
|
-
"message": "User needs to re-authenticate this trigger"
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
except Exception as e:
|
|
514
|
-
logger.error(f"OAuth authentication failed: {e}")
|
|
515
|
-
# Continue without auth - triggers can handle missing tokens
|
|
516
|
-
|
|
517
|
-
# Step 4: Create metadata manager
|
|
518
|
-
metadata_manager = MetadataManager(
|
|
519
|
-
database=self.database,
|
|
520
|
-
registration_id=registration["id"],
|
|
521
|
-
initial_metadata=registration.get("metadata", {})
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# Step 5: Call handler with parsed registration data
|
|
525
|
-
result = await trigger.trigger_handler(payload, auth_user, metadata_manager)
|
|
526
|
-
registration_id = registration["id"]
|
|
527
|
-
|
|
528
|
-
# Check if we should invoke agents
|
|
423
|
+
query_params = dict(request.query_params)
|
|
424
|
+
result = await trigger.trigger_handler(payload, query_params, self.database, self.langchain_auth_client)
|
|
529
425
|
if not result.invoke_agent:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"agents_invoked": 0
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
# Get agents linked to this trigger registration
|
|
426
|
+
return result.response_body
|
|
427
|
+
|
|
428
|
+
registration_id = result.registration["id"]
|
|
537
429
|
agent_links = await self.database.get_agents_for_trigger(registration_id)
|
|
538
|
-
|
|
539
|
-
if not agent_links:
|
|
540
|
-
logger.info(f"No agents linked to registration {registration_id}")
|
|
541
|
-
return {
|
|
542
|
-
"success": True,
|
|
543
|
-
"agents_invoked": 0
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
logger.info(f"Processing trigger result for registration {registration_id} with {len(agent_links)} linked agents")
|
|
547
|
-
|
|
548
|
-
# Invoke each linked agent
|
|
430
|
+
|
|
549
431
|
agents_invoked = 0
|
|
550
432
|
for agent_link in agent_links:
|
|
551
433
|
agent_id = agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
|
|
552
|
-
|
|
553
|
-
# Use the data string from TriggerHandlerResult directly
|
|
434
|
+
|
|
554
435
|
agent_input = {
|
|
555
436
|
"messages": [
|
|
556
|
-
{"role": "human", "content": result.
|
|
437
|
+
{"role": "human", "content": result.agent_message}
|
|
557
438
|
]
|
|
558
439
|
}
|
|
559
440
|
|
|
560
441
|
try:
|
|
561
442
|
success = await self._invoke_agent(
|
|
562
443
|
agent_id=agent_id,
|
|
563
|
-
user_id=registration["user_id"],
|
|
444
|
+
user_id=result.registration["user_id"],
|
|
564
445
|
input_data=agent_input,
|
|
565
446
|
)
|
|
566
447
|
if success:
|
|
567
448
|
agents_invoked += 1
|
|
568
449
|
except Exception as e:
|
|
569
450
|
logger.error(f"Error invoking agent {agent_id}: {e}", exc_info=True)
|
|
570
|
-
|
|
571
451
|
logger.info(f"Processed trigger handler, invoked {agents_invoked} agents")
|
|
572
452
|
|
|
573
453
|
return {
|
|
@@ -595,12 +475,10 @@ class TriggerServer:
|
|
|
595
475
|
logger.info(f"Invoking LangGraph agent {agent_id} for user {user_id}")
|
|
596
476
|
|
|
597
477
|
try:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
user_id
|
|
601
|
-
|
|
602
|
-
agent_id=agent_id
|
|
603
|
-
)
|
|
478
|
+
headers = {
|
|
479
|
+
"x-auth-scheme": "oap-trigger",
|
|
480
|
+
"x-supabase-user-id": user_id,
|
|
481
|
+
}
|
|
604
482
|
|
|
605
483
|
thread = await self.langgraph_client.threads.create(
|
|
606
484
|
metadata={
|
|
@@ -634,41 +512,6 @@ class TriggerServer:
|
|
|
634
512
|
logger.error(f"Error invoking agent {agent_id}: {e}")
|
|
635
513
|
raise
|
|
636
514
|
|
|
637
|
-
async def _get_authenticated_user(self, trigger: TriggerTemplate, user_id: str) -> UserAuthInfo:
|
|
638
|
-
"""Get authenticated user with OAuth tokens for the trigger's required providers."""
|
|
639
|
-
providers = {}
|
|
640
|
-
|
|
641
|
-
# Get tokens for each required OAuth provider
|
|
642
|
-
for provider, scopes in trigger.oauth_providers.items():
|
|
643
|
-
try:
|
|
644
|
-
auth_result = await self.langchain_auth_client.authenticate(
|
|
645
|
-
provider=provider,
|
|
646
|
-
scopes=scopes,
|
|
647
|
-
user_id=user_id
|
|
648
|
-
)
|
|
649
|
-
|
|
650
|
-
# Debug logging
|
|
651
|
-
logger.info(f"Auth result for {provider}: {vars(auth_result) if hasattr(auth_result, '__dict__') else 'Not available'}")
|
|
652
|
-
|
|
653
|
-
if hasattr(auth_result, 'token') and auth_result.token:
|
|
654
|
-
providers[provider] = ProviderAuthInfo(token=auth_result.token)
|
|
655
|
-
logger.debug(f"Successfully got {provider} token for user {user_id}")
|
|
656
|
-
elif hasattr(auth_result, 'auth_required') and auth_result.auth_required:
|
|
657
|
-
logger.info(f"User {user_id} needs to authenticate for {provider}")
|
|
658
|
-
providers[provider] = ProviderAuthInfo(
|
|
659
|
-
auth_required=True,
|
|
660
|
-
auth_url=getattr(auth_result, 'auth_url', None),
|
|
661
|
-
auth_id=getattr(auth_result, 'auth_id', None)
|
|
662
|
-
)
|
|
663
|
-
else:
|
|
664
|
-
logger.warning(f"No token returned for {provider} provider")
|
|
665
|
-
|
|
666
|
-
except Exception as e:
|
|
667
|
-
logger.error(f"Failed to get {provider} token: {e}")
|
|
668
|
-
# Continue with other providers
|
|
669
|
-
|
|
670
|
-
return UserAuthInfo(user_id=user_id, providers=providers)
|
|
671
|
-
|
|
672
515
|
def get_app(self) -> FastAPI:
|
|
673
516
|
"""Get the FastAPI app instance."""
|
|
674
517
|
return self.app
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Core types and interfaces for the triggers framework."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProviderAuthInfo(BaseModel):
|
|
14
|
+
"""Authentication info for a specific OAuth provider."""
|
|
15
|
+
|
|
16
|
+
token: Optional[str] = None
|
|
17
|
+
auth_required: bool = False
|
|
18
|
+
auth_url: Optional[str] = None
|
|
19
|
+
auth_id: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserAuthInfo(BaseModel):
|
|
23
|
+
"""User authentication info containing OAuth tokens or auth requirements."""
|
|
24
|
+
|
|
25
|
+
user_id: str
|
|
26
|
+
providers: Dict[str, ProviderAuthInfo] = Field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
class Config:
|
|
29
|
+
arbitrary_types_allowed = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentInvocationRequest(BaseModel):
|
|
35
|
+
"""Request to invoke an AI agent."""
|
|
36
|
+
|
|
37
|
+
assistant_id: str
|
|
38
|
+
user_id: str
|
|
39
|
+
input_data: Any
|
|
40
|
+
thread_id: Optional[str] = None
|
|
41
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TriggerHandlerResult(BaseModel):
|
|
45
|
+
"""Result returned by trigger handlers."""
|
|
46
|
+
invoke_agent: bool = Field(default=True, description="Whether to invoke agents for this event")
|
|
47
|
+
agent_message: Optional[str] = Field(default=None, description="String message to send to agents")
|
|
48
|
+
response_body: Optional[Dict[str, Any]] = Field(default=None, description="Custom HTTP response body (when invoke_agent=False)")
|
|
49
|
+
registration: Optional[Dict[str, Any]] = Field(default=None, description="Registration data (required when invoke_agent=True)")
|
|
50
|
+
|
|
51
|
+
def model_post_init(self, __context) -> None:
|
|
52
|
+
"""Validate that required fields are provided based on invoke_agent."""
|
|
53
|
+
if self.invoke_agent and not self.agent_message:
|
|
54
|
+
raise ValueError("agent_message is required when invoke_agent=True")
|
|
55
|
+
if self.invoke_agent and not self.registration:
|
|
56
|
+
raise ValueError("registration is required when invoke_agent=True")
|
|
57
|
+
if not self.invoke_agent and not self.response_body:
|
|
58
|
+
raise ValueError("response_body is required when invoke_agent=False")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TriggerRegistrationResult(BaseModel):
|
|
62
|
+
"""Result returned by registration handlers."""
|
|
63
|
+
create_registration: bool = Field(default=True, description="Whether to create database registration (False = return custom response)")
|
|
64
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata to store with the registration")
|
|
65
|
+
response_body: Optional[Dict[str, Any]] = Field(default=None, description="Custom HTTP response body (when create_registration=False)")
|
|
66
|
+
status_code: Optional[int] = Field(default=None, description="HTTP status code (when create_registration=False)")
|
|
67
|
+
|
|
68
|
+
def model_post_init(self, __context) -> None:
|
|
69
|
+
"""Validate that required fields are provided based on create_registration."""
|
|
70
|
+
if self.create_registration and not self.metadata:
|
|
71
|
+
self.metadata = {} # Allow empty metadata for create_registration=True
|
|
72
|
+
if not self.create_registration and (not self.response_body or not self.status_code):
|
|
73
|
+
raise ValueError("Both response_body and status_code are required when create_registration=False")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TriggerRegistrationModel(BaseModel):
|
|
77
|
+
"""Base class for trigger resource models that define how webhooks are matched to registrations."""
|
|
78
|
+
pass
|
|
@@ -61,10 +61,14 @@ class TriggerDatabaseInterface(ABC):
|
|
|
61
61
|
async def find_registration_by_resource(
|
|
62
62
|
self,
|
|
63
63
|
template_id: str,
|
|
64
|
-
resource_data: Dict[str, Any]
|
|
65
|
-
user_id: str
|
|
64
|
+
resource_data: Dict[str, Any]
|
|
66
65
|
) -> Optional[Dict[str, Any]]:
|
|
67
|
-
"""Find trigger registration by matching resource data
|
|
66
|
+
"""Find trigger registration by matching resource data."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def get_all_registrations(self, template_id: str) -> List[Dict[str, Any]]:
|
|
71
|
+
"""Get all registrations for a specific trigger template."""
|
|
68
72
|
pass
|
|
69
73
|
|
|
70
74
|
@abstractmethod
|
|
@@ -130,7 +134,3 @@ class TriggerDatabaseInterface(ABC):
|
|
|
130
134
|
"""Get user ID by email from trigger registrations."""
|
|
131
135
|
pass
|
|
132
136
|
|
|
133
|
-
@abstractmethod
|
|
134
|
-
async def validate_api_key(self, api_key: str) -> Optional[str]:
|
|
135
|
-
"""Validate API key and return user_id if valid, None if invalid."""
|
|
136
|
-
pass
|
|
@@ -143,7 +143,7 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
143
143
|
return response.data[0] if response.data else None
|
|
144
144
|
|
|
145
145
|
except Exception as e:
|
|
146
|
-
logger.
|
|
146
|
+
logger.exception(f"Error creating trigger registration: {e}")
|
|
147
147
|
return None
|
|
148
148
|
|
|
149
149
|
async def get_user_trigger_registrations(self, user_id: str) -> List[Dict[str, Any]]:
|
|
@@ -225,15 +225,14 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
225
225
|
async def find_registration_by_resource(
|
|
226
226
|
self,
|
|
227
227
|
template_id: str,
|
|
228
|
-
resource_data: Dict[str, Any]
|
|
229
|
-
user_id: str
|
|
228
|
+
resource_data: Dict[str, Any]
|
|
230
229
|
) -> Optional[Dict[str, Any]]:
|
|
231
|
-
"""Find trigger registration by matching resource data
|
|
230
|
+
"""Find trigger registration by matching resource data."""
|
|
232
231
|
try:
|
|
233
|
-
# Build query to match against trigger_registrations with template_id
|
|
232
|
+
# Build query to match against trigger_registrations with template_id filter
|
|
234
233
|
query = self.client.table("trigger_registrations").select(
|
|
235
234
|
"*, trigger_templates(id, name, description)"
|
|
236
|
-
).eq("trigger_templates.id", template_id)
|
|
235
|
+
).eq("trigger_templates.id", template_id)
|
|
237
236
|
|
|
238
237
|
# Add resource field matches
|
|
239
238
|
for field, value in resource_data.items():
|
|
@@ -249,6 +248,18 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
249
248
|
logger.error(f"Error finding registration by resource: {e}")
|
|
250
249
|
return None
|
|
251
250
|
|
|
251
|
+
async def get_all_registrations(self, template_id: str) -> List[Dict[str, Any]]:
|
|
252
|
+
"""Get all registrations for a specific trigger template."""
|
|
253
|
+
try:
|
|
254
|
+
response = self.client.table("trigger_registrations").select(
|
|
255
|
+
"*, trigger_templates(id, name, description)"
|
|
256
|
+
).eq("trigger_templates.id", template_id).execute()
|
|
257
|
+
|
|
258
|
+
return response.data or []
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(f"Error getting all registrations for template {template_id}: {e}")
|
|
261
|
+
return []
|
|
262
|
+
|
|
252
263
|
# ========== Agent-Trigger Links ==========
|
|
253
264
|
|
|
254
265
|
async def link_agent_to_trigger(
|
|
@@ -346,30 +357,3 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
346
357
|
logger.error(f"Error getting user by email: {e}")
|
|
347
358
|
return None
|
|
348
359
|
|
|
349
|
-
async def validate_api_key(self, api_key: str) -> Optional[str]:
|
|
350
|
-
"""Validate API key and return user_id if valid, None if invalid."""
|
|
351
|
-
try:
|
|
352
|
-
# Query all user API keys to find a match
|
|
353
|
-
response = self.client.table("user_api_keys").select("user_id, key_hash").execute()
|
|
354
|
-
|
|
355
|
-
if not response.data:
|
|
356
|
-
return None
|
|
357
|
-
|
|
358
|
-
# Check each encrypted key to see if it matches the provided key
|
|
359
|
-
for row in response.data:
|
|
360
|
-
try:
|
|
361
|
-
decrypted_key = self._decrypt_secret(row["key_hash"])
|
|
362
|
-
if decrypted_key == api_key:
|
|
363
|
-
logger.info(f"Valid API key authenticated for user: {row['user_id']}")
|
|
364
|
-
return row["user_id"]
|
|
365
|
-
except Exception as e:
|
|
366
|
-
# Skip keys that fail to decrypt
|
|
367
|
-
logger.debug(f"Failed to decrypt API key: {e}")
|
|
368
|
-
continue
|
|
369
|
-
|
|
370
|
-
logger.warning(f"Invalid API key provided")
|
|
371
|
-
return None
|
|
372
|
-
|
|
373
|
-
except Exception as e:
|
|
374
|
-
logger.error(f"Error validating API key: {e}")
|
|
375
|
-
return None
|
{langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/langchain_triggers/decorators.py
RENAMED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
-
from typing import Any,
|
|
7
|
-
from .
|
|
6
|
+
from typing import Any, Dict, List, Type, get_type_hints
|
|
7
|
+
from langchain_auth.client import Client
|
|
8
|
+
from .core import TriggerHandlerResult, TriggerRegistrationResult
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
|
|
10
11
|
class TriggerTemplate:
|
|
@@ -19,9 +20,6 @@ class TriggerTemplate:
|
|
|
19
20
|
|
|
20
21
|
registration_handler,
|
|
21
22
|
trigger_handler,
|
|
22
|
-
registration_resolver,
|
|
23
|
-
|
|
24
|
-
oauth_providers: Optional[Dict[str, List[str]]] = None,
|
|
25
23
|
):
|
|
26
24
|
self.id = id
|
|
27
25
|
self.name = name
|
|
@@ -29,21 +27,17 @@ class TriggerTemplate:
|
|
|
29
27
|
self.registration_model = registration_model
|
|
30
28
|
self.registration_handler = registration_handler
|
|
31
29
|
self.trigger_handler = trigger_handler
|
|
32
|
-
self.registration_resolver = registration_resolver
|
|
33
|
-
self.oauth_providers = oauth_providers or {}
|
|
34
30
|
|
|
35
31
|
self._validate_handler_signatures()
|
|
36
32
|
|
|
37
33
|
def _validate_handler_signatures(self):
|
|
38
34
|
"""Validate that all handler functions have the correct signatures."""
|
|
39
|
-
# Expected: async def handler(
|
|
40
|
-
self._validate_handler("registration_handler", self.registration_handler, [self.registration_model
|
|
35
|
+
# Expected: async def handler(user_id: str, auth_client: Client, registration: RegistrationModel) -> TriggerRegistrationResult
|
|
36
|
+
self._validate_handler("registration_handler", self.registration_handler, [str, Client, self.registration_model], TriggerRegistrationResult)
|
|
41
37
|
|
|
42
|
-
# Expected: async def handler(payload: Dict[str, Any],
|
|
43
|
-
self._validate_handler("trigger_handler", self.trigger_handler, [Dict[str, Any],
|
|
38
|
+
# Expected: async def handler(payload: Dict[str, Any], query_params: Dict[str, str], database, auth_client: Client) -> TriggerHandlerResult
|
|
39
|
+
self._validate_handler("trigger_handler", self.trigger_handler, [Dict[str, Any], Dict[str, str], Any, Client], TriggerHandlerResult)
|
|
44
40
|
|
|
45
|
-
# Expected: async def resolver(payload: Dict[str, Any], query_params: Dict[str, str]) -> RegistrationModel
|
|
46
|
-
self._validate_handler("registration_resolver", self.registration_resolver, [Dict[str, Any], Dict[str, str]], self.registration_model)
|
|
47
41
|
|
|
48
42
|
def _validate_handler(self, handler_name: str, handler_func, expected_types: List[Type], expected_return_type: Type = None):
|
|
49
43
|
"""Common validation logic for all handler functions."""
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""Core types and interfaces for the triggers framework."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any, Dict, Optional
|
|
6
|
-
from pydantic import BaseModel, Field
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ProviderAuthInfo(BaseModel):
|
|
11
|
-
"""Authentication info for a specific OAuth provider."""
|
|
12
|
-
|
|
13
|
-
token: Optional[str] = None
|
|
14
|
-
auth_required: bool = False
|
|
15
|
-
auth_url: Optional[str] = None
|
|
16
|
-
auth_id: Optional[str] = None
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class UserAuthInfo(BaseModel):
|
|
20
|
-
"""User authentication info containing OAuth tokens or auth requirements."""
|
|
21
|
-
|
|
22
|
-
user_id: str
|
|
23
|
-
providers: Dict[str, ProviderAuthInfo] = Field(default_factory=dict)
|
|
24
|
-
|
|
25
|
-
class Config:
|
|
26
|
-
arbitrary_types_allowed = True
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class MetadataManager:
|
|
30
|
-
"""Manages trigger registration metadata with database persistence."""
|
|
31
|
-
|
|
32
|
-
def __init__(self, database: Any, registration_id: str, initial_metadata: Dict[str, Any]):
|
|
33
|
-
self.database = database
|
|
34
|
-
self.registration_id = registration_id
|
|
35
|
-
self.metadata = initial_metadata.copy()
|
|
36
|
-
|
|
37
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
38
|
-
"""Get a metadata value by key."""
|
|
39
|
-
return self.metadata.get(key, default)
|
|
40
|
-
|
|
41
|
-
async def update(self, updates: Dict[str, Any]) -> None:
|
|
42
|
-
"""Update metadata and persist to database."""
|
|
43
|
-
# Update local state
|
|
44
|
-
self.metadata.update(updates)
|
|
45
|
-
|
|
46
|
-
# Persist to database
|
|
47
|
-
await self.database.update_trigger_metadata(self.registration_id, updates)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class AgentInvocationRequest(BaseModel):
|
|
52
|
-
"""Request to invoke an AI agent."""
|
|
53
|
-
|
|
54
|
-
assistant_id: str
|
|
55
|
-
user_id: str
|
|
56
|
-
input_data: Any
|
|
57
|
-
thread_id: Optional[str] = None
|
|
58
|
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class TriggerHandlerResult(BaseModel):
|
|
62
|
-
"""Result returned by trigger handlers."""
|
|
63
|
-
|
|
64
|
-
invoke_agent: bool = Field(default=True, description="Whether to invoke agents for this event")
|
|
65
|
-
data: Optional[str] = Field(default=None, description="String data to send to agents")
|
|
66
|
-
|
|
67
|
-
def model_post_init(self, __context) -> None:
|
|
68
|
-
"""Validate that data is provided when invoke_agent is True."""
|
|
69
|
-
if self.invoke_agent and not self.data:
|
|
70
|
-
raise ValueError("data field is required when invoke_agent is True")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class TriggerRegistrationResult(BaseModel):
|
|
74
|
-
"""Result returned by registration handlers."""
|
|
75
|
-
|
|
76
|
-
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata to store with the registration")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class TriggerRegistrationModel(BaseModel):
|
|
80
|
-
"""Base class for trigger resource models that define how webhooks are matched to registrations."""
|
|
81
|
-
|
|
82
|
-
class Config:
|
|
83
|
-
arbitrary_types_allowed = True
|
{langchain_trigger_server-0.1.8 → langchain_trigger_server-0.1.9}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|