langgraph-api 0.5.4__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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/__init__.py +93 -27
- langgraph_api/api/a2a.py +36 -32
- langgraph_api/api/assistants.py +114 -26
- langgraph_api/api/mcp.py +3 -3
- langgraph_api/api/meta.py +15 -2
- langgraph_api/api/openapi.py +27 -17
- langgraph_api/api/profile.py +108 -0
- langgraph_api/api/runs.py +114 -57
- langgraph_api/api/store.py +19 -2
- langgraph_api/api/threads.py +133 -10
- langgraph_api/asgi_transport.py +14 -9
- langgraph_api/auth/custom.py +23 -13
- langgraph_api/cli.py +86 -41
- langgraph_api/command.py +2 -2
- langgraph_api/config/__init__.py +532 -0
- langgraph_api/config/_parse.py +58 -0
- langgraph_api/config/schemas.py +431 -0
- langgraph_api/cron_scheduler.py +17 -1
- langgraph_api/encryption/__init__.py +15 -0
- langgraph_api/encryption/aes_json.py +158 -0
- langgraph_api/encryption/context.py +35 -0
- langgraph_api/encryption/custom.py +280 -0
- langgraph_api/encryption/middleware.py +632 -0
- langgraph_api/encryption/shared.py +63 -0
- langgraph_api/errors.py +12 -1
- langgraph_api/executor_entrypoint.py +11 -6
- langgraph_api/feature_flags.py +19 -0
- langgraph_api/graph.py +163 -64
- langgraph_api/{grpc_ops → grpc}/client.py +142 -12
- langgraph_api/{grpc_ops → grpc}/config_conversion.py +16 -10
- langgraph_api/grpc/generated/__init__.py +29 -0
- langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
- langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
- langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
- langgraph_api/grpc/generated/core_api_pb2.py +216 -0
- langgraph_api/{grpc_ops → grpc}/generated/core_api_pb2.pyi +292 -372
- langgraph_api/{grpc_ops → grpc}/generated/core_api_pb2_grpc.py +252 -31
- langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
- langgraph_api/{grpc_ops → grpc}/generated/engine_common_pb2.pyi +178 -104
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
- langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/errors_pb2.py +39 -0
- langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
- langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
- langgraph_api/grpc/ops/__init__.py +370 -0
- langgraph_api/grpc/ops/assistants.py +424 -0
- langgraph_api/grpc/ops/runs.py +792 -0
- langgraph_api/grpc/ops/threads.py +1013 -0
- langgraph_api/http.py +16 -5
- langgraph_api/js/client.mts +1 -4
- langgraph_api/js/package.json +28 -27
- langgraph_api/js/remote.py +39 -17
- langgraph_api/js/sse.py +2 -2
- langgraph_api/js/ui.py +1 -1
- langgraph_api/js/yarn.lock +1139 -869
- langgraph_api/metadata.py +29 -3
- langgraph_api/middleware/http_logger.py +1 -1
- langgraph_api/middleware/private_network.py +7 -7
- langgraph_api/models/run.py +44 -26
- langgraph_api/otel_context.py +205 -0
- langgraph_api/patch.py +2 -2
- langgraph_api/queue_entrypoint.py +34 -35
- langgraph_api/route.py +33 -1
- langgraph_api/schema.py +84 -9
- langgraph_api/self_hosted_logs.py +2 -2
- langgraph_api/self_hosted_metrics.py +73 -3
- langgraph_api/serde.py +16 -4
- langgraph_api/server.py +33 -31
- langgraph_api/state.py +3 -2
- langgraph_api/store.py +25 -16
- langgraph_api/stream.py +20 -16
- langgraph_api/thread_ttl.py +28 -13
- langgraph_api/timing/__init__.py +25 -0
- langgraph_api/timing/profiler.py +200 -0
- langgraph_api/timing/timer.py +318 -0
- langgraph_api/utils/__init__.py +53 -8
- langgraph_api/utils/config.py +2 -1
- langgraph_api/utils/future.py +10 -6
- langgraph_api/utils/uuids.py +29 -62
- langgraph_api/validation.py +6 -0
- langgraph_api/webhook.py +120 -6
- langgraph_api/worker.py +54 -24
- {langgraph_api-0.5.4.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +8 -6
- langgraph_api-0.7.3.dist-info/RECORD +168 -0
- {langgraph_api-0.5.4.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
- langgraph_runtime/__init__.py +1 -0
- langgraph_runtime/routes.py +11 -0
- logging.json +1 -3
- openapi.json +635 -537
- langgraph_api/config.py +0 -523
- langgraph_api/grpc_ops/generated/__init__.py +0 -5
- langgraph_api/grpc_ops/generated/core_api_pb2.py +0 -275
- langgraph_api/grpc_ops/generated/engine_common_pb2.py +0 -194
- langgraph_api/grpc_ops/ops.py +0 -1045
- langgraph_api-0.5.4.dist-info/RECORD +0 -121
- /langgraph_api/{grpc_ops → grpc}/__init__.py +0 -0
- /langgraph_api/{grpc_ops → grpc}/generated/engine_common_pb2_grpc.py +0 -0
- {langgraph_api-0.5.4.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.5.4.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|