langgraph-api 0.4.1__py3-none-any.whl → 0.7.3__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.
Files changed (135) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/__init__.py +111 -51
  3. langgraph_api/api/a2a.py +1610 -0
  4. langgraph_api/api/assistants.py +212 -89
  5. langgraph_api/api/mcp.py +3 -3
  6. langgraph_api/api/meta.py +52 -28
  7. langgraph_api/api/openapi.py +27 -17
  8. langgraph_api/api/profile.py +108 -0
  9. langgraph_api/api/runs.py +342 -195
  10. langgraph_api/api/store.py +19 -2
  11. langgraph_api/api/threads.py +209 -27
  12. langgraph_api/asgi_transport.py +14 -9
  13. langgraph_api/asyncio.py +14 -4
  14. langgraph_api/auth/custom.py +52 -37
  15. langgraph_api/auth/langsmith/backend.py +4 -3
  16. langgraph_api/auth/langsmith/client.py +13 -8
  17. langgraph_api/cli.py +230 -133
  18. langgraph_api/command.py +5 -3
  19. langgraph_api/config/__init__.py +532 -0
  20. langgraph_api/config/_parse.py +58 -0
  21. langgraph_api/config/schemas.py +431 -0
  22. langgraph_api/cron_scheduler.py +17 -1
  23. langgraph_api/encryption/__init__.py +15 -0
  24. langgraph_api/encryption/aes_json.py +158 -0
  25. langgraph_api/encryption/context.py +35 -0
  26. langgraph_api/encryption/custom.py +280 -0
  27. langgraph_api/encryption/middleware.py +632 -0
  28. langgraph_api/encryption/shared.py +63 -0
  29. langgraph_api/errors.py +12 -1
  30. langgraph_api/executor_entrypoint.py +11 -6
  31. langgraph_api/feature_flags.py +29 -0
  32. langgraph_api/graph.py +176 -76
  33. langgraph_api/grpc/client.py +313 -0
  34. langgraph_api/grpc/config_conversion.py +231 -0
  35. langgraph_api/grpc/generated/__init__.py +29 -0
  36. langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
  37. langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
  38. langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
  39. langgraph_api/grpc/generated/core_api_pb2.py +216 -0
  40. langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
  41. langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
  42. langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
  43. langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
  44. langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
  45. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
  46. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
  47. langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
  48. langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
  49. langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
  50. langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
  51. langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
  52. langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
  53. langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
  54. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
  55. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
  56. langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
  57. langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
  58. langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
  59. langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
  60. langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
  61. langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
  62. langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
  63. langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
  64. langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
  65. langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
  66. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
  67. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
  68. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
  69. langgraph_api/grpc/generated/errors_pb2.py +39 -0
  70. langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
  71. langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
  72. langgraph_api/grpc/ops/__init__.py +370 -0
  73. langgraph_api/grpc/ops/assistants.py +424 -0
  74. langgraph_api/grpc/ops/runs.py +792 -0
  75. langgraph_api/grpc/ops/threads.py +1013 -0
  76. langgraph_api/http.py +16 -5
  77. langgraph_api/http_metrics.py +15 -35
  78. langgraph_api/http_metrics_utils.py +38 -0
  79. langgraph_api/js/build.mts +1 -1
  80. langgraph_api/js/client.http.mts +13 -7
  81. langgraph_api/js/client.mts +2 -5
  82. langgraph_api/js/package.json +29 -28
  83. langgraph_api/js/remote.py +56 -30
  84. langgraph_api/js/src/graph.mts +20 -0
  85. langgraph_api/js/sse.py +2 -2
  86. langgraph_api/js/ui.py +1 -1
  87. langgraph_api/js/yarn.lock +1204 -1006
  88. langgraph_api/logging.py +29 -2
  89. langgraph_api/metadata.py +99 -28
  90. langgraph_api/middleware/http_logger.py +7 -2
  91. langgraph_api/middleware/private_network.py +7 -7
  92. langgraph_api/models/run.py +54 -93
  93. langgraph_api/otel_context.py +205 -0
  94. langgraph_api/patch.py +5 -3
  95. langgraph_api/queue_entrypoint.py +154 -65
  96. langgraph_api/route.py +47 -5
  97. langgraph_api/schema.py +88 -10
  98. langgraph_api/self_hosted_logs.py +124 -0
  99. langgraph_api/self_hosted_metrics.py +450 -0
  100. langgraph_api/serde.py +79 -37
  101. langgraph_api/server.py +138 -60
  102. langgraph_api/state.py +4 -3
  103. langgraph_api/store.py +25 -16
  104. langgraph_api/stream.py +80 -29
  105. langgraph_api/thread_ttl.py +31 -13
  106. langgraph_api/timing/__init__.py +25 -0
  107. langgraph_api/timing/profiler.py +200 -0
  108. langgraph_api/timing/timer.py +318 -0
  109. langgraph_api/utils/__init__.py +53 -8
  110. langgraph_api/utils/cache.py +47 -10
  111. langgraph_api/utils/config.py +2 -1
  112. langgraph_api/utils/errors.py +77 -0
  113. langgraph_api/utils/future.py +10 -6
  114. langgraph_api/utils/headers.py +76 -2
  115. langgraph_api/utils/retriable_client.py +74 -0
  116. langgraph_api/utils/stream_codec.py +315 -0
  117. langgraph_api/utils/uuids.py +29 -62
  118. langgraph_api/validation.py +9 -0
  119. langgraph_api/webhook.py +120 -6
  120. langgraph_api/worker.py +55 -24
  121. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
  122. langgraph_api-0.7.3.dist-info/RECORD +168 -0
  123. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
  124. langgraph_runtime/__init__.py +1 -0
  125. langgraph_runtime/routes.py +11 -0
  126. logging.json +1 -3
  127. openapi.json +839 -478
  128. langgraph_api/config.py +0 -387
  129. langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
  130. langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
  131. langgraph_api/js/package-lock.json +0 -3308
  132. langgraph_api-0.4.1.dist-info/RECORD +0 -107
  133. /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
  134. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
  135. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
langgraph_api/schema.py CHANGED
@@ -16,9 +16,19 @@ RunStatus = Literal["pending", "running", "error", "success", "timeout", "interr
16
16
  ThreadStatus = Literal["idle", "busy", "interrupted", "error"]
17
17
 
18
18
  StreamMode = Literal[
19
- "values", "messages", "updates", "events", "debug", "tasks", "checkpoints", "custom"
19
+ "values",
20
+ "messages",
21
+ "updates",
22
+ "events",
23
+ "debug",
24
+ "tasks",
25
+ "checkpoints",
26
+ "custom",
27
+ "messages-tuple",
20
28
  ]
21
29
 
30
+ ThreadStreamMode = Literal["lifecycle", "run_modes", "state_update"]
31
+
22
32
  MultitaskStrategy = Literal["reject", "rollback", "interrupt", "enqueue"]
23
33
 
24
34
  OnConflictBehavior = Literal["raise", "do_nothing"]
@@ -48,10 +58,16 @@ class Config(TypedDict, total=False):
48
58
  """
49
59
  Runtime values for attributes previously made configurable on this Runnable,
50
60
  or sub-Runnables, through .configurable_fields() or .configurable_alternatives().
51
- Check .output_schema() for a description of the attributes that have been made
61
+ Check .output_schema() for a description of the attributes that have been made
52
62
  configurable.
53
63
  """
54
64
 
65
+ __encryption_context__: dict[str, Any]
66
+ """
67
+ Internal: Encryption context for encryption/decryption operations.
68
+ Not exposed to users.
69
+ """
70
+
55
71
 
56
72
  class Checkpoint(TypedDict):
57
73
  thread_id: str
@@ -60,7 +76,7 @@ class Checkpoint(TypedDict):
60
76
  checkpoint_map: dict[str, Any] | None
61
77
 
62
78
 
63
- class Assistant(TypedDict):
79
+ class Assistant(TypedDict, total=False):
64
80
  """Assistant model."""
65
81
 
66
82
  assistant_id: UUID
@@ -110,6 +126,17 @@ class DeprecatedInterrupt(TypedDict, total=False):
110
126
  """When the interrupt occurred, always "during"."""
111
127
 
112
128
 
129
+ class ThreadTTLInfo(TypedDict, total=False):
130
+ """TTL information for a thread. Only present when ?include=ttl is passed."""
131
+
132
+ strategy: Literal["delete", "keep_latest"]
133
+ """The TTL strategy."""
134
+ ttl_minutes: float
135
+ """The TTL in minutes."""
136
+ expires_at: datetime
137
+ """When the thread will expire."""
138
+
139
+
113
140
  class Thread(TypedDict):
114
141
  thread_id: UUID
115
142
  """The ID of the thread."""
@@ -121,14 +148,14 @@ class Thread(TypedDict):
121
148
  """The thread metadata."""
122
149
  config: Fragment
123
150
  """The thread config."""
124
- context: Fragment
125
- """The thread context."""
126
151
  status: ThreadStatus
127
152
  """The status of the thread. One of 'idle', 'busy', 'interrupted', "error"."""
128
153
  values: Fragment
129
154
  """The current state of the thread."""
130
155
  interrupts: dict[str, list[Interrupt]]
131
156
  """The current interrupts of the thread, a map of task_id to list of interrupts."""
157
+ ttl: NotRequired[ThreadTTLInfo]
158
+ """TTL information if set for this thread. Only present when ?include=ttl is passed."""
132
159
 
133
160
 
134
161
  class ThreadTask(TypedDict):
@@ -146,7 +173,7 @@ class ThreadState(TypedDict):
146
173
  next: Sequence[str]
147
174
  """The name of the node to execute in each task for this step."""
148
175
  checkpoint: Checkpoint
149
- """The checkpoint keys. This object can be passed to the /threads and /runs
176
+ """The checkpoint keys. This object can be passed to the /threads and /runs
150
177
  endpoints to resume execution or update state."""
151
178
  metadata: Fragment
152
179
  """Metadata for this state"""
@@ -174,6 +201,7 @@ class RunKwargs(TypedDict):
174
201
  subgraphs: bool
175
202
  resumable: bool
176
203
  checkpoint_during: bool
204
+ durability: str | None
177
205
 
178
206
 
179
207
  class Run(TypedDict):
@@ -217,6 +245,8 @@ class Cron(TypedDict):
217
245
  """The ID of the assistant."""
218
246
  thread_id: UUID | None
219
247
  """The ID of the thread."""
248
+ on_run_completed: NotRequired[Literal["delete", "keep"] | None]
249
+ """What to do with the thread after the run completes."""
220
250
  end_time: datetime | None
221
251
  """The end date to stop running the cron."""
222
252
  schedule: str
@@ -246,8 +276,9 @@ class ThreadUpdateResponse(TypedDict):
246
276
  class QueueStats(TypedDict):
247
277
  n_pending: int
248
278
  n_running: int
249
- max_age_secs: datetime | None
250
- med_age_secs: datetime | None
279
+ pending_runs_wait_time_max_secs: float | None
280
+ pending_runs_wait_time_med_secs: float | None
281
+ pending_unblocked_runs_wait_time_max_secs: float | None
251
282
 
252
283
 
253
284
  # Canonical field sets for select= validation and type aliases for ops
@@ -274,7 +305,6 @@ ThreadSelectField = Literal[
274
305
  "updated_at",
275
306
  "metadata",
276
307
  "config",
277
- "context",
278
308
  "status",
279
309
  "values",
280
310
  "interrupts",
@@ -300,6 +330,7 @@ CronSelectField = Literal[
300
330
  "cron_id",
301
331
  "assistant_id",
302
332
  "thread_id",
333
+ "on_run_completed",
303
334
  "end_time",
304
335
  "schedule",
305
336
  "created_at",
@@ -308,6 +339,53 @@ CronSelectField = Literal[
308
339
  "payload",
309
340
  "next_run_date",
310
341
  "metadata",
311
- "now",
312
342
  ]
313
343
  CRON_FIELDS: set[str] = set(CronSelectField.__args__) # type: ignore[attr-defined]
344
+
345
+ # Encryption field constants
346
+ # These define which fields are encrypted for each model type.
347
+ #
348
+ # Note: Checkpoint encryption (checkpoint, metadata columns in checkpoints table, plus
349
+ # blob data in checkpoint_blobs and checkpoint_writes) is handled directly by the
350
+ # Checkpointer class in storage_postgres/langgraph_runtime_postgres/checkpoint.py.
351
+ # The checkpointer uses encrypt_json_if_needed/decrypt_json_if_needed directly rather
352
+ # than the field list pattern used by the API middleware. This is because checkpoints
353
+ # are only accessed via the checkpointer's internal methods (aget_tuple, aput, etc.),
354
+ # not through generic API CRUD operations.
355
+
356
+ THREAD_ENCRYPTION_FIELDS = ["metadata", "config", "values", "interrupts", "error"]
357
+
358
+ # kwargs is a nested blob - its subfields are decrypted automatically by the middleware
359
+ RUN_ENCRYPTION_FIELDS = ["metadata", "kwargs"]
360
+
361
+ ASSISTANT_ENCRYPTION_FIELDS = ["metadata", "config", "context"]
362
+
363
+ # payload is a nested blob - its subfields are decrypted automatically by the middleware
364
+ CRON_ENCRYPTION_FIELDS = ["metadata", "payload"]
365
+
366
+ # Store encryption - only the value field contains user data
367
+ STORE_ENCRYPTION_FIELDS = ["value"]
368
+
369
+ # The middleware automatically decrypts these subfields when decrypting the parent field.
370
+ # This is recursive: if a subfield is also in NESTED_ENCRYPTED_SUBFIELDS, its subfields
371
+ # are decrypted too (e.g., run.kwargs.config.configurable).
372
+ NESTED_ENCRYPTED_SUBFIELDS: dict[tuple[str, str], list[str]] = {
373
+ ("run", "kwargs"): ["input", "config", "context", "command"],
374
+ ("run", "config"): ["configurable", "metadata"],
375
+ ("cron", "payload"): ["metadata", "context", "input", "config"],
376
+ ("cron", "config"): ["configurable", "metadata"],
377
+ ("assistant", "config"): ["configurable"],
378
+ }
379
+
380
+ # Convenience alias for cron payload subfields.
381
+ #
382
+ # This is a reflection of an unfortunate asymmetry in cron's data model.
383
+ #
384
+ # The cron API requests have payload fields (metadata, input, config, context) at the
385
+ # top level, but at rest they're nested inside the `payload` JSONB column (with
386
+ # metadata also duplicated as a top-level column). This alias is used to encrypt
387
+ # those fields in the flat request before storage.
388
+ CRON_PAYLOAD_ENCRYPTION_SUBFIELDS = NESTED_ENCRYPTED_SUBFIELDS[("cron", "payload")]
389
+
390
+ # Convenience alias for run kwargs subfields, used by the worker for decryption.
391
+ RUN_KWARGS_ENCRYPTION_SUBFIELDS = NESTED_ENCRYPTED_SUBFIELDS[("run", "kwargs")]
@@ -0,0 +1,124 @@
1
+ import logging
2
+ import os
3
+ from typing import cast
4
+
5
+ import structlog
6
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
7
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
8
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
9
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
10
+ from opentelemetry.util.types import Attributes
11
+
12
+ from langgraph_api import config
13
+ from langgraph_api.logging import OTLPFormatter
14
+
15
+ logger = structlog.stdlib.get_logger(__name__)
16
+
17
+ _logger_provider = None
18
+ _customer_attributes = {}
19
+
20
+
21
+ # see https://github.com/open-telemetry/opentelemetry-python/issues/3649 for why we need this
22
+ class AttrFilteredLoggingHandler(LoggingHandler):
23
+ DROP_ATTRIBUTES = ("_logger",)
24
+
25
+ @staticmethod
26
+ def _get_attributes(record: logging.LogRecord) -> Attributes:
27
+ base_attributes = LoggingHandler._get_attributes(record)
28
+ attributes = {
29
+ k: v
30
+ for k, v in base_attributes.items()
31
+ if k not in AttrFilteredLoggingHandler.DROP_ATTRIBUTES
32
+ }
33
+ if _customer_attributes:
34
+ attributes.update(_customer_attributes)
35
+ return cast("Attributes", attributes)
36
+
37
+
38
+ def initialize_self_hosted_logs() -> None:
39
+ global _logger_provider
40
+
41
+ if not config.LANGGRAPH_LOGS_ENABLED:
42
+ return
43
+
44
+ if not config.LANGGRAPH_LOGS_ENDPOINT:
45
+ raise RuntimeError(
46
+ "LANGGRAPH_LOGS_ENABLED is true but no LANGGRAPH_LOGS_ENDPOINT is configured"
47
+ )
48
+
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:
52
+ logger.warning(
53
+ "Self-hosted logs require a license key, and do not work with hybrid deployments yet."
54
+ )
55
+ return
56
+
57
+ try:
58
+ resource_attributes = {
59
+ SERVICE_NAME: config.SELF_HOSTED_OBSERVABILITY_SERVICE_NAME,
60
+ }
61
+
62
+ if config.LANGGRAPH_CLOUD_LICENSE_KEY:
63
+ try:
64
+ from langgraph_license.validation import (
65
+ CUSTOMER_ID, # type: ignore[unresolved-import]
66
+ CUSTOMER_NAME, # type: ignore[unresolved-import]
67
+ )
68
+
69
+ if CUSTOMER_ID:
70
+ _customer_attributes["customer_id"] = CUSTOMER_ID
71
+ if CUSTOMER_NAME:
72
+ _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
+ except ImportError:
79
+ pass
80
+ except Exception as e:
81
+ logger.warning("Failed to get customer info from license", exc_info=e)
82
+
83
+ if config.IS_QUEUE_ENTRYPOINT:
84
+ _customer_attributes["entrypoint"] = "queue"
85
+ elif config.IS_EXECUTOR_ENTRYPOINT:
86
+ _customer_attributes["entrypoint"] = "executor"
87
+ else:
88
+ _customer_attributes["entrypoint"] = "api"
89
+
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
+ )
101
+ )
102
+ )
103
+ handler = AttrFilteredLoggingHandler(logger_provider=_logger_provider)
104
+ handler.setFormatter(OTLPFormatter("%(message)s", None, "%"))
105
+ logging.getLogger().addHandler(handler)
106
+
107
+ logger.info(
108
+ "Self-hosted logs initialized successfully",
109
+ endpoint=config.LANGGRAPH_LOGS_ENDPOINT,
110
+ )
111
+ except Exception as e:
112
+ logger.exception("Failed to initialize self-hosted logs", exc_info=e)
113
+
114
+
115
+ def shutdown_self_hosted_logs() -> None:
116
+ global _logger_provider
117
+
118
+ if _logger_provider:
119
+ try:
120
+ logger.info("Shutting down self-hosted logs")
121
+ _logger_provider.shutdown()
122
+ _logger_provider = None
123
+ except Exception as e:
124
+ logger.exception("Failed to shutdown self-hosted logs", exc_info=e)