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
@@ -0,0 +1,24 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+
7
+ GRPC_GENERATED_VERSION = '1.75.1'
8
+ GRPC_VERSION = grpc.__version__
9
+ _version_not_supported = False
10
+
11
+ try:
12
+ from grpc._utilities import first_version_is_lower
13
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
14
+ except ImportError:
15
+ _version_not_supported = True
16
+
17
+ if _version_not_supported:
18
+ raise RuntimeError(
19
+ f'The grpc package installed is at version {GRPC_VERSION},'
20
+ + f' but the generated code in enum_thread_stream_mode_pb2_grpc.py depends on'
21
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
22
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
23
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
24
+ )
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: errors.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'errors.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x65rrors.proto\x12\x06\x65rrors\"V\n\x16UserCodeExecutionError\x12\x12\n\nerror_type\x18\x01 \x01(\t\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x11\n\ttraceback\x18\x03 \x01(\t\")\n\x18GraphRecursionLimitError\x12\r\n\x05limit\x18\x01 \x01(\x05\x42<Z:github.com/langchain-ai/langgraph-api/core/internal/errorsb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'errors_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ _globals['DESCRIPTOR']._loaded_options = None
34
+ _globals['DESCRIPTOR']._serialized_options = b'Z:github.com/langchain-ai/langgraph-api/core/internal/errors'
35
+ _globals['_USERCODEEXECUTIONERROR']._serialized_start=24
36
+ _globals['_USERCODEEXECUTIONERROR']._serialized_end=110
37
+ _globals['_GRAPHRECURSIONLIMITERROR']._serialized_start=112
38
+ _globals['_GRAPHRECURSIONLIMITERROR']._serialized_end=153
39
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,21 @@
1
+ from google.protobuf import descriptor as _descriptor
2
+ from google.protobuf import message as _message
3
+ from typing import ClassVar as _ClassVar, Optional as _Optional
4
+
5
+ DESCRIPTOR: _descriptor.FileDescriptor
6
+
7
+ class UserCodeExecutionError(_message.Message):
8
+ __slots__ = ("error_type", "error_message", "traceback")
9
+ ERROR_TYPE_FIELD_NUMBER: _ClassVar[int]
10
+ ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
11
+ TRACEBACK_FIELD_NUMBER: _ClassVar[int]
12
+ error_type: str
13
+ error_message: str
14
+ traceback: str
15
+ def __init__(self, error_type: _Optional[str] = ..., error_message: _Optional[str] = ..., traceback: _Optional[str] = ...) -> None: ...
16
+
17
+ class GraphRecursionLimitError(_message.Message):
18
+ __slots__ = ("limit",)
19
+ LIMIT_FIELD_NUMBER: _ClassVar[int]
20
+ limit: int
21
+ def __init__(self, limit: _Optional[int] = ...) -> None: ...
@@ -0,0 +1,24 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+
7
+ GRPC_GENERATED_VERSION = '1.75.1'
8
+ GRPC_VERSION = grpc.__version__
9
+ _version_not_supported = False
10
+
11
+ try:
12
+ from grpc._utilities import first_version_is_lower
13
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
14
+ except ImportError:
15
+ _version_not_supported = True
16
+
17
+ if _version_not_supported:
18
+ raise RuntimeError(
19
+ f'The grpc package installed is at version {GRPC_VERSION},'
20
+ + f' but the generated code in errors_pb2_grpc.py depends on'
21
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
22
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
23
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
24
+ )
@@ -0,0 +1,370 @@
1
+ """gRPC-based operations for LangGraph API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ from http import HTTPStatus
8
+ from typing import TYPE_CHECKING, Any, overload
9
+
10
+ import orjson
11
+ import structlog
12
+ from google.protobuf.json_format import MessageToDict
13
+ from google.protobuf.struct_pb2 import Struct # type: ignore[import]
14
+ from grpc import StatusCode
15
+ from grpc.aio import AioRpcError
16
+ from langgraph_sdk.schema import Config
17
+ from starlette.exceptions import HTTPException
18
+
19
+ from langgraph_api.auth.custom import handle_event as auth_handle_event
20
+ from langgraph_api.grpc.generated import core_api_pb2 as pb
21
+ from langgraph_api.serde import json_dumpb
22
+ from langgraph_api.utils import get_auth_ctx
23
+
24
+ _MAX_AUTH_FILTER_DEPTH = 2
25
+
26
+ if TYPE_CHECKING:
27
+ from langgraph_api.schema import Context
28
+
29
+ __all__ = ["Assistants", "Runs", "Threads"]
30
+
31
+ logger = structlog.stdlib.get_logger(__name__)
32
+
33
+ GRPC_STATUS_TO_HTTP_STATUS = {
34
+ StatusCode.NOT_FOUND: HTTPStatus.NOT_FOUND,
35
+ StatusCode.ALREADY_EXISTS: HTTPStatus.CONFLICT,
36
+ StatusCode.INVALID_ARGUMENT: HTTPStatus.UNPROCESSABLE_ENTITY,
37
+ }
38
+
39
+
40
+ def map_if_exists(if_exists: str) -> Any:
41
+ """Map if_exists string to protobuf OnConflictBehavior."""
42
+ from langgraph_api.grpc.generated import core_api_pb2 as pb
43
+
44
+ if if_exists == "do_nothing":
45
+ return pb.OnConflictBehavior.DO_NOTHING
46
+ return pb.OnConflictBehavior.RAISE
47
+
48
+
49
+ @overload
50
+ def consolidate_config_and_context(
51
+ config: Config | None, context: None
52
+ ) -> tuple[Config, None]: ...
53
+
54
+
55
+ @overload
56
+ def consolidate_config_and_context(
57
+ config: Config | None, context: Context
58
+ ) -> tuple[Config, Context]: ...
59
+
60
+
61
+ def consolidate_config_and_context(
62
+ config: Config | None, context: Context | None
63
+ ) -> tuple[Config, Context | None]:
64
+ """Return a new (config, context) with consistent configurable/context.
65
+
66
+ Does not mutate the passed-in objects. If both configurable and context
67
+ are provided, raises 400. If only one is provided, mirrors it to the other.
68
+ """
69
+ cfg: Config = Config(config or {})
70
+ ctx: Context | None = dict(context) if context is not None else None
71
+ configurable = cfg.get("configurable")
72
+
73
+ if configurable and ctx:
74
+ raise HTTPException(
75
+ status_code=400,
76
+ detail="Cannot specify both configurable and context. Prefer setting context alone."
77
+ " Context was introduced in LangGraph 0.6.0 and "
78
+ "is the long term planned replacement for configurable.",
79
+ )
80
+
81
+ if configurable:
82
+ ctx = configurable
83
+ elif ctx is not None:
84
+ cfg["configurable"] = ctx
85
+
86
+ return cfg, ctx
87
+
88
+
89
+ def dict_to_struct(data: dict[str, Any]) -> Struct:
90
+ """Convert a dictionary to a protobuf Struct."""
91
+ struct = Struct()
92
+ if data:
93
+ struct.update(data)
94
+ return struct
95
+
96
+
97
+ def struct_to_dict(struct: Struct) -> dict[str, Any]:
98
+ """Convert a protobuf Struct to a dictionary."""
99
+ return MessageToDict(struct) if struct else {}
100
+
101
+
102
+ def exception_to_struct(exception: BaseException | None) -> Struct | None:
103
+ """Convert an exception to a protobuf Struct."""
104
+ if exception is None:
105
+ return None
106
+ import orjson
107
+
108
+ try:
109
+ payload = orjson.loads(json_dumpb(exception))
110
+ except orjson.JSONDecodeError:
111
+ payload = {"error": type(exception).__name__, "message": str(exception)}
112
+ return dict_to_struct(payload)
113
+
114
+
115
+ def _map_sort_order(sort_order: str | None) -> Any:
116
+ """Map string sort_order to protobuf enum."""
117
+ from langgraph_api.grpc.generated import core_api_pb2 as pb
118
+
119
+ if sort_order and sort_order.upper() == "ASC":
120
+ return pb.SortOrder.ASC
121
+ return pb.SortOrder.DESC
122
+
123
+
124
+ def _handle_grpc_error(error: AioRpcError) -> None:
125
+ """Handle gRPC errors and convert to appropriate exceptions.
126
+
127
+ We get two types of exception back from GRPC:
128
+ - A JSON string that contains a message body. These we want to just return the message.
129
+ - A string. This we can return verbatim.
130
+ Always return detail as a string here.
131
+ """
132
+ error_details = error.details()
133
+ if error_details is not None:
134
+ try:
135
+ details = orjson.loads(error_details)
136
+ error_details = orjson.dumps(details.get("message", "")).decode()
137
+ except orjson.JSONDecodeError:
138
+ # error details is not json, so just retun it as is
139
+ pass
140
+
141
+ raise HTTPException(
142
+ status_code=GRPC_STATUS_TO_HTTP_STATUS.get(
143
+ error.code(), HTTPStatus.INTERNAL_SERVER_ERROR
144
+ ),
145
+ detail=error_details,
146
+ )
147
+
148
+
149
+ def _serialize_filter_value(value: Any) -> str:
150
+ """Serialize a filter value to a valid JSON string for JSONB comparison.
151
+
152
+ Uses orjson for serialization to handle UUIDs, datetimes, etc.
153
+ All values are returned as valid JSON strings that can be parsed with ::jsonb.
154
+
155
+ Examples:
156
+ "johndoe" -> '"johndoe"' (JSON string)
157
+ uuid.UUID("...") -> '"uuid-string"' (JSON string)
158
+ datetime(...) -> '"2024-03-15T12:15:00+00:00"' (JSON string)
159
+ 42 -> '42' (JSON number)
160
+ {"foo": "bar"} -> '{"foo": "bar"}' (JSON object)
161
+ """
162
+ # Serialize everything with orjson to get valid JSON
163
+ json_bytes = orjson.dumps(value)
164
+ return json_bytes.decode("utf-8")
165
+
166
+
167
+ def _filters_to_proto(
168
+ filters: dict[str, Any] | None, *, _depth: int = 0
169
+ ) -> list[pb.AuthFilter]:
170
+ """Convert Python auth filters to gRPC proto format.
171
+
172
+ We have some weird auth semantics today:
173
+ - Objects are supported in the $eq operator, but not in the $contains operator or default case.
174
+ - List of numbers in contains don't work anywhere. This is fine and can be removed in the future.
175
+ - Odd nesting doesn't work anywhere (e.g. filter {"outer_key": "inner_value"} won't match on {"outer_key": {"inner_key": "inner_value"}})
176
+
177
+ Args:
178
+ filters: Python dict with filter values, e.g., {"owner": "user123"}
179
+
180
+ Returns:
181
+ List of AuthFilter proto messages, empty list if no filters
182
+
183
+ Raises:
184
+ HTTPException: If filters exceed maximum nesting depth (_MAX_AUTH_FILTER_DEPTH).
185
+ """
186
+ if not filters:
187
+ return []
188
+
189
+ proto_filters: list[pb.AuthFilter] = []
190
+
191
+ for key, filter_value in filters.items():
192
+ auth_filter = pb.AuthFilter()
193
+
194
+ if key == "$or":
195
+ if _depth >= _MAX_AUTH_FILTER_DEPTH:
196
+ raise HTTPException(
197
+ status_code=500,
198
+ detail=f"Your auth handler returned a filter with too much nesting. Maximum nesting depth is {_MAX_AUTH_FILTER_DEPTH}. Check the filter returned by your auth handler.",
199
+ )
200
+ if not isinstance(filter_value, list) or len(filter_value) < 2:
201
+ raise HTTPException(
202
+ status_code=500,
203
+ detail="Your auth handler returned a filter with an invalid $or operator. The $or operator must be a list of at least 2 filter objects. Check the filter returned by your auth handler.",
204
+ )
205
+ # Recursively convert each filter, wrapping multi-filter branches in AND
206
+ nested_filters = []
207
+ for filter_dict in filter_value:
208
+ branch_filters = _filters_to_proto(filter_dict, _depth=_depth + 1)
209
+ if not branch_filters:
210
+ continue
211
+ if len(branch_filters) == 1:
212
+ nested_filters.append(branch_filters[0])
213
+ else:
214
+ and_filter = pb.AuthFilter()
215
+ and_filter.and_filter.CopyFrom(
216
+ pb.AndAuthFilter(filters=branch_filters)
217
+ )
218
+ nested_filters.append(and_filter)
219
+ auth_filter.or_filter.CopyFrom(pb.OrAuthFilter(filters=nested_filters))
220
+ proto_filters.append(auth_filter)
221
+ elif key == "$and":
222
+ if _depth >= _MAX_AUTH_FILTER_DEPTH:
223
+ raise HTTPException(
224
+ status_code=500,
225
+ detail=f"Your auth handler returned a filter with too much nesting. Maximum nesting depth is {_MAX_AUTH_FILTER_DEPTH}. Check the filter returned by your auth handler.",
226
+ )
227
+ if not isinstance(filter_value, list) or len(filter_value) < 2:
228
+ raise HTTPException(
229
+ status_code=500,
230
+ detail="Your auth handler returned a filter with an invalid $and operator. The $and operator must be a list of at least 2 filter objects. Check the filter returned by your auth handler.",
231
+ )
232
+ # Flatten $and branches into the current AND level.
233
+ for filter_dict in filter_value:
234
+ branch_filters = _filters_to_proto(filter_dict, _depth=_depth + 1)
235
+ proto_filters.extend(branch_filters)
236
+ else:
237
+ # We expect one key in the dict with a specific known value
238
+ if isinstance(filter_value, dict):
239
+ if len(filter_value.keys()) != 1:
240
+ logger.error(
241
+ "Error parsing filter: filter_value is not a dict with one key"
242
+ )
243
+ raise HTTPException(
244
+ status_code=500,
245
+ detail="Your auth handler returned a filter with an invalid value. The value must be a dict with one key. Check the filter returned by your auth handler.",
246
+ )
247
+
248
+ operator = next(iter(filter_value.keys()))
249
+ value = filter_value[operator]
250
+
251
+ if operator == "$eq":
252
+ matchstr = _serialize_filter_value(value)
253
+ auth_filter.eq.CopyFrom(pb.EqAuthFilter(key=key, match=matchstr))
254
+ elif operator == "$contains":
255
+ if isinstance(value, list):
256
+ matches = [_serialize_filter_value(item) for item in value]
257
+ auth_filter.contains.CopyFrom(
258
+ pb.ContainsAuthFilter(key=key, matches=matches)
259
+ )
260
+ else:
261
+ # If the value itself is not a list, wrap it as a single-item list
262
+ serialized = _serialize_filter_value(value)
263
+ auth_filter.contains.CopyFrom(
264
+ pb.ContainsAuthFilter(key=key, matches=[serialized])
265
+ )
266
+ else:
267
+ logger.error(
268
+ "Error parsing filter: operator is not $eq or $contains"
269
+ )
270
+ raise HTTPException(
271
+ status_code=500,
272
+ detail="Your auth handler returned a filter with an invalid key. The key must be one of $eq or $contains. Check the filter returned by your auth handler.",
273
+ )
274
+ # Otherwise, it's the default case - simple value means equality check
275
+ else:
276
+ # Serialize with orjson (for proper datetime/UUID handling), then unwrap if it's a JSON string
277
+ matchstr = _serialize_filter_value(filter_value)
278
+ auth_filter.eq.CopyFrom(pb.EqAuthFilter(key=key, match=matchstr))
279
+ proto_filters.append(auth_filter)
280
+
281
+ return proto_filters
282
+
283
+
284
+ class Authenticated:
285
+ """Base class for authenticated operations (matches storage_postgres interface)."""
286
+
287
+ resource: str = "assistants"
288
+
289
+ @classmethod
290
+ async def handle_event(
291
+ cls,
292
+ ctx: Any, # Auth context
293
+ action: str,
294
+ value: Any,
295
+ ) -> list[pb.AuthFilter]:
296
+ """Handle authentication event and convert filters to gRPC proto format.
297
+
298
+ Args:
299
+ ctx: Auth context (from get_auth_ctx())
300
+ action: Action being performed (e.g., "create", "read", "update", "delete", "search")
301
+ value: Value being operated on
302
+
303
+ Returns:
304
+ List of AuthFilter proto messages, empty list if no filters
305
+ """
306
+ # Get auth context if not provided
307
+ if ctx is None:
308
+ ctx = get_auth_ctx()
309
+
310
+ # If still no context, no auth filters needed
311
+ if ctx is None:
312
+ return []
313
+
314
+ # Create auth context for the handler
315
+ from langgraph_sdk import Auth
316
+
317
+ auth_ctx = Auth.types.AuthContext(
318
+ resource=cls.resource,
319
+ action=action,
320
+ user=ctx.user,
321
+ permissions=ctx.permissions,
322
+ )
323
+
324
+ # Call the auth system to get filters
325
+ filters = await auth_handle_event(auth_ctx, value)
326
+
327
+ # Convert Python filters to gRPC proto format
328
+ return _filters_to_proto(filters)
329
+
330
+
331
+ def grpc_error_guard(cls):
332
+ """Class decorator to wrap async methods and handle gRPC errors uniformly."""
333
+ for name, attr in list(cls.__dict__.items()):
334
+ func = None
335
+ wrapper_type = None
336
+ if isinstance(attr, staticmethod):
337
+ func = attr.__func__
338
+ wrapper_type = staticmethod
339
+ elif isinstance(attr, classmethod):
340
+ func = attr.__func__
341
+ wrapper_type = classmethod
342
+ elif callable(attr):
343
+ func = attr
344
+
345
+ if func and asyncio.iscoroutinefunction(func):
346
+
347
+ def make_wrapper(f):
348
+ @functools.wraps(f)
349
+ async def wrapped(*args, **kwargs):
350
+ try:
351
+ return await f(*args, **kwargs)
352
+ except AioRpcError as e:
353
+ _handle_grpc_error(e)
354
+
355
+ return wrapped # noqa: B023
356
+
357
+ wrapped = make_wrapper(func)
358
+ if wrapper_type is staticmethod:
359
+ setattr(cls, name, staticmethod(wrapped))
360
+ elif wrapper_type is classmethod:
361
+ setattr(cls, name, classmethod(wrapped))
362
+ else:
363
+ setattr(cls, name, wrapped)
364
+ return cls
365
+
366
+
367
+ # Import at the end to avoid circular imports
368
+ from .assistants import Assistants # noqa: E402
369
+ from .runs import Runs # noqa: E402
370
+ from .threads import Threads # noqa: E402