langchain-trigger-server 0.2.7__tar.gz → 0.2.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.

Files changed (28) hide show
  1. langchain_trigger_server-0.2.9/.github/workflows/release.yml +124 -0
  2. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/PKG-INFO +1 -1
  3. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/app.py +51 -16
  4. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/core.py +7 -0
  5. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/cron_manager.py +5 -0
  6. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/pyproject.toml +1 -1
  7. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/tests/unit/test_trigger_server_api.py +95 -1
  8. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/uv.lock +1 -1
  9. langchain_trigger_server-0.2.9/version_comparison.txt +1 -0
  10. langchain_trigger_server-0.2.7/.github/workflows/release.yml +0 -38
  11. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/.github/actions/uv_setup/action.yml +0 -0
  12. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/.github/workflows/_lint.yml +0 -0
  13. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/.github/workflows/_test.yml +0 -0
  14. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/.github/workflows/ci.yml +0 -0
  15. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/.gitignore +0 -0
  16. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/Makefile +0 -0
  17. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/README.md +0 -0
  18. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/__init__.py +0 -0
  19. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/auth/__init__.py +0 -0
  20. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/auth/slack_hmac.py +0 -0
  21. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/database/__init__.py +0 -0
  22. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/database/interface.py +0 -0
  23. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/database/supabase.py +0 -0
  24. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/decorators.py +0 -0
  25. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/triggers/__init__.py +0 -0
  26. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/langchain_triggers/triggers/cron_trigger.py +0 -0
  27. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/tests/__init__.py +0 -0
  28. {langchain_trigger_server-0.2.7 → langchain_trigger_server-0.2.9}/tests/unit/__init__.py +0 -0
@@ -0,0 +1,124 @@
1
+ name: Build and publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ release:
7
+ types: [published]
8
+ workflow_dispatch:
9
+
10
+ env:
11
+ PYTHON_VERSION: "3.11"
12
+ UV_FROZEN: "true"
13
+ UV_NO_SYNC: "true"
14
+
15
+ jobs:
16
+ # First, run lint and test to ensure quality
17
+ lint-and-test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ matrix:
21
+ python-version: ["3.11", "3.12"]
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Python + uv
26
+ uses: "./.github/actions/uv_setup"
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+
30
+ - name: Install dependencies
31
+ run: uv sync --group dev
32
+
33
+ - name: Run lint
34
+ run: uv run ruff check .
35
+
36
+ - name: Run tests
37
+ run: uv run pytest tests/
38
+
39
+ # Check version and publish package
40
+ build-and-publish:
41
+ needs: lint-and-test
42
+ name: Build and publish Python distribution to PyPI
43
+ runs-on: ubuntu-latest
44
+ environment:
45
+ name: pypi
46
+ url: https://pypi.org/p/langchain-trigger-server
47
+ permissions:
48
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
49
+ contents: read # Required for private repository access
50
+
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+ with:
54
+ token: ${{ secrets.GITHUB_TOKEN }}
55
+
56
+ - name: Set up Python + uv
57
+ uses: "./.github/actions/uv_setup"
58
+ with:
59
+ python-version: ${{ env.PYTHON_VERSION }}
60
+
61
+ - name: Get current version from pyproject.toml
62
+ id: get-version
63
+ run: |
64
+ VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
65
+ echo "current_version=$VERSION" >> $GITHUB_OUTPUT
66
+ echo "Current version: $VERSION"
67
+
68
+ - name: Get latest PyPI version
69
+ id: get-pypi-version
70
+ run: |
71
+ PYPI_VERSION=$(python -c "
72
+ import json, urllib.request, urllib.error
73
+ try:
74
+ with urllib.request.urlopen('https://pypi.org/pypi/langchain-trigger-server/json') as response:
75
+ data = json.loads(response.read())
76
+ print(data['info']['version'])
77
+ except urllib.error.HTTPError as e:
78
+ if e.code == 404:
79
+ print('0.0.0') # Package doesn't exist yet
80
+ else:
81
+ raise
82
+ ")
83
+ echo "pypi_version=$PYPI_VERSION" >> $GITHUB_OUTPUT
84
+ echo "PyPI version: $PYPI_VERSION"
85
+
86
+ - name: Compare versions
87
+ id: compare-versions
88
+ run: |
89
+ python -c "
90
+ from distutils.version import LooseVersion
91
+ current = LooseVersion('${{ steps.get-version.outputs.current_version }}')
92
+ pypi = LooseVersion('${{ steps.get-pypi-version.outputs.pypi_version }}')
93
+ should_publish = current > pypi
94
+ print(f'Current: {current}, PyPI: {pypi}, Should publish: {should_publish}')
95
+ " > version_comparison.txt
96
+ cat version_comparison.txt
97
+
98
+ SHOULD_PUBLISH=$(python -c "
99
+ from distutils.version import LooseVersion
100
+ current = LooseVersion('${{ steps.get-version.outputs.current_version }}')
101
+ pypi = LooseVersion('${{ steps.get-pypi-version.outputs.pypi_version }}')
102
+ print('true' if current > pypi else 'false')
103
+ ")
104
+ echo "should_publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT
105
+
106
+ - name: Build package
107
+ if: steps.compare-versions.outputs.should_publish == 'true'
108
+ run: uv build
109
+
110
+ - name: Publish package distributions to PyPI
111
+ if: steps.compare-versions.outputs.should_publish == 'true'
112
+ uses: pypa/gh-action-pypi-publish@release/v1
113
+ with:
114
+ verbose: true
115
+ print-hash: true
116
+ attestations: false
117
+
118
+ - name: Create summary
119
+ run: |
120
+ if [ "${{ steps.compare-versions.outputs.should_publish }}" = "true" ]; then
121
+ echo "✅ Published langchain-trigger-server v${{ steps.get-version.outputs.current_version }} to PyPI" >> $GITHUB_STEP_SUMMARY
122
+ else
123
+ echo "⏭️ Skipped langchain-trigger-server: v${{ steps.get-version.outputs.current_version }} not newer than PyPI v${{ steps.get-pypi-version.outputs.pypi_version }}" >> $GITHUB_STEP_SUMMARY
124
+ fi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-trigger-server
3
- Version: 0.2.7
3
+ Version: 0.2.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
@@ -5,8 +5,10 @@ from __future__ import annotations
5
5
  import logging
6
6
  import os
7
7
  from collections.abc import Callable
8
+ from datetime import UTC, datetime, timedelta
8
9
  from typing import Any
9
10
 
11
+ import jwt
10
12
  from fastapi import Depends, FastAPI, HTTPException, Request
11
13
  from langchain_auth.client import Client
12
14
  from langgraph_sdk import get_client
@@ -27,6 +29,28 @@ from .triggers.cron_trigger import CRON_TRIGGER_ID
27
29
  logger = logging.getLogger(__name__)
28
30
 
29
31
 
32
+ def get_x_service_jwt_token(
33
+ payload: dict[str, Any] | None = None, expiration_seconds: int = 60 * 60
34
+ ) -> str:
35
+ exp_datetime = datetime.now(tz=UTC) + timedelta(seconds=expiration_seconds)
36
+ exp = int(exp_datetime.timestamp())
37
+
38
+ payload = payload or {}
39
+ payload = {
40
+ "sub": "unspecified",
41
+ "exp": exp,
42
+ **payload,
43
+ }
44
+
45
+ secret = os.environ["X_SERVICE_AUTH_JWT_SECRET"]
46
+
47
+ return jwt.encode(
48
+ payload,
49
+ secret,
50
+ algorithm="HS256",
51
+ )
52
+
53
+
30
54
  class AuthenticationMiddleware(BaseHTTPMiddleware):
31
55
  """Middleware to handle authentication for API endpoints."""
32
56
 
@@ -106,7 +130,6 @@ class TriggerServer:
106
130
 
107
131
  # LangGraph configuration
108
132
  self.langgraph_api_url = os.getenv("LANGGRAPH_API_URL")
109
- self.langsmith_api_key = os.getenv("LANGCHAIN_API_KEY")
110
133
  self.trigger_server_auth_api_url = os.getenv("TRIGGER_SERVER_HOST_API_URL")
111
134
 
112
135
  if not self.langgraph_api_url:
@@ -115,16 +138,10 @@ class TriggerServer:
115
138
  self.langgraph_api_url = self.langgraph_api_url.rstrip("/")
116
139
 
117
140
  # Initialize LangGraph SDK client
118
- self.langgraph_client = get_client(
119
- url=self.langgraph_api_url, api_key=self.langsmith_api_key
120
- )
141
+ self.langgraph_client = get_client(url=self.langgraph_api_url, api_key=None)
121
142
  logger.info(
122
143
  f"✓ LangGraph client initialized with URL: {self.langgraph_api_url}"
123
144
  )
124
- if self.langsmith_api_key:
125
- logger.info("✓ LangGraph client initialized with API key.")
126
- else:
127
- logger.warning("⚠ LangGraph client initialized without API key")
128
145
 
129
146
  # Initialize LangChain auth client
130
147
  langchain_api_key = os.getenv("LANGCHAIN_API_KEY")
@@ -323,6 +340,7 @@ class TriggerServer:
323
340
  raise HTTPException(
324
341
  status_code=400, detail=f"Unknown trigger type: {trigger_id}"
325
342
  )
343
+ client_metadata = payload.pop("metadata", None)
326
344
 
327
345
  # Parse payload into registration model first
328
346
  try:
@@ -368,11 +386,16 @@ class TriggerServer:
368
386
 
369
387
  resource_dict = registration_instance.model_dump()
370
388
 
389
+ merged_metadata = {}
390
+ if client_metadata:
391
+ merged_metadata["client_metadata"] = client_metadata
392
+ merged_metadata.update(result.metadata)
393
+
371
394
  registration = await self.database.create_trigger_registration(
372
395
  user_id=user_id,
373
396
  template_id=trigger.id,
374
397
  resource=resource_dict,
375
- metadata=result.metadata,
398
+ metadata=merged_metadata,
376
399
  )
377
400
 
378
401
  if not registration:
@@ -579,6 +602,7 @@ class TriggerServer:
579
602
  body_bytes = await request.body()
580
603
  body_str = body_bytes.decode("utf-8")
581
604
 
605
+ # TODO(sam/palash): We should not have API specific things in this framework repo. We should clean this up.
582
606
  if self._is_slack_trigger(trigger):
583
607
  await self._verify_slack_webhook_auth_with_body(
584
608
  request, body_str
@@ -623,6 +647,11 @@ class TriggerServer:
623
647
  # Ensure agent_id and user_id are strings for JSON serialization
624
648
  agent_id_str = str(agent_id)
625
649
  user_id_str = str(result.registration["user_id"])
650
+ tenant_id_str = str(
651
+ result.registration.get("metadata", {})
652
+ .get("client_metadata", {})
653
+ .get("tenant_id")
654
+ )
626
655
 
627
656
  agent_input = {"messages": [{"role": "human", "content": message}]}
628
657
 
@@ -630,6 +659,7 @@ class TriggerServer:
630
659
  success = await self._invoke_agent(
631
660
  agent_id=agent_id_str,
632
661
  user_id=user_id_str,
662
+ tenant_id=tenant_id_str,
633
663
  input_data=agent_input,
634
664
  )
635
665
  if success:
@@ -656,6 +686,7 @@ class TriggerServer:
656
686
  self,
657
687
  agent_id: str,
658
688
  user_id: str,
689
+ tenant_id: str,
659
690
  input_data: dict[str, Any],
660
691
  ) -> bool:
661
692
  """Invoke LangGraph agent using the SDK."""
@@ -665,20 +696,23 @@ class TriggerServer:
665
696
 
666
697
  try:
667
698
  headers = {
668
- "x-auth-scheme": "agent-builder-trigger",
699
+ "x-api-key": "",
700
+ "x-auth-scheme": "langsmith-agent",
669
701
  "x-user-id": user_id_str,
702
+ "x-tenant-id": tenant_id,
703
+ "x-service-key": get_x_service_jwt_token(
704
+ payload={
705
+ "tenant_id": tenant_id,
706
+ "user_id": user_id_str,
707
+ }
708
+ ),
670
709
  }
671
710
 
672
- # Note: API key is already set in client initialization, no need to add to headers
673
- if not self.langsmith_api_key:
674
- logger.warning(
675
- "No LANGSMITH_API_KEY available - authentication may fail"
676
- )
677
-
678
711
  thread = await self.langgraph_client.threads.create(
679
712
  metadata={
680
713
  "triggered_by": "langchain-triggers",
681
714
  "user_id": user_id_str,
715
+ "tenant_id": tenant_id,
682
716
  },
683
717
  headers=headers,
684
718
  )
@@ -691,6 +725,7 @@ class TriggerServer:
691
725
  metadata={
692
726
  "triggered_by": "langchain-triggers",
693
727
  "user_id": user_id_str,
728
+ "tenant_id": tenant_id,
694
729
  },
695
730
  headers=headers,
696
731
  )
@@ -88,6 +88,13 @@ class TriggerRegistrationResult(BaseModel):
88
88
  """Validate that required fields are provided based on create_registration."""
89
89
  if self.create_registration and not self.metadata:
90
90
  self.metadata = {} # Allow empty metadata for create_registration=True
91
+
92
+ if "client_metadata" in self.metadata:
93
+ raise ValueError(
94
+ "The 'client_metadata' key is reserved for client-provided metadata. "
95
+ "Registration handlers must not use this key in their metadata."
96
+ )
97
+
91
98
  if not self.create_registration and (
92
99
  not self.response_body or not self.status_code
93
100
  ):
@@ -202,6 +202,9 @@ class CronTriggerManager:
202
202
  """Execute a cron job - invoke agents. Can be called manually or by scheduler."""
203
203
  registration_id = registration["id"]
204
204
  user_id = registration["user_id"]
205
+ tenant_id = (
206
+ registration.get("metadata", {}).get("client_metadata", {}).get("tenant_id")
207
+ )
205
208
 
206
209
  # Get agent links
207
210
  agent_links = await self.trigger_server.database.get_agents_for_trigger(
@@ -222,6 +225,7 @@ class CronTriggerManager:
222
225
  # Ensure agent_id and user_id are strings for JSON serialization
223
226
  agent_id_str = str(agent_id)
224
227
  user_id_str = str(user_id)
228
+ tenant_id_str = str(tenant_id)
225
229
 
226
230
  current_time = datetime.utcnow()
227
231
  current_time_str = current_time.strftime("%A, %B %d, %Y at %H:%M UTC")
@@ -239,6 +243,7 @@ class CronTriggerManager:
239
243
  success = await self.trigger_server._invoke_agent(
240
244
  agent_id=agent_id_str,
241
245
  user_id=user_id_str,
246
+ tenant_id=tenant_id_str,
242
247
  input_data=agent_input,
243
248
  )
244
249
  if success:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langchain-trigger-server"
7
- version = "0.2.7"
7
+ version = "0.2.9"
8
8
  description = "Generic event-driven triggers framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -6,11 +6,11 @@ from unittest.mock import AsyncMock, patch
6
6
  import pytest
7
7
  import pytest_asyncio
8
8
  from httpx import ASGITransport, AsyncClient
9
+ from pydantic import BaseModel
9
10
 
10
11
  from langchain_triggers import TriggerServer
11
12
 
12
13
 
13
- # Mock auth handler that returns a valid user
14
14
  async def mock_auth_handler(request_body, headers):
15
15
  """Mock authentication handler for testing."""
16
16
  auth_header = headers.get("authorization", "")
@@ -21,6 +21,17 @@ async def mock_auth_handler(request_body, headers):
21
21
  return {"identity": f"test_user_{token}"}
22
22
 
23
23
 
24
+ class TestRegistration(BaseModel):
25
+ """Simple registration model for testing."""
26
+
27
+ name: str
28
+
29
+
30
+ async def dummy_trigger_handler(payload, query_params, database, auth_client):
31
+ """Dummy trigger handler for test triggers."""
32
+ return None
33
+
34
+
24
35
  # Mock database class
25
36
  class MockDatabase:
26
37
  """Mock database for testing."""
@@ -354,3 +365,86 @@ async def test_user_isolation(trigger_server):
354
365
  data = response.json()
355
366
  assert len(data["data"]) == 1
356
367
  assert data["data"][0]["user_id"] == "test_user_token2"
368
+
369
+
370
+ @pytest.mark.asyncio
371
+ async def test_client_metadata_storage(trigger_server):
372
+ """Test that client-provided metadata is stored as client_metadata."""
373
+ from langchain_triggers import TriggerRegistrationResult, TriggerTemplate
374
+
375
+ async def test_registration_handler(user_id, auth_client, registration):
376
+ return TriggerRegistrationResult(metadata={"handler_data": "from_handler"})
377
+
378
+ test_trigger = TriggerTemplate(
379
+ id="test_metadata_trigger",
380
+ provider="test",
381
+ name="Test Metadata",
382
+ description="Tests metadata storage",
383
+ registration_model=TestRegistration,
384
+ registration_handler=test_registration_handler,
385
+ trigger_handler=dummy_trigger_handler,
386
+ )
387
+ trigger_server.add_trigger(test_trigger)
388
+
389
+ transport = ASGITransport(app=trigger_server.app, raise_app_exceptions=True)
390
+ async with AsyncClient(base_url="http://localhost", transport=transport) as client:
391
+ response = await client.post(
392
+ "/v1/triggers/registrations",
393
+ headers={"Authorization": "Bearer token1"},
394
+ json={
395
+ "type": "test_metadata_trigger",
396
+ "name": "test",
397
+ "metadata": {"tenant_id": "org-123"},
398
+ },
399
+ )
400
+
401
+ assert response.status_code == 200
402
+ data = response.json()
403
+ assert data["success"] is True
404
+
405
+ # Verify metadata in API response
406
+ registration_id = data["data"]["id"]
407
+ metadata = data["data"]["metadata"]
408
+ assert metadata["client_metadata"]["tenant_id"] == "org-123"
409
+ assert metadata["handler_data"] == "from_handler"
410
+
411
+ # Verify metadata persisted in database
412
+ db_registration = await trigger_server.database.get_trigger_registration(
413
+ registration_id, user_id="test_user_token1"
414
+ )
415
+ assert db_registration is not None
416
+ db_metadata = db_registration["metadata"]
417
+ assert db_metadata["client_metadata"]["tenant_id"] == "org-123"
418
+ assert db_metadata["handler_data"] == "from_handler"
419
+
420
+
421
+ @pytest.mark.asyncio
422
+ async def test_handler_cannot_use_client_metadata_key(trigger_server):
423
+ """Test that handlers cannot use the reserved client_metadata key."""
424
+ from langchain_triggers import TriggerRegistrationResult, TriggerTemplate
425
+
426
+ async def bad_handler(user_id, auth_client, registration):
427
+ # Handler tries to use reserved key
428
+ return TriggerRegistrationResult(metadata={"client_metadata": "not allowed"})
429
+
430
+ test_trigger = TriggerTemplate(
431
+ id="test_bad_handler",
432
+ provider="test",
433
+ name="Test Bad Handler",
434
+ description="Tests validation",
435
+ registration_model=TestRegistration,
436
+ registration_handler=bad_handler,
437
+ trigger_handler=dummy_trigger_handler,
438
+ )
439
+ trigger_server.add_trigger(test_trigger)
440
+
441
+ transport = ASGITransport(app=trigger_server.app, raise_app_exceptions=True)
442
+ async with AsyncClient(base_url="http://localhost", transport=transport) as client:
443
+ response = await client.post(
444
+ "/v1/triggers/registrations",
445
+ headers={"Authorization": "Bearer token1"},
446
+ json={"type": "test_bad_handler", "name": "test"},
447
+ )
448
+
449
+ assert response.status_code == 500
450
+ assert "client_metadata" in response.json()["detail"]
@@ -393,7 +393,7 @@ wheels = [
393
393
 
394
394
  [[package]]
395
395
  name = "langchain-trigger-server"
396
- version = "0.2.6rc8"
396
+ version = "0.2.9"
397
397
  source = { editable = "." }
398
398
  dependencies = [
399
399
  { name = "apscheduler" },
@@ -0,0 +1 @@
1
+ Current: 0.2.9, PyPI: 0.2.8, Should publish: True
@@ -1,38 +0,0 @@
1
- name: Build and publish to PyPI
2
-
3
- on:
4
- release:
5
- types: [published]
6
- workflow_dispatch:
7
-
8
- jobs:
9
- build-and-publish:
10
- name: Build and publish Python distribution to PyPI
11
- runs-on: ubuntu-latest
12
- environment:
13
- name: pypi
14
- url: https://pypi.org/p/langchain-trigger-server
15
- permissions:
16
- id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
17
- contents: read # Required for private repository access
18
-
19
- steps:
20
- - uses: actions/checkout@v4
21
- with:
22
- token: ${{ secrets.GITHUB_TOKEN }}
23
-
24
- - name: Set up Python
25
- uses: actions/setup-python@v4
26
- with:
27
- python-version: "3.11"
28
-
29
- - name: Install build dependencies
30
- run: |
31
- python -m pip install --upgrade pip
32
- python -m pip install build
33
-
34
- - name: Build package
35
- run: python -m build
36
-
37
- - name: Publish package distributions to PyPI
38
- uses: pypa/gh-action-pypi-publish@release/v1