langgraph-api 0.14.0.dev5__py3-none-any.whl → 0.14.0.dev7__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.
langgraph_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.14.0.dev5"
1
+ __version__ = "0.14.0.dev7"
@@ -0,0 +1,71 @@
1
+ """Data-plane JWT authentication for Beacon OTLP exports."""
2
+
3
+ import time
4
+ from datetime import UTC, datetime, timedelta
5
+ from typing import Any
6
+
7
+ import jwt
8
+ from requests.sessions import Session
9
+
10
+ DATA_PLANE_AUTH_HEADER = "X-Data-Plane-Auth"
11
+ BEACON_OTEL_COLLECTOR_PURPOSE = "beacon-otel-collector"
12
+
13
+ _BEACON_OTEL_JWT_TTL_SECONDS = 60 * 60
14
+ _BEACON_OTEL_JWT_REFRESH_BEFORE_SECONDS = 60
15
+
16
+
17
+ def sign_data_plane_jwt(data_plane_id: str, organization_id: str, secret: str) -> str:
18
+ now = datetime.now(UTC)
19
+ return jwt.encode(
20
+ {
21
+ "data_plane_id": data_plane_id,
22
+ "organization_id": organization_id,
23
+ "purpose": BEACON_OTEL_COLLECTOR_PURPOSE,
24
+ "iat": now,
25
+ "exp": now + timedelta(seconds=_BEACON_OTEL_JWT_TTL_SECONDS),
26
+ },
27
+ secret,
28
+ algorithm="HS256",
29
+ )
30
+
31
+
32
+ class _BeaconOtelJWTCache:
33
+ def __init__(self, data_plane_id: str, organization_id: str, secret: str) -> None:
34
+ self._data_plane_id = data_plane_id
35
+ self._organization_id = organization_id
36
+ self._secret = secret
37
+ self._cached_token: tuple[str, float] | None = None
38
+
39
+ def get(self) -> str:
40
+ cached_token = self._cached_token
41
+ if cached_token is not None:
42
+ token, refresh_at = cached_token
43
+ if time.monotonic() < refresh_at:
44
+ return token
45
+
46
+ token = sign_data_plane_jwt(
47
+ self._data_plane_id, self._organization_id, self._secret
48
+ )
49
+ refresh_at = time.monotonic() + (
50
+ _BEACON_OTEL_JWT_TTL_SECONDS - _BEACON_OTEL_JWT_REFRESH_BEFORE_SECONDS
51
+ )
52
+ self._cached_token = (token, refresh_at)
53
+ return token
54
+
55
+
56
+ class _DataPlaneAuthSession(Session):
57
+ def __init__(self, cache: _BeaconOtelJWTCache) -> None:
58
+ super().__init__()
59
+ self._cache = cache
60
+
61
+ def request(self, method: str, url: str, **kwargs: Any):
62
+ headers = dict(kwargs.pop("headers", None) or {})
63
+ headers[DATA_PLANE_AUTH_HEADER] = self._cache.get()
64
+ return super().request(method, url, headers=headers, **kwargs)
65
+
66
+
67
+ def beacon_otel_session(
68
+ data_plane_id: str, organization_id: str, secret: str
69
+ ) -> Session:
70
+ cache = _BeaconOtelJWTCache(data_plane_id, organization_id, secret)
71
+ return _DataPlaneAuthSession(cache)
@@ -614,6 +614,14 @@ DATADOG_METRICS_ENABLED = bool(LSD_DD_API_KEY)
614
614
  EXPOSE_INTERNAL_METRICS_PROMETHEUS = env(
615
615
  "EXPOSE_INTERNAL_METRICS_PROMETHEUS", cast=bool, default=False
616
616
  )
617
+
618
+ # These are only set for internal agents (polly/insights/engine/etc..) deployed in byoc so we can fetch logs (see api/langgraph_api/self_hosted_logs.py)
619
+ DATA_PLANE_ID: str | None = env("DATA_PLANE_ID", cast=str, default=None)
620
+ DATA_PLANE_ORG_ID: str | None = env("DATA_PLANE_ORG_ID", cast=str, default=None)
621
+ DATA_PLANE_JWT_SECRET: str | None = env("DATA_PLANE_JWT_SECRET", cast=str, default=None)
622
+ BYOC_HOST = env("BYOC_HOST", cast=str, default="byoc")
623
+ BYOC_ENV = env("BYOC_ENV", cast=str, default="byoc")
624
+
617
625
  LANGGRAPH_LOGS_ENDPOINT = env("LANGGRAPH_LOGS_ENDPOINT", cast=str, default=None)
618
626
  LANGGRAPH_LOGS_ENABLED = env("LANGGRAPH_LOGS_ENABLED", cast=bool, default=False)
619
627
 
@@ -633,7 +641,9 @@ FF_PROFILE_IMPORTS = env("FF_PROFILE_IMPORTS", cast=bool, default=False)
633
641
 
634
642
  JS_READY_TIMEOUT_SECS = env("LANGGRAPH_JS_READY_TIMEOUT_SECS", cast=int, default=120)
635
643
 
636
- SELF_HOSTED_OBSERVABILITY_SERVICE_NAME = "LGP_Self_Hosted"
644
+ SELF_HOSTED_OBSERVABILITY_SERVICE_NAME = env(
645
+ "SELF_HOSTED_OBSERVABILITY_SERVICE_NAME", cast=str, default="LGP_Self_Hosted"
646
+ )
637
647
 
638
648
  IS_QUEUE_ENTRYPOINT = False
639
649
  IS_EXECUTOR_ENTRYPOINT = False
@@ -660,6 +670,8 @@ __all__ = [
660
670
  "BG_JOB_MAX_RETRIES",
661
671
  "BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS",
662
672
  "BG_JOB_TIMEOUT_SECS",
673
+ "BYOC_ENV",
674
+ "BYOC_HOST",
663
675
  "CHECKPOINTER_CONFIG",
664
676
  "CHECKPOINTER_POSTGRES_POOL_MIN_SIZE",
665
677
  "CHECKPOINTER_POSTGRES_POOL_TIMEOUT_SECONDS",
@@ -670,6 +682,9 @@ __all__ = [
670
682
  "CRON_SCHEDULER_SLEEP_TIME",
671
683
  "DATABASE_URI",
672
684
  "DATADOG_METRICS_ENABLED",
685
+ "DATA_PLANE_ID",
686
+ "DATA_PLANE_JWT_SECRET",
687
+ "DATA_PLANE_ORG_ID",
673
688
  "DB_MIGRATION_BY_CORE_API",
674
689
  "EXPOSE_INTERNAL_METRICS_PROMETHEUS",
675
690
  "FF_CRONS_ENABLED",
@@ -10,6 +10,7 @@ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
10
10
  from opentelemetry.util.types import Attributes
11
11
 
12
12
  from langgraph_api import config
13
+ from langgraph_api.beacon_data_plane_jwt_auth import beacon_otel_session
13
14
  from langgraph_api.logging import OTLPFormatter
14
15
 
15
16
  logger = structlog.stdlib.get_logger(__name__)
@@ -45,10 +46,27 @@ def initialize_self_hosted_logs() -> None:
45
46
  raise RuntimeError(
46
47
  "LANGGRAPH_LOGS_ENABLED is true but no LANGGRAPH_LOGS_ENDPOINT is configured"
47
48
  )
49
+ log_endpoint = config.LANGGRAPH_LOGS_ENDPOINT
50
+
51
+ data_plane_settings = (
52
+ ("DATA_PLANE_ID", config.DATA_PLANE_ID),
53
+ ("DATA_PLANE_ORG_ID", config.DATA_PLANE_ORG_ID),
54
+ ("DATA_PLANE_JWT_SECRET", config.DATA_PLANE_JWT_SECRET),
55
+ )
56
+ has_data_plane_settings = any(value for _, value in data_plane_settings)
57
+ missing_data_plane_settings = [
58
+ name for name, value in data_plane_settings if not value
59
+ ]
60
+ if has_data_plane_settings and missing_data_plane_settings:
61
+ raise RuntimeError(
62
+ "LANGGRAPH_LOGS_ENABLED is true but required data plane settings are missing: "
63
+ + ", ".join(missing_data_plane_settings)
64
+ )
48
65
 
49
- # For now, this is only enabled for fully self-hosted customers
50
- # We will need to update the otel collector auth model to support hybrid customers
51
- if not config.LANGGRAPH_CLOUD_LICENSE_KEY:
66
+ # "data-plane" here refers to a BYOC data plane
67
+ is_data_plane = has_data_plane_settings
68
+ # For now, the non-data-plane path is only enabled for fully self-hosted customers.
69
+ if not is_data_plane and not config.LANGGRAPH_CLOUD_LICENSE_KEY:
52
70
  logger.warning(
53
71
  "Self-hosted logs require a license key, and do not work with hybrid deployments yet."
54
72
  )
@@ -58,6 +76,13 @@ def initialize_self_hosted_logs() -> None:
58
76
  resource_attributes = {
59
77
  SERVICE_NAME: config.SELF_HOSTED_OBSERVABILITY_SERVICE_NAME,
60
78
  }
79
+ if is_data_plane:
80
+ resource_attributes["data_plane_id"] = cast("str", config.DATA_PLANE_ID)
81
+ resource_attributes["organization_id"] = cast(
82
+ "str", config.DATA_PLANE_ORG_ID
83
+ )
84
+ resource_attributes["host"] = config.BYOC_HOST
85
+ resource_attributes["env"] = config.BYOC_ENV
61
86
 
62
87
  if config.LANGGRAPH_CLOUD_LICENSE_KEY:
63
88
  try:
@@ -70,43 +95,47 @@ def initialize_self_hosted_logs() -> None:
70
95
  _customer_attributes["customer_id"] = CUSTOMER_ID
71
96
  if CUSTOMER_NAME:
72
97
  _customer_attributes["customer_name"] = CUSTOMER_NAME
73
-
74
- # resolves to pod name in k8s, or container id in docker
75
- instance_id = os.environ.get("HOSTNAME")
76
- if instance_id:
77
- _customer_attributes["instance_id"] = instance_id
78
98
  except ImportError:
79
99
  pass
80
100
  except Exception as e:
81
101
  logger.warning("Failed to get customer info from license", exc_info=e)
82
102
 
103
+ # Resolves to pod name in k8s, or container ID in Docker.
104
+ instance_id = os.environ.get("HOSTNAME")
105
+ if instance_id:
106
+ _customer_attributes["instance_id"] = instance_id
107
+
83
108
  if config.IS_QUEUE_ENTRYPOINT:
84
109
  _customer_attributes["entrypoint"] = "queue"
85
- elif config.IS_EXECUTOR_ENTRYPOINT:
86
- _customer_attributes["entrypoint"] = "executor"
87
110
  else:
88
111
  _customer_attributes["entrypoint"] = "api"
89
112
 
90
- _logger_provider = LoggerProvider(
91
- resource=Resource.create(resource_attributes),
92
- )
93
- _logger_provider.add_log_record_processor(
94
- BatchLogRecordProcessor(
95
- OTLPLogExporter(
96
- endpoint=config.LANGGRAPH_LOGS_ENDPOINT,
97
- headers={
98
- "X-Langchain-License-Key": config.LANGGRAPH_CLOUD_LICENSE_KEY,
99
- },
100
- )
113
+ if is_data_plane:
114
+ exporter = OTLPLogExporter(
115
+ endpoint=log_endpoint,
116
+ session=beacon_otel_session(
117
+ cast("str", config.DATA_PLANE_ID),
118
+ cast("str", config.DATA_PLANE_ORG_ID),
119
+ cast("str", config.DATA_PLANE_JWT_SECRET),
120
+ ),
101
121
  )
102
- )
122
+ else:
123
+ exporter = OTLPLogExporter(
124
+ endpoint=log_endpoint,
125
+ headers={
126
+ "X-Langchain-License-Key": config.LANGGRAPH_CLOUD_LICENSE_KEY,
127
+ },
128
+ )
129
+
130
+ _logger_provider = LoggerProvider(resource=Resource.create(resource_attributes))
131
+ _logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
103
132
  handler = AttrFilteredLoggingHandler(logger_provider=_logger_provider)
104
133
  handler.setFormatter(OTLPFormatter("%(message)s", None, "%"))
105
134
  logging.getLogger().addHandler(handler)
106
135
 
107
136
  logger.info(
108
137
  "Self-hosted logs initialized successfully",
109
- endpoint=config.LANGGRAPH_LOGS_ENDPOINT,
138
+ endpoint=log_endpoint,
110
139
  )
111
140
  except Exception as e:
112
141
  logger.exception("Failed to initialize self-hosted logs", exc_info=e)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.14.0.dev5
3
+ Version: 0.14.0.dev7
4
4
  Author-email: Will Fu-Hinthorn <will@langchain.dev>, Josh Rogers <josh@langchain.dev>, Parker Rule <parker@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -1,7 +1,8 @@
1
- langgraph_api/__init__.py,sha256=NBG1Mp6KRQPil9UZH10UybVbzGuSikFTeHud5X9p9SI,28
1
+ langgraph_api/__init__.py,sha256=peIxw98li2d4NZ6L-KBi3u4nBpoo1ehyNTk3sSEGSWg,28
2
2
  langgraph_api/_factory_utils.py,sha256=_FqW0olMu2PGLsnRq0ylt3a4jDCe12YIFHExMXI6Nyo,5976
3
3
  langgraph_api/asgi_transport.py,sha256=XApY3lIWBZTMbbsl8dDJzl0cLGirmAGE0SifqZUnXvs,11896
4
4
  langgraph_api/asyncio.py,sha256=smqyAoO9nIyPPQRw1IVSCh85fwoZP4PEo-5dDhxGruc,10676
5
+ langgraph_api/beacon_data_plane_jwt_auth.py,sha256=SoX2M-_u7oas6zWyss2LBs83E6ynBCFOoD5vVJEHRSU,2297
5
6
  langgraph_api/cache.py,sha256=LuB3Te0UdXC8a-uEJoRHoe5XsgXulXI59f_Q6DCJNKc,13005
6
7
  langgraph_api/cli.py,sha256=ATtS9s9Cx7QNiGPJceKnMCko29A25ZA-xz39fdxmgfg,22389
7
8
  langgraph_api/command.py,sha256=d-k8h6H4ix1n7fSZ-Zb01NbSkEyqrD6cMKfDFXEIYEw,821
@@ -22,7 +23,7 @@ langgraph_api/queue_entrypoint.py,sha256=Bh8YPWN_2Dj3hxnYE9T3w9Igc8bIq7UcT8dngOO
22
23
  langgraph_api/release_tags.py,sha256=Dd2L2Mdh7c5D7RhFvdGCXROLH3WI1QKXbEqgjITFFoQ,9437
23
24
  langgraph_api/route.py,sha256=_KE8A8Q-J-QfqjGlyM2Kc6n5cirmgt8xmI5-pI8kVEE,8837
24
25
  langgraph_api/schema.py,sha256=EkarLwAUKApevb03AmLRumrAVAc2EsG_SLhYFPncSzk,15318
25
- langgraph_api/self_hosted_logs.py,sha256=FoUkPdtpt-nuEhejne8o1Q2phE9CccoHdoR_PvXPcBU,4442
26
+ langgraph_api/self_hosted_logs.py,sha256=rzzr9iVwJgNbDHjrxvuTODCtzpdr9-9X6UAB-3OaXU0,5742
26
27
  langgraph_api/self_hosted_metrics.py,sha256=pWsQQ-2ukoFIbmVfzNOSkwCqZ5Cnts6pRSWTII44Ll4,16844
27
28
  langgraph_api/serde.py,sha256=V3fO9bkUOlBX3okw5Qi31nlcr59fcuXMgL7DHNyarZY,8855
28
29
  langgraph_api/server.py,sha256=R0z4R6DjnRwsqjDOjWD-7R3nKoQcS6v7UriG1LuzA1o,11213
@@ -65,7 +66,7 @@ langgraph_api/auth/studio_user.py,sha256=gNCicIo6cYaHmFj2sEdsvDYkKW7NWfGXGS2tTAM
65
66
  langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
67
  langgraph_api/auth/langsmith/backend.py,sha256=Y6-VxD7zfV1jzGdjmQ66CgNa3SenLbo3d_375CcKZ9U,3770
67
68
  langgraph_api/auth/langsmith/client.py,sha256=79kwCVeHU64nsHsxWipfZhf44lM6vfs2nlfTxlJF6LU,4142
68
- langgraph_api/config/__init__.py,sha256=w4YeHOZqqK-v4x0O45Fq8GVkF1NUycxTuzsF6pYqI24,26789
69
+ langgraph_api/config/__init__.py,sha256=R3WTyMb96Q-UsrZIY-mvZrtJvkrriSUSA0LVYR3vhXA,27474
69
70
  langgraph_api/config/_parse.py,sha256=RpfZgFAPJlRMv13AzGr3kAYbIrqHcgjzO8IgeboTw4A,3922
70
71
  langgraph_api/config/schemas.py,sha256=cHzVvepthpD7DDeWE2ytkOHah-iDNH7xRx9dSWUatQI,20864
71
72
  langgraph_api/encryption/__init__.py,sha256=gaCZ00CocSbqSqrDn6XJHaSp2CZCnC8qnrD9G4fbzyI,363
@@ -237,8 +238,8 @@ langgraph_grpc_common/proto/store_pb2.py,sha256=thQ5HBLblZ7Adayp1U8ty_lrpn-7XEHn
237
238
  langgraph_grpc_common/proto/store_pb2.pyi,sha256=_dcxyxV8moR6H67qU42r2h2p_Ub-PZSHs3OoS9KMdSY,27782
238
239
  langgraph_grpc_common/proto/store_pb2_grpc.py,sha256=Kyt8eV2NXkjh3be_BYytGdD19kNf74961cruonMvCrw,3300
239
240
  langgraph_grpc_common/proto/store_pb2_grpc.pyi,sha256=oS1h2cDoq2OjyM8xazcmc8LwRDC5oCAD0zto2QUmPQw,2027
240
- langgraph_api-0.14.0.dev5.dist-info/METADATA,sha256=8qCqMKKf7BpdGKHc6krTm4b1Mri_RHof9XKR1u23aN0,4630
241
- langgraph_api-0.14.0.dev5.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
242
- langgraph_api-0.14.0.dev5.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
243
- langgraph_api-0.14.0.dev5.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
244
- langgraph_api-0.14.0.dev5.dist-info/RECORD,,
241
+ langgraph_api-0.14.0.dev7.dist-info/METADATA,sha256=Q9FxNfaj68qfyoo7tYg4Tcfb4vH1DFi7Ssn_LlHcDNA,4630
242
+ langgraph_api-0.14.0.dev7.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
243
+ langgraph_api-0.14.0.dev7.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
244
+ langgraph_api-0.14.0.dev7.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
245
+ langgraph_api-0.14.0.dev7.dist-info/RECORD,,