langchain-trigger-server 0.3.6__tar.gz → 0.3.8__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 (31) hide show
  1. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/PKG-INFO +1 -1
  2. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/__init__.py +0 -2
  3. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/app.py +6 -281
  4. langchain_trigger_server-0.3.8/langchain_triggers/auth/__init__.py +3 -0
  5. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/core.py +0 -27
  6. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/cron_manager.py +5 -83
  7. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/decorators.py +7 -8
  8. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/triggers/cron_trigger.py +71 -15
  9. langchain_trigger_server-0.3.8/langchain_triggers/util.py +73 -0
  10. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/pyproject.toml +1 -1
  11. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/tests/unit/test_cron_manager_polling_filter.py +3 -5
  12. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/tests/unit/test_trigger_server_api.py +3 -3
  13. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/uv.lock +1 -1
  14. langchain_trigger_server-0.3.8/version_comparison.txt +1 -0
  15. langchain_trigger_server-0.3.6/langchain_triggers/auth/__init__.py +0 -15
  16. langchain_trigger_server-0.3.6/langchain_triggers/auth/slack_hmac.py +0 -90
  17. langchain_trigger_server-0.3.6/version_comparison.txt +0 -1
  18. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/.github/actions/uv_setup/action.yml +0 -0
  19. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/.github/workflows/_lint.yml +0 -0
  20. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/.github/workflows/_test.yml +0 -0
  21. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/.github/workflows/ci.yml +0 -0
  22. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/.github/workflows/release.yml +0 -0
  23. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/.gitignore +0 -0
  24. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/Makefile +0 -0
  25. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/README.md +0 -0
  26. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/database/__init__.py +0 -0
  27. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/database/interface.py +0 -0
  28. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/langchain_triggers/triggers/__init__.py +0 -0
  29. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/tests/__init__.py +0 -0
  30. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/tests/unit/__init__.py +0 -0
  31. {langchain_trigger_server-0.3.6 → langchain_trigger_server-0.3.8}/tests/unit/test_cron_manager_schedule_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-trigger-server
3
- Version: 0.3.6
3
+ Version: 0.3.8
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
@@ -2,7 +2,6 @@
2
2
 
3
3
  from .app import TriggerServer
4
4
  from .core import (
5
- TriggerHandlerResult,
6
5
  TriggerRegistrationModel,
7
6
  TriggerRegistrationResult,
8
7
  UserAuthInfo,
@@ -15,7 +14,6 @@ __version__ = "0.1.0"
15
14
  __all__ = [
16
15
  "UserAuthInfo",
17
16
  "TriggerRegistrationModel",
18
- "TriggerHandlerResult",
19
17
  "TriggerRegistrationResult",
20
18
  "TriggerTemplate",
21
19
  "TriggerServer",
@@ -15,12 +15,6 @@ from langgraph_sdk import get_client
15
15
  from starlette.middleware.base import BaseHTTPMiddleware
16
16
  from starlette.responses import Response
17
17
 
18
- from .auth.slack_hmac import (
19
- SlackSignatureVerificationError,
20
- extract_slack_headers,
21
- get_slack_signing_secret,
22
- verify_slack_signature,
23
- )
24
18
  from .core import TriggerType
25
19
  from .cron_manager import CronTriggerManager
26
20
  from .database import TriggerDatabaseInterface
@@ -361,7 +355,7 @@ class TriggerServer:
361
355
  detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}",
362
356
  )
363
357
  result = await trigger.registration_handler(
364
- request, user_id, self.langchain_auth_client, registration_instance
358
+ request, user_id, registration_instance
365
359
  )
366
360
 
367
361
  # Check if handler requested to skip registration (e.g., for OAuth or URL verification)
@@ -643,143 +637,18 @@ class TriggerServer:
643
637
  getattr(trigger, "provider", "<unknown>"),
644
638
  request.headers.get("content-type", ""),
645
639
  )
646
- if request.method == "POST":
647
- if request.headers.get("content-type", "").startswith(
648
- "application/json"
649
- ):
650
- # Read body once for both auth and parsing
651
- body_bytes = await request.body()
652
- body_str = body_bytes.decode("utf-8")
653
- logger.debug(
654
- "request_body_read bytes=%s",
655
- len(body_bytes) if body_bytes is not None else 0,
656
- )
657
-
658
- # TODO(sam/palash): We should not have API specific things in this framework repo. We should clean this up.
659
- if self._is_slack_trigger(trigger):
660
- logger.info(
661
- "slack_request_detected trigger_id=%s verifying_signature=true",
662
- getattr(trigger, "id", "<unknown>"),
663
- )
664
- await self._verify_slack_webhook_auth_with_body(
665
- request, body_str
666
- )
667
640
 
668
- import json
669
-
670
- try:
671
- payload = json.loads(body_str)
672
- except Exception as e:
673
- logger.error("json_parse_error error=%s", e)
674
- raise
675
- logger.info(
676
- "payload_parsed kind=json keys=%s",
677
- sorted(payload.keys())
678
- if isinstance(payload, dict)
679
- else "<non-dict>",
680
- )
681
-
682
- if (
683
- payload.get("type") == "url_verification"
684
- and "challenge" in payload
685
- ):
686
- logger.info("Responding to Slack URL verification challenge")
687
- return {"challenge": payload["challenge"]}
688
- else:
689
- # Handle form data or other content types
690
- body = await request.body()
691
- payload = {"raw_body": body.decode("utf-8") if body else ""}
692
- logger.info(
693
- "payload_parsed kind=raw length=%s",
694
- len(payload.get("raw_body", "")),
695
- )
696
- else:
697
- payload = dict(request.query_params)
698
- logger.info(
699
- "payload_parsed kind=query keys=%s",
700
- sorted(payload.keys()),
701
- )
702
-
703
- query_params = dict(request.query_params)
704
641
  logger.info(
705
- "invoking_trigger_handler trigger_id=%s provider=%s query_keys=%s",
642
+ "invoking_trigger_handler trigger_id=%s provider=%s",
706
643
  getattr(trigger, "id", "<unknown>"),
707
644
  getattr(trigger, "provider", "<unknown>"),
708
- sorted(query_params.keys()),
709
- )
710
- result = await trigger.trigger_handler(
711
- payload, query_params, self.database, self.langchain_auth_client
712
- )
713
- logger.info(
714
- "trigger_handler_completed invoke_agent=%s messages=%s has_registration=%s",
715
- getattr(result, "invoke_agent", None),
716
- len(getattr(result, "agent_messages", []) or []),
717
- bool(getattr(result, "registration", None)),
718
- )
719
- if not result.invoke_agent:
720
- logger.info(
721
- "no_agent_invocation trigger_id=%s returning_handler_response=true",
722
- getattr(trigger, "id", "<unknown>"),
723
- )
724
- return result.response_body
725
-
726
- registration_id = result.registration["id"]
727
- agent_links = await self.database.get_agents_for_trigger(registration_id)
728
- logger.info(
729
- "agent_links_fetched registration_id=%s count=%s",
730
- registration_id,
731
- len(agent_links) if agent_links is not None else 0,
732
645
  )
733
-
734
- agents_invoked = 0
735
- # Iterate through each message and invoke agents for each
736
- for message in result.agent_messages:
737
- logger.debug(
738
- "processing_agent_message message_len=%s",
739
- len(str(message)) if message is not None else 0,
740
- )
741
- for agent_link in agent_links:
742
- agent_id = (
743
- agent_link
744
- if isinstance(agent_link, str)
745
- else agent_link.get("agent_id")
746
- )
747
- # Ensure agent_id and user_id are strings for JSON serialization
748
- agent_id_str = str(agent_id)
749
- user_id_str = str(result.registration["user_id"])
750
- tenant_id_str = str(result.registration["tenant_id"])
751
-
752
- agent_input = {"messages": [{"role": "human", "content": message}]}
753
-
754
- try:
755
- logger.info(
756
- "invoking_agent assistant_id=%s user_id=%s tenant_id=%s",
757
- agent_id_str,
758
- user_id_str,
759
- tenant_id_str,
760
- )
761
- success = await self._invoke_agent(
762
- agent_id=agent_id_str,
763
- user_id=user_id_str,
764
- tenant_id=tenant_id_str,
765
- input_data=agent_input,
766
- )
767
- if success:
768
- agents_invoked += 1
769
- logger.info(
770
- "agent_invocation_success assistant_id=%s registration_id=%s",
771
- agent_id_str,
772
- registration_id,
773
- )
774
- except Exception as e:
775
- logger.error(
776
- f"Error invoking agent {agent_id_str}: {e}", exc_info=True
777
- )
646
+ response = await trigger.trigger_handler(request, self.database)
778
647
  logger.info(
779
- f"Processed trigger handler with {len(result.agent_messages)} messages, invoked {agents_invoked} agents"
648
+ "trigger_handler_completed trigger_id=%s",
649
+ getattr(trigger, "id", "<unknown>"),
780
650
  )
781
-
782
- return {"success": True, "agents_invoked": agents_invoked}
651
+ return response
783
652
 
784
653
  except HTTPException:
785
654
  raise
@@ -856,150 +725,6 @@ class TriggerServer:
856
725
  logger.error(f"Error invoking agent {agent_id}: {e}")
857
726
  raise
858
727
 
859
- def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
860
- """Check if a trigger is from Slack and requires HMAC signature verification."""
861
- return trigger.provider.lower() == "slack" or "slack" in trigger.id.lower()
862
-
863
- async def _verify_slack_webhook_auth(self, request: Request) -> None:
864
- """Verify Slack HMAC signature for webhook requests.
865
-
866
- Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
867
- The signature is computed from the timestamp, body, and signing secret.
868
-
869
- Args:
870
- request: The FastAPI request object
871
-
872
- Raises:
873
- HTTPException: If authentication fails
874
- """
875
- try:
876
- signing_secret = get_slack_signing_secret()
877
- if not signing_secret:
878
- logger.error("SLACK_SIGNING_SECRET environment variable not set")
879
- raise HTTPException(
880
- status_code=500,
881
- detail="Slack signing secret not configured on server",
882
- )
883
-
884
- headers_dict = dict(request.headers)
885
- signature, timestamp = extract_slack_headers(headers_dict)
886
-
887
- if not signature:
888
- logger.error("Missing X-Slack-Signature header")
889
- raise HTTPException(
890
- status_code=401,
891
- detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
892
- )
893
-
894
- if not timestamp:
895
- logger.error("Missing X-Slack-Request-Timestamp header")
896
- raise HTTPException(
897
- status_code=401,
898
- detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
899
- )
900
-
901
- body = await request.body()
902
- body_str = body.decode("utf-8")
903
-
904
- try:
905
- verify_slack_signature(
906
- signing_secret=signing_secret,
907
- timestamp=timestamp,
908
- body=body_str,
909
- signature=signature,
910
- )
911
- logger.info(
912
- f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
913
- )
914
- except SlackSignatureVerificationError as e:
915
- logger.error(f"Slack signature verification failed: {e}")
916
- raise HTTPException(
917
- status_code=401,
918
- detail=f"Slack signature verification failed: {str(e)}",
919
- )
920
-
921
- # Store verification info in request state
922
- request.state.slack_verified = True
923
- request.state.slack_timestamp = timestamp
924
-
925
- except HTTPException:
926
- raise
927
- except Exception as e:
928
- logger.error(f"Unexpected error during Slack webhook authentication: {e}")
929
- raise HTTPException(
930
- status_code=500, detail=f"Authentication error: {str(e)}"
931
- )
932
-
933
- async def _verify_slack_webhook_auth_with_body(
934
- self, request: Request, body_str: str
935
- ) -> None:
936
- """Verify Slack HMAC signature for webhook requests using pre-read body.
937
-
938
- Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
939
- The signature is computed from the timestamp, body, and signing secret.
940
-
941
- Args:
942
- request: The FastAPI request object
943
- body_str: The request body as a string (already read)
944
-
945
- Raises:
946
- HTTPException: If authentication fails
947
- """
948
- try:
949
- signing_secret = get_slack_signing_secret()
950
- if not signing_secret:
951
- logger.error("SLACK_SIGNING_SECRET environment variable not set")
952
- raise HTTPException(
953
- status_code=500,
954
- detail="Slack signing secret not configured on server",
955
- )
956
-
957
- headers_dict = dict(request.headers)
958
- signature, timestamp = extract_slack_headers(headers_dict)
959
-
960
- if not signature:
961
- logger.error("Missing X-Slack-Signature header")
962
- raise HTTPException(
963
- status_code=401,
964
- detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
965
- )
966
-
967
- if not timestamp:
968
- logger.error("Missing X-Slack-Request-Timestamp header")
969
- raise HTTPException(
970
- status_code=401,
971
- detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
972
- )
973
-
974
- try:
975
- verify_slack_signature(
976
- signing_secret=signing_secret,
977
- timestamp=timestamp,
978
- body=body_str,
979
- signature=signature,
980
- )
981
- logger.info(
982
- f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
983
- )
984
- except SlackSignatureVerificationError as e:
985
- logger.error(f"Slack signature verification failed: {e}")
986
- raise HTTPException(
987
- status_code=401,
988
- detail=f"Slack signature verification failed: {str(e)}",
989
- )
990
-
991
- # Store verification info in request state
992
- request.state.slack_verified = True
993
- request.state.slack_timestamp = timestamp
994
-
995
- except HTTPException:
996
- raise
997
- except Exception as e:
998
- logger.error(f"Unexpected error during Slack webhook authentication: {e}")
999
- raise HTTPException(
1000
- status_code=500, detail=f"Authentication error: {str(e)}"
1001
- )
1002
-
1003
728
  def get_app(self) -> FastAPI:
1004
729
  """Get the FastAPI app instance."""
1005
730
  return self.app
@@ -0,0 +1,3 @@
1
+ """Authentication utilities for trigger webhooks."""
2
+
3
+ __all__ = []
@@ -47,33 +47,6 @@ class AgentInvocationRequest(BaseModel):
47
47
  metadata: dict[str, Any] = Field(default_factory=dict)
48
48
 
49
49
 
50
- class TriggerHandlerResult(BaseModel):
51
- """Result returned by trigger handlers."""
52
-
53
- invoke_agent: bool = Field(
54
- default=True, description="Whether to invoke agents for this event"
55
- )
56
- agent_messages: list[str] | None = Field(
57
- default=None,
58
- description="List of messages to send to agents (one invocation per message)",
59
- )
60
- response_body: dict[str, Any] | None = Field(
61
- default=None, description="Custom HTTP response body (when invoke_agent=False)"
62
- )
63
- registration: dict[str, Any] | None = Field(
64
- default=None, description="Registration data (required when invoke_agent=True)"
65
- )
66
-
67
- def model_post_init(self, __context) -> None:
68
- """Validate that required fields are provided based on invoke_agent."""
69
- if self.invoke_agent and not self.agent_messages:
70
- raise ValueError("agent_messages is required when invoke_agent=True")
71
- if self.invoke_agent and not self.registration:
72
- raise ValueError("registration is required when invoke_agent=True")
73
- if not self.invoke_agent and not self.response_body:
74
- raise ValueError("response_body is required when invoke_agent=False")
75
-
76
-
77
50
  class TriggerRegistrationResult(BaseModel):
78
51
  """Result returned by registration handlers."""
79
52
 
@@ -8,7 +8,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
8
8
  from apscheduler.triggers.cron import CronTrigger as APSCronTrigger
9
9
  from pydantic import BaseModel
10
10
 
11
- from langchain_triggers.core import TriggerHandlerResult, TriggerType
11
+ from langchain_triggers.core import TriggerType
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -280,21 +280,10 @@ class CronTriggerManager:
280
280
  await self._record_execution(execution)
281
281
 
282
282
  async def execute_cron_job(self, registration: dict[str, Any]) -> int:
283
- """Execute a cron job - invoke agents. Can be called manually or by scheduler."""
283
+ """Execute a cron job - calls poll handler which invokes agents."""
284
284
  registration_id = registration["id"]
285
- user_id = registration["user_id"]
286
285
  template_id = registration.get("template_id")
287
286
  template_id = str(template_id) if template_id is not None else None
288
- tenant_id = str(registration.get("tenant_id"))
289
-
290
- # Get agent links
291
- agent_links = await self.trigger_server.database.get_agents_for_trigger(
292
- registration_id
293
- )
294
-
295
- if not agent_links:
296
- logger.warning(f"No agents linked to cron job {registration_id}")
297
- return 0
298
287
 
299
288
  tmpl = next(
300
289
  (t for t in self.trigger_server.triggers if t.id == template_id), None
@@ -308,85 +297,18 @@ class CronTriggerManager:
308
297
  )
309
298
  return 0
310
299
 
311
- result: TriggerHandlerResult = await tmpl.poll_handler(
300
+ response = await tmpl.poll_handler(
312
301
  registration,
313
302
  self.trigger_server.database,
314
- self.trigger_server.langchain_auth_client,
315
303
  )
316
304
 
317
- if not result.invoke_agent:
318
- logger.info(
319
- "poll_result "
320
- f"registration_id={registration_id} "
321
- f"trigger_id={template_id} "
322
- f"provider={(tmpl.provider or '').lower()} "
323
- f"invoke_agent=false messages_count=0"
324
- )
325
- return 0
326
-
327
- agents_invoked = 0
328
- messages = result.agent_messages or []
329
-
305
+ agents_invoked = response.get("agents_invoked", 0)
330
306
  logger.info(
331
307
  "poll_result "
332
308
  f"registration_id={registration_id} "
333
309
  f"trigger_id={template_id} "
334
310
  f"provider={(tmpl.provider or '').lower()} "
335
- f"invoke_agent=true messages_count={len(messages)} "
336
- f"agents_linked={len(agent_links)}"
337
- )
338
-
339
- for _message in messages:
340
- for agent_link in agent_links:
341
- agent_id = (
342
- agent_link
343
- if isinstance(agent_link, str)
344
- else agent_link.get("agent_id")
345
- )
346
- # Ensure agent_id and user_id are strings for JSON serialization
347
- agent_id_str = str(agent_id)
348
- user_id_str = str(user_id)
349
- tenant_id_str = str(tenant_id)
350
-
351
- current_time = datetime.utcnow()
352
- current_time_str = current_time.strftime("%A, %B %d, %Y at %H:%M UTC")
353
-
354
- agent_input = {
355
- "messages": [
356
- {
357
- "role": "human",
358
- "content": f"Cron trigger fired at {current_time_str}",
359
- }
360
- ]
361
- }
362
-
363
- try:
364
- success = await self.trigger_server._invoke_agent(
365
- agent_id=agent_id_str,
366
- user_id=user_id_str,
367
- tenant_id=tenant_id_str,
368
- input_data=agent_input,
369
- )
370
- if success:
371
- logger.info(
372
- "invoke_agent_ok "
373
- f"registration_id={registration_id} "
374
- f"agent_id={agent_id_str}"
375
- )
376
- agents_invoked += 1
377
- except Exception as e:
378
- logger.error(
379
- "invoke_agent_err "
380
- f"registration_id={registration_id} "
381
- f"agent_id={agent_id_str} "
382
- f"error={str(e)}"
383
- )
384
-
385
- logger.info(
386
- "poll_invoke_summary "
387
- f"registration_id={registration_id} "
388
- f"agents_invoked={agents_invoked} "
389
- f"messages_count={len(messages)}"
311
+ f"agents_invoked={agents_invoked}"
390
312
  )
391
313
  return agents_invoked
392
314
 
@@ -10,10 +10,9 @@ import inspect
10
10
  from typing import Any, get_type_hints
11
11
 
12
12
  from fastapi import Request
13
- from langchain_auth.client import Client
14
13
  from pydantic import BaseModel
15
14
 
16
- from .core import TriggerHandlerResult, TriggerRegistrationResult, TriggerType
15
+ from .core import TriggerRegistrationResult, TriggerType
17
16
 
18
17
 
19
18
  class TriggerTemplate:
@@ -48,11 +47,11 @@ class TriggerTemplate:
48
47
 
49
48
  def _validate_handler_signatures(self):
50
49
  """Validate that all handler functions have the correct signatures."""
51
- # Expected reg: async def handler(request: Request, user_id: str, auth_client: Client, registration: RegistrationModel) -> TriggerRegistrationResult
50
+ # Expected reg: async def handler(request: Request, user_id: str, registration: RegistrationModel) -> TriggerRegistrationResult
52
51
  self._validate_handler(
53
52
  "registration_handler",
54
53
  self.registration_handler,
55
- [Request, str, Client, self.registration_model],
54
+ [Request, str, self.registration_model],
56
55
  TriggerRegistrationResult,
57
56
  )
58
57
 
@@ -64,8 +63,8 @@ class TriggerTemplate:
64
63
  self._validate_handler(
65
64
  "trigger_handler",
66
65
  self.trigger_handler,
67
- [dict[str, Any], dict[str, str], Any, Client],
68
- TriggerHandlerResult,
66
+ [Request, Any],
67
+ dict[str, Any],
69
68
  )
70
69
  else:
71
70
  if not self.poll_handler:
@@ -75,8 +74,8 @@ class TriggerTemplate:
75
74
  self._validate_handler(
76
75
  "poll_handler",
77
76
  self.poll_handler,
78
- [dict[str, Any], Any, Client],
79
- TriggerHandlerResult,
77
+ [dict[str, Any], Any],
78
+ dict[str, Any],
80
79
  )
81
80
 
82
81
  def _validate_handler(
@@ -1,21 +1,22 @@
1
1
  """Cron-based trigger for scheduled agent execution."""
2
2
 
3
3
  import logging
4
+ import uuid
4
5
  from datetime import datetime
5
6
  from typing import Any
6
7
 
7
8
  from croniter import croniter
8
9
  from fastapi import Request
9
- from langchain_auth.client import Client
10
+ from langgraph_sdk import get_client
10
11
  from pydantic import Field
11
12
 
12
13
  from langchain_triggers.core import (
13
- TriggerHandlerResult,
14
14
  TriggerRegistrationModel,
15
15
  TriggerRegistrationResult,
16
16
  TriggerType,
17
17
  )
18
18
  from langchain_triggers.decorators import TriggerTemplate
19
+ from langchain_triggers.util import create_service_auth_headers, get_langgraph_url
19
20
 
20
21
  logger = logging.getLogger(__name__)
21
22
 
@@ -34,7 +35,7 @@ class CronRegistration(TriggerRegistrationModel):
34
35
 
35
36
 
36
37
  async def cron_registration_handler(
37
- request: Request, user_id: str, auth_client: Client, registration: CronRegistration
38
+ request: Request, user_id: str, registration: CronRegistration
38
39
  ) -> TriggerRegistrationResult:
39
40
  """Handle cron trigger registration - validates cron pattern and prepares for scheduling."""
40
41
  logger.info(f"Cron registration request: {registration}")
@@ -78,18 +79,73 @@ async def cron_registration_handler(
78
79
  async def cron_poll_handler(
79
80
  registration: dict[str, Any],
80
81
  database,
81
- auth_client: Client,
82
- ) -> TriggerHandlerResult:
83
- """Polling handler for generic cron.
84
-
85
- Produces a simple time-based message for linked agents.
86
- """
87
- message = "Cron trigger fired"
88
- return TriggerHandlerResult(
89
- invoke_agent=True,
90
- agent_messages=[message],
91
- registration=registration,
92
- )
82
+ ) -> dict[str, Any]:
83
+ """Polling handler for generic cron - invokes agents directly."""
84
+ registration_id = registration["id"]
85
+ user_id = str(registration["user_id"])
86
+ tenant_id = str(registration.get("tenant_id", ""))
87
+
88
+ agent_links = await database.get_agents_for_trigger(registration_id)
89
+
90
+ if not agent_links:
91
+ logger.info(f"cron_no_linked_agents registration_id={registration_id}")
92
+ return {"success": True, "message": "No linked agents", "agents_invoked": 0}
93
+
94
+ langgraph_url = get_langgraph_url()
95
+
96
+ client = get_client(url=langgraph_url, api_key=None)
97
+ headers = create_service_auth_headers(user_id, tenant_id)
98
+
99
+ current_time = datetime.utcnow()
100
+ current_time_str = current_time.strftime("%A, %B %d, %Y at %H:%M UTC")
101
+
102
+ agents_invoked = 0
103
+ for agent_link in agent_links:
104
+ agent_id = str(
105
+ agent_link if isinstance(agent_link, str) else agent_link.get("agent_id")
106
+ )
107
+
108
+ try:
109
+ thread_id = str(uuid.uuid4())
110
+
111
+ try:
112
+ await client.threads.create(
113
+ thread_id=thread_id,
114
+ if_exists="do_nothing",
115
+ metadata={
116
+ "triggered_by": "cron-trigger",
117
+ "user_id": user_id,
118
+ "tenant_id": tenant_id,
119
+ "registration_id": str(registration_id),
120
+ },
121
+ headers=headers,
122
+ )
123
+ except Exception as thread_err:
124
+ logger.warning(f"cron_thread_create_failed thread_id={thread_id} error={str(thread_err)}")
125
+
126
+ await client.runs.create(
127
+ thread_id,
128
+ agent_id,
129
+ input={
130
+ "messages": [
131
+ {
132
+ "role": "human",
133
+ "content": f"Cron trigger fired at {current_time_str}",
134
+ }
135
+ ]
136
+ },
137
+ headers=headers,
138
+ )
139
+ logger.info(
140
+ f"cron_run_ok registration_id={registration_id} agent_id={agent_id} thread_id={thread_id}"
141
+ )
142
+ agents_invoked += 1
143
+ except Exception as e:
144
+ logger.error(
145
+ f"cron_run_err registration_id={registration_id} agent_id={agent_id} error={str(e)}"
146
+ )
147
+
148
+ return {"success": True, "agents_invoked": agents_invoked}
93
149
 
94
150
 
95
151
  cron_trigger = TriggerTemplate(
@@ -0,0 +1,73 @@
1
+ """Utility functions for trigger handlers."""
2
+
3
+ import os
4
+ from datetime import UTC, datetime, timedelta
5
+ from typing import Any
6
+
7
+ import jwt
8
+
9
+
10
+ def get_x_service_jwt_token(
11
+ payload: dict[str, Any] | None = None, expiration_seconds: int = 60 * 60
12
+ ) -> str:
13
+ """Create X-Service-Key JWT token for service-to-service authentication.
14
+
15
+ Args:
16
+ payload: Optional payload to include in JWT
17
+ expiration_seconds: Token expiration time in seconds (default 1 hour)
18
+
19
+ Returns:
20
+ JWT token string
21
+ """
22
+ exp_datetime = datetime.now(tz=UTC) + timedelta(seconds=expiration_seconds)
23
+ exp = int(exp_datetime.timestamp())
24
+
25
+ payload = payload or {}
26
+ payload = {
27
+ "sub": "unspecified",
28
+ "exp": exp,
29
+ **payload,
30
+ }
31
+
32
+ secret = os.environ["X_SERVICE_AUTH_JWT_SECRET"]
33
+
34
+ return jwt.encode(
35
+ payload,
36
+ secret,
37
+ algorithm="HS256",
38
+ )
39
+
40
+
41
+ def create_service_auth_headers(user_id: str, tenant_id: str) -> dict[str, str]:
42
+ """Create authentication headers with X-Service-Key JWT token.
43
+
44
+ Args:
45
+ user_id: User ID for the request
46
+ tenant_id: Tenant ID for the request
47
+
48
+ Returns:
49
+ Dictionary of authentication headers
50
+ """
51
+ headers = {
52
+ "x-api-key": "",
53
+ "x-auth-scheme": "langsmith-agent",
54
+ "x-user-id": user_id,
55
+ "x-tenant-id": tenant_id,
56
+ "x-service-key": get_x_service_jwt_token(
57
+ payload={
58
+ "tenant_id": tenant_id,
59
+ "user_id": user_id,
60
+ }
61
+ ),
62
+ }
63
+
64
+ return headers
65
+
66
+
67
+ def get_langgraph_url() -> str:
68
+ """Get LangGraph API URL from environment.
69
+
70
+ Returns:
71
+ LangGraph API URL
72
+ """
73
+ return os.environ.get("LANGGRAPH_API_URL", "http://localhost:8123")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langchain-trigger-server"
7
- version = "0.3.6"
7
+ version = "0.3.8"
8
8
  description = "Generic event-driven triggers framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -15,16 +15,14 @@ class _DummyRegModel(BaseModel):
15
15
  field: str | None = None
16
16
 
17
17
 
18
- async def _dummy_reg_handler(request, user_id, auth_client, registration):
18
+ async def _dummy_reg_handler(request, user_id, registration):
19
19
  from langchain_triggers.core import TriggerRegistrationResult
20
20
 
21
21
  return TriggerRegistrationResult()
22
22
 
23
23
 
24
- async def _dummy_trigger_handler(payload, query_params, database, auth_client):
25
- from langchain_triggers.core import TriggerHandlerResult
26
-
27
- return TriggerHandlerResult(invoke_agent=False, response_body={"ok": True})
24
+ async def _dummy_trigger_handler(request, database):
25
+ return {"ok": True}
28
26
 
29
27
 
30
28
  class _FakeDB:
@@ -30,9 +30,9 @@ class TestRegistration(BaseModel):
30
30
  name: str
31
31
 
32
32
 
33
- async def dummy_trigger_handler(payload, query_params, database, auth_client):
33
+ async def dummy_trigger_handler(request, database):
34
34
  """Dummy trigger handler for test triggers."""
35
- return None
35
+ return {"ok": True}
36
36
 
37
37
 
38
38
  # Mock database class
@@ -399,7 +399,7 @@ async def test_metadata_storage(trigger_server):
399
399
  """Test that handler metadata is properly stored."""
400
400
  from langchain_triggers import TriggerRegistrationResult, TriggerTemplate
401
401
 
402
- async def test_registration_handler(request, user_id, auth_client, registration):
402
+ async def test_registration_handler(request, user_id, registration):
403
403
  return TriggerRegistrationResult(metadata={"handler_data": "from_handler"})
404
404
 
405
405
  test_trigger = TriggerTemplate(
@@ -381,7 +381,7 @@ wheels = [
381
381
 
382
382
  [[package]]
383
383
  name = "langchain-trigger-server"
384
- version = "0.3.5"
384
+ version = "0.3.8"
385
385
  source = { editable = "." }
386
386
  dependencies = [
387
387
  { name = "apscheduler" },
@@ -0,0 +1 @@
1
+ Current: 0.3.8, PyPI: 0.3.7, Should publish: True
@@ -1,15 +0,0 @@
1
- """Authentication utilities for trigger webhooks."""
2
-
3
- from .slack_hmac import (
4
- SlackSignatureVerificationError,
5
- extract_slack_headers,
6
- get_slack_signing_secret,
7
- verify_slack_signature,
8
- )
9
-
10
- __all__ = [
11
- "verify_slack_signature",
12
- "get_slack_signing_secret",
13
- "extract_slack_headers",
14
- "SlackSignatureVerificationError",
15
- ]
@@ -1,90 +0,0 @@
1
- """Slack HMAC signature verification for webhook authentication.
2
-
3
- Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
4
- Each request includes an X-Slack-Signature header that must be verified
5
- against your app's signing secret.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import hashlib
11
- import hmac
12
- import logging
13
- import os
14
- import time
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- class SlackSignatureVerificationError(Exception):
20
- """Exception raised when Slack signature verification fails."""
21
-
22
- pass
23
-
24
-
25
- def verify_slack_signature(
26
- signing_secret: str,
27
- timestamp: str,
28
- body: str,
29
- signature: str,
30
- max_age_seconds: int = 300,
31
- ) -> bool:
32
- try:
33
- # Verify timestamp to prevent replay attacks
34
- current_time = int(time.time())
35
- request_time = int(timestamp)
36
-
37
- if abs(current_time - request_time) > max_age_seconds:
38
- logger.error(
39
- f"Slack request timestamp too old. "
40
- f"Current: {current_time}, Request: {request_time}, "
41
- f"Diff: {abs(current_time - request_time)}s"
42
- )
43
- raise SlackSignatureVerificationError(
44
- f"Request timestamp is too old (>{max_age_seconds}s). Possible replay attack."
45
- )
46
-
47
- # Format: v0:{timestamp}:{body}
48
- sig_basestring = f"v0:{timestamp}:{body}"
49
-
50
- # Create HMAC-SHA256 hash
51
- my_signature = (
52
- "v0="
53
- + hmac.new(
54
- signing_secret.encode(), sig_basestring.encode(), hashlib.sha256
55
- ).hexdigest()
56
- )
57
-
58
- if not hmac.compare_digest(my_signature, signature):
59
- logger.error(
60
- f"Slack signature mismatch. Expected: {my_signature}, Got: {signature}"
61
- )
62
- raise SlackSignatureVerificationError("Signature verification failed")
63
-
64
- logger.info("Successfully verified Slack webhook signature")
65
- return True
66
-
67
- except ValueError as e:
68
- logger.error(f"Invalid timestamp format: {e}")
69
- raise SlackSignatureVerificationError(f"Invalid timestamp: {str(e)}")
70
- except Exception as e:
71
- logger.error(f"Unexpected error during Slack signature verification: {e}")
72
- raise SlackSignatureVerificationError(f"Verification error: {str(e)}")
73
-
74
-
75
- def get_slack_signing_secret() -> str | None:
76
- """Get Slack signing secret from SLACK_SIGNING_SECRET environment variable."""
77
- secret = os.getenv("SLACK_SIGNING_SECRET")
78
- if not secret:
79
- logger.warning("SLACK_SIGNING_SECRET environment variable not set")
80
- return secret
81
-
82
-
83
- def extract_slack_headers(headers: dict) -> tuple[str | None, str | None]:
84
- """Extract Slack signature and timestamp from request headers."""
85
- signature = headers.get("x-slack-signature") or headers.get("X-Slack-Signature")
86
- timestamp = headers.get("x-slack-request-timestamp") or headers.get(
87
- "X-Slack-Request-Timestamp"
88
- )
89
-
90
- return signature, timestamp
@@ -1 +0,0 @@
1
- Current: 0.3.6, PyPI: 0.3.5, Should publish: True