langgraph-api 0.0.24__tar.gz → 0.0.25__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 langgraph-api might be problematic. Click here for more details.
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/PKG-INFO +2 -2
- langgraph_api-0.0.25/langgraph_api/api/__init__.py +135 -0
- langgraph_api-0.0.25/langgraph_api/api/openapi.py +314 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/custom.py +2 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/cli.py +9 -2
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/config.py +37 -3
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/remote_new.py +1 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/remote_old.py +1 -0
- langgraph_api-0.0.25/langgraph_api/server.py +139 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/utils.py +29 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/pyproject.toml +4 -3
- langgraph_api-0.0.24/langgraph_api/api/__init__.py +0 -64
- langgraph_api-0.0.24/langgraph_api/api/openapi.py +0 -69
- langgraph_api-0.0.24/langgraph_api/server.py +0 -67
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/LICENSE +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/README.md +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/graph.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/http.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/client.new.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/server_sent_events.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/hooks.mjs +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/parser/parser.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/schema/types.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/schema/types.template.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/api.test.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/compose-postgres.yml +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/.gitignore +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/agent.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/delay.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/error.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/nested.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/package.json +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/weather.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/yarn.lock +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/parser.test.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/tests/utils.mts +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/lifespan.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/queue.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/route.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/state.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/stream.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_license/middleware.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/__init__.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/checkpoint.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/database.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/ops.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/queue.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/retry.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/store.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/langgraph_storage/ttl_dict.py +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/logging.json +0 -0
- {langgraph_api-0.0.24 → langgraph_api-0.0.25}/openapi.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.25
|
|
4
4
|
Summary:
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
Author: Nuno Campos
|
|
@@ -15,7 +15,7 @@ Requires-Dist: httpx (>=0.27.0)
|
|
|
15
15
|
Requires-Dist: jsonschema-rs (>=0.25.0,<0.26.0)
|
|
16
16
|
Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
|
|
17
17
|
Requires-Dist: langgraph (>=0.2.56,<0.3.0)
|
|
18
|
-
Requires-Dist: langgraph-checkpoint (>=2.0.
|
|
18
|
+
Requires-Dist: langgraph-checkpoint (>=2.0.15,<3.0)
|
|
19
19
|
Requires-Dist: langgraph-sdk (>=0.1.51,<0.2.0)
|
|
20
20
|
Requires-Dist: langsmith (>=0.1.63,<0.4.0)
|
|
21
21
|
Requires-Dist: orjson (>=3.10.1)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
from starlette.applications import Starlette
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
10
|
+
from starlette.routing import BaseRoute, Mount, Route
|
|
11
|
+
|
|
12
|
+
from langgraph_api.api.assistants import assistants_routes
|
|
13
|
+
from langgraph_api.api.meta import meta_info, meta_metrics
|
|
14
|
+
from langgraph_api.api.openapi import get_openapi_spec
|
|
15
|
+
from langgraph_api.api.runs import runs_routes
|
|
16
|
+
from langgraph_api.api.store import store_routes
|
|
17
|
+
from langgraph_api.api.threads import threads_routes
|
|
18
|
+
from langgraph_api.auth.middleware import auth_middleware
|
|
19
|
+
from langgraph_api.config import HTTP_CONFIG, MIGRATIONS_PATH
|
|
20
|
+
from langgraph_api.graph import js_bg_tasks
|
|
21
|
+
from langgraph_api.validation import DOCS_HTML
|
|
22
|
+
from langgraph_storage.database import connect, healthcheck
|
|
23
|
+
|
|
24
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def ok(request: Request):
|
|
28
|
+
check_db = int(request.query_params.get("check_db", "0")) # must be "0" or "1"
|
|
29
|
+
if check_db:
|
|
30
|
+
await healthcheck()
|
|
31
|
+
if js_bg_tasks:
|
|
32
|
+
from langgraph_api.js.remote import js_healthcheck
|
|
33
|
+
|
|
34
|
+
await js_healthcheck()
|
|
35
|
+
return JSONResponse({"ok": True})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def openapi(request: Request):
|
|
39
|
+
spec = await asyncio.to_thread(get_openapi_spec)
|
|
40
|
+
return Response(spec, media_type="application/json")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def docs(request: Request):
|
|
44
|
+
return HTMLResponse(DOCS_HTML)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
meta_routes: list[BaseRoute] = [
|
|
48
|
+
Route("/ok", ok, methods=["GET"]),
|
|
49
|
+
Route("/openapi.json", openapi, methods=["GET"]),
|
|
50
|
+
Route("/docs", docs, methods=["GET"]),
|
|
51
|
+
Route("/info", meta_info, methods=["GET"]),
|
|
52
|
+
Route("/metrics", meta_metrics, methods=["GET"]),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
protected_routes: list[BaseRoute] = []
|
|
56
|
+
|
|
57
|
+
if HTTP_CONFIG:
|
|
58
|
+
if not HTTP_CONFIG.get("disable_assistants"):
|
|
59
|
+
protected_routes.extend(assistants_routes)
|
|
60
|
+
if not HTTP_CONFIG.get("disable_runs"):
|
|
61
|
+
protected_routes.extend(runs_routes)
|
|
62
|
+
if not HTTP_CONFIG.get("disable_threads"):
|
|
63
|
+
protected_routes.extend(threads_routes)
|
|
64
|
+
if not HTTP_CONFIG.get("disable_store"):
|
|
65
|
+
protected_routes.extend(store_routes)
|
|
66
|
+
else:
|
|
67
|
+
protected_routes.extend(assistants_routes)
|
|
68
|
+
protected_routes.extend(runs_routes)
|
|
69
|
+
protected_routes.extend(threads_routes)
|
|
70
|
+
protected_routes.extend(store_routes)
|
|
71
|
+
|
|
72
|
+
routes: list[BaseRoute] = []
|
|
73
|
+
user_router = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_custom_app(app_import: str) -> Starlette | None:
|
|
77
|
+
# Expect a string in either "path/to/file.py:my_variable" or "some.module.in:my_variable"
|
|
78
|
+
logger.info(f"Loading custom app from {app_import}")
|
|
79
|
+
path, name = app_import.rsplit(":", 1)
|
|
80
|
+
try:
|
|
81
|
+
if os.path.isfile(path) or path.endswith(".py"):
|
|
82
|
+
# Import from file path using a unique module name.
|
|
83
|
+
spec = importlib.util.spec_from_file_location("user_router_module", path)
|
|
84
|
+
if spec is None or spec.loader is None:
|
|
85
|
+
raise ImportError(f"Cannot load spec from {path}")
|
|
86
|
+
module = importlib.util.module_from_spec(spec)
|
|
87
|
+
spec.loader.exec_module(module)
|
|
88
|
+
else:
|
|
89
|
+
# Import as a normal module.
|
|
90
|
+
module = importlib.import_module(path)
|
|
91
|
+
user_router = getattr(module, name)
|
|
92
|
+
if not isinstance(user_router, Starlette):
|
|
93
|
+
raise TypeError(
|
|
94
|
+
f"Object '{name}' in module '{path}' is not a Starlette or FastAPI application. "
|
|
95
|
+
"Please initialize your app by importing and using the appropriate class: "
|
|
96
|
+
"\nfrom starlette.applications import Starlette\n\napp = Starlette(...)\n\n"
|
|
97
|
+
"or\n\nfrom fastapi import FastAPI\n\napp = FastAPI(...)\n\n"
|
|
98
|
+
)
|
|
99
|
+
except ImportError as e:
|
|
100
|
+
raise ImportError(f"Failed to import app module '{path}'") from e
|
|
101
|
+
except AttributeError as e:
|
|
102
|
+
raise AttributeError(f"App '{name}' not found in module '{path}'") from e
|
|
103
|
+
return user_router
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if HTTP_CONFIG:
|
|
107
|
+
if router_import := HTTP_CONFIG.get("app"):
|
|
108
|
+
user_router = load_custom_app(router_import)
|
|
109
|
+
if not HTTP_CONFIG.get("disable_meta"):
|
|
110
|
+
routes.extend(meta_routes)
|
|
111
|
+
if protected_routes:
|
|
112
|
+
routes.append(
|
|
113
|
+
Mount(
|
|
114
|
+
"/",
|
|
115
|
+
middleware=[auth_middleware],
|
|
116
|
+
routes=protected_routes,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
routes.extend(meta_routes)
|
|
122
|
+
routes.append(Mount("/", middleware=[auth_middleware], routes=protected_routes))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if "inmem" in MIGRATIONS_PATH:
|
|
126
|
+
|
|
127
|
+
async def truncate(request: Request):
|
|
128
|
+
from langgraph_storage.checkpoint import Checkpointer
|
|
129
|
+
|
|
130
|
+
Checkpointer().clear()
|
|
131
|
+
async with connect() as conn:
|
|
132
|
+
conn.clear()
|
|
133
|
+
return JSONResponse({"ok": True})
|
|
134
|
+
|
|
135
|
+
routes.insert(0, Route("/internal/truncate", truncate, methods=["POST"]))
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import typing
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
import orjson
|
|
7
|
+
|
|
8
|
+
from langgraph_api.config import LANGGRAPH_AUTH, LANGGRAPH_AUTH_TYPE
|
|
9
|
+
from langgraph_api.graph import GRAPHS
|
|
10
|
+
from langgraph_api.validation import openapi
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
CUSTOM_OPENAPI_SPEC = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_custom_spec(spec: dict):
|
|
18
|
+
global CUSTOM_OPENAPI_SPEC
|
|
19
|
+
CUSTOM_OPENAPI_SPEC = spec
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache(maxsize=1)
|
|
23
|
+
def get_openapi_spec() -> str:
|
|
24
|
+
# patch the graph_id enums
|
|
25
|
+
graph_ids = list(GRAPHS.keys())
|
|
26
|
+
for schema in (
|
|
27
|
+
"Assistant",
|
|
28
|
+
"AssistantCreate",
|
|
29
|
+
"AssistantPatch",
|
|
30
|
+
"GraphSchema",
|
|
31
|
+
"AssistantSearchRequest",
|
|
32
|
+
):
|
|
33
|
+
openapi["components"]["schemas"][schema]["properties"]["graph_id"]["enum"] = (
|
|
34
|
+
graph_ids
|
|
35
|
+
)
|
|
36
|
+
# patch the auth schemes
|
|
37
|
+
if LANGGRAPH_AUTH_TYPE == "langsmith":
|
|
38
|
+
openapi["security"] = [
|
|
39
|
+
{"x-api-key": []},
|
|
40
|
+
]
|
|
41
|
+
openapi["components"]["securitySchemes"] = {
|
|
42
|
+
"x-api-key": {"type": "apiKey", "in": "header", "name": "x-api-key"}
|
|
43
|
+
}
|
|
44
|
+
if LANGGRAPH_AUTH:
|
|
45
|
+
# Allow user to specify OpenAPI security configuration
|
|
46
|
+
if isinstance(LANGGRAPH_AUTH, dict) and "openapi" in LANGGRAPH_AUTH:
|
|
47
|
+
openapi_config = LANGGRAPH_AUTH["openapi"]
|
|
48
|
+
if isinstance(openapi_config, dict):
|
|
49
|
+
# Add security schemes
|
|
50
|
+
if "securitySchemes" in openapi_config:
|
|
51
|
+
openapi["components"]["securitySchemes"] = openapi_config[
|
|
52
|
+
"securitySchemes"
|
|
53
|
+
]
|
|
54
|
+
elif "security_schemes" in openapi_config:
|
|
55
|
+
# For our sorry python users
|
|
56
|
+
openapi["components"]["securitySchemes"] = openapi_config[
|
|
57
|
+
"security_schemes"
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Add default security if specified
|
|
61
|
+
if "security" in openapi_config:
|
|
62
|
+
openapi["security"] = openapi_config["security"]
|
|
63
|
+
|
|
64
|
+
if "paths" in openapi_config:
|
|
65
|
+
for path, methods in openapi_config["paths"].items():
|
|
66
|
+
if path in openapi["paths"]:
|
|
67
|
+
openapi_path = openapi["paths"][path]
|
|
68
|
+
for method, security in methods.items():
|
|
69
|
+
method = method.lower()
|
|
70
|
+
if method in openapi_path:
|
|
71
|
+
openapi_path[method]["security"] = security
|
|
72
|
+
else:
|
|
73
|
+
logger.warning(
|
|
74
|
+
"Custom authentication is enabled but no OpenAPI security configuration was provided. "
|
|
75
|
+
"API documentation will not show authentication requirements. "
|
|
76
|
+
"Add 'openapi' section to auth section of your `langgraph.json` file to specify security schemes."
|
|
77
|
+
)
|
|
78
|
+
final = openapi
|
|
79
|
+
if CUSTOM_OPENAPI_SPEC:
|
|
80
|
+
final = merge_openapi_specs(openapi, CUSTOM_OPENAPI_SPEC)
|
|
81
|
+
return orjson.dumps(final)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def merge_openapi_specs(spec_a: dict, spec_b: dict) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
Merge two OpenAPI specifications with spec_b taking precedence on conflicts.
|
|
87
|
+
|
|
88
|
+
This function handles merging of the following keys:
|
|
89
|
+
- "openapi": Uses spec_b’s version.
|
|
90
|
+
- "info": Merges dictionaries with spec_b taking precedence.
|
|
91
|
+
- "servers": Merges lists with deduplication (by URL and description).
|
|
92
|
+
- "paths": For shared paths, merges HTTP methods:
|
|
93
|
+
- If a method exists in both, spec_b’s definition wins.
|
|
94
|
+
- Otherwise, methods from both are preserved.
|
|
95
|
+
Additionally, merges path-level "parameters" by (name, in).
|
|
96
|
+
- "components": Merges per component type (schemas, responses, etc.).
|
|
97
|
+
- "security" and "tags": Merges lists with deduplication using a key function.
|
|
98
|
+
- "externalDocs" and any additional keys: spec_b wins.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
spec_a (dict): First OpenAPI specification.
|
|
102
|
+
spec_b (dict): Second OpenAPI specification (takes precedence).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
dict: The merged OpenAPI specification.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
TypeError: If either input is not a dict.
|
|
109
|
+
ValueError: If a required field (openapi, info, paths) is missing.
|
|
110
|
+
"""
|
|
111
|
+
if not isinstance(spec_a, dict) or not isinstance(spec_b, dict):
|
|
112
|
+
raise TypeError("Both specifications must be dictionaries.")
|
|
113
|
+
|
|
114
|
+
required_fields = {"openapi", "info", "paths"}
|
|
115
|
+
for spec in (spec_a, spec_b):
|
|
116
|
+
missing = required_fields - spec.keys()
|
|
117
|
+
if missing:
|
|
118
|
+
raise ValueError(f"Missing required OpenAPI fields: {missing}")
|
|
119
|
+
|
|
120
|
+
merged = copy.deepcopy(spec_a)
|
|
121
|
+
|
|
122
|
+
if "openapi" in spec_b:
|
|
123
|
+
merged["openapi"] = spec_b["openapi"]
|
|
124
|
+
|
|
125
|
+
# Merge "info": Combine dictionaries with spec_b overriding spec_a.
|
|
126
|
+
merged["info"] = {**merged.get("info", {}), **spec_b.get("info", {})}
|
|
127
|
+
|
|
128
|
+
# Merge "servers": Use deduplication based on (url, description).
|
|
129
|
+
merged["servers"] = _merge_lists(
|
|
130
|
+
merged.get("servers", []),
|
|
131
|
+
spec_b.get("servers", []),
|
|
132
|
+
key_func=lambda x: (x.get("url"), x.get("description")),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Merge "paths": Merge individual paths and methods.
|
|
136
|
+
merged["paths"] = _merge_paths(merged.get("paths", {}), spec_b.get("paths", {}))
|
|
137
|
+
|
|
138
|
+
# Merge "components": Merge per component type.
|
|
139
|
+
merged["components"] = _merge_components(
|
|
140
|
+
merged.get("components", {}), spec_b.get("components", {})
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Merge "security": Merge lists with deduplication.
|
|
144
|
+
merged["security"] = _merge_lists(
|
|
145
|
+
merged.get("security", []),
|
|
146
|
+
spec_b.get("security", []),
|
|
147
|
+
key_func=lambda x: tuple(sorted(x.items())),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Merge "tags": Deduplicate tags by "name".
|
|
151
|
+
merged["tags"] = _merge_lists(
|
|
152
|
+
merged.get("tags", []), spec_b.get("tags", []), key_func=lambda x: x.get("name")
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Merge "externalDocs": Use spec_b if provided.
|
|
156
|
+
if "externalDocs" in spec_b:
|
|
157
|
+
merged["externalDocs"] = spec_b["externalDocs"]
|
|
158
|
+
|
|
159
|
+
# Merge any additional keys not explicitly handled.
|
|
160
|
+
handled_keys = {
|
|
161
|
+
"openapi",
|
|
162
|
+
"info",
|
|
163
|
+
"servers",
|
|
164
|
+
"paths",
|
|
165
|
+
"components",
|
|
166
|
+
"security",
|
|
167
|
+
"tags",
|
|
168
|
+
"externalDocs",
|
|
169
|
+
}
|
|
170
|
+
for key in set(spec_a.keys()).union(spec_b.keys()) - handled_keys:
|
|
171
|
+
merged[key] = spec_b.get(key, spec_a.get(key))
|
|
172
|
+
|
|
173
|
+
return merged
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _merge_lists(list_a: list, list_b: list, key_func) -> list:
|
|
177
|
+
"""
|
|
178
|
+
Merge two lists using a key function for deduplication.
|
|
179
|
+
Items from list_b take precedence over items from list_a.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
list_a (list): First list.
|
|
183
|
+
list_b (list): Second list.
|
|
184
|
+
key_func (callable): Function that returns a key used for deduplication.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
list: Merged list.
|
|
188
|
+
"""
|
|
189
|
+
merged_dict = {}
|
|
190
|
+
for item in list_a:
|
|
191
|
+
key = _ensure_hashable(key_func(item))
|
|
192
|
+
if key not in merged_dict:
|
|
193
|
+
merged_dict[key] = item
|
|
194
|
+
for item in list_b:
|
|
195
|
+
key = _ensure_hashable(key_func(item))
|
|
196
|
+
merged_dict[key] = item # spec_b wins
|
|
197
|
+
return list(merged_dict.values())
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _merge_paths(paths_a: dict, paths_b: dict) -> dict:
|
|
201
|
+
"""
|
|
202
|
+
Merge OpenAPI paths objects.
|
|
203
|
+
|
|
204
|
+
For each path:
|
|
205
|
+
- If the path exists in both specs, merge HTTP methods:
|
|
206
|
+
- If a method exists in both, use spec_b’s definition.
|
|
207
|
+
- Otherwise, preserve both.
|
|
208
|
+
- Additionally, merge path-level "parameters" if present.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
paths_a (dict): Paths from the first spec.
|
|
212
|
+
paths_b (dict): Paths from the second spec.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
dict: Merged paths.
|
|
216
|
+
"""
|
|
217
|
+
merged_paths = {}
|
|
218
|
+
# Start with all paths from paths_a.
|
|
219
|
+
for path, methods in paths_a.items():
|
|
220
|
+
merged_paths[path] = copy.deepcopy(methods)
|
|
221
|
+
|
|
222
|
+
# Merge or add paths from paths_b.
|
|
223
|
+
for path, methods_b in paths_b.items():
|
|
224
|
+
if path not in merged_paths:
|
|
225
|
+
merged_paths[path] = copy.deepcopy(methods_b)
|
|
226
|
+
else:
|
|
227
|
+
methods_a = merged_paths[path]
|
|
228
|
+
for method, details_b in methods_b.items():
|
|
229
|
+
key = method.lower()
|
|
230
|
+
# If the method is "parameters", merge them.
|
|
231
|
+
if key == "parameters":
|
|
232
|
+
params_a = methods_a.get("parameters", [])
|
|
233
|
+
params_b = details_b if isinstance(details_b, list) else []
|
|
234
|
+
methods_a["parameters"] = _merge_lists(
|
|
235
|
+
params_a,
|
|
236
|
+
params_b,
|
|
237
|
+
key_func=lambda x: (x.get("name"), x.get("in")),
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
# For HTTP methods, spec_b wins if conflict.
|
|
241
|
+
methods_a[key] = copy.deepcopy(details_b)
|
|
242
|
+
merged_paths[path] = methods_a
|
|
243
|
+
return merged_paths
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _merge_components(components_a: dict, components_b: dict) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
Merge OpenAPI components objects.
|
|
249
|
+
|
|
250
|
+
For each component type (schemas, responses, parameters, examples, requestBodies,
|
|
251
|
+
headers, securitySchemes, links, callbacks), merge dictionaries with spec_b taking precedence.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
components_a (dict): Components from the first spec.
|
|
255
|
+
components_b (dict): Components from the second spec.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
dict: Merged components.
|
|
259
|
+
"""
|
|
260
|
+
merged_components = {}
|
|
261
|
+
# Define the common component types to merge.
|
|
262
|
+
component_types = {
|
|
263
|
+
"schemas",
|
|
264
|
+
"responses",
|
|
265
|
+
"parameters",
|
|
266
|
+
"examples",
|
|
267
|
+
"requestBodies",
|
|
268
|
+
"headers",
|
|
269
|
+
"securitySchemes",
|
|
270
|
+
"links",
|
|
271
|
+
"callbacks",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for comp_type in component_types:
|
|
275
|
+
comp_a = components_a.get(comp_type, {})
|
|
276
|
+
comp_b = components_b.get(comp_type, {})
|
|
277
|
+
merged_components[comp_type] = {**comp_a, **comp_b}
|
|
278
|
+
|
|
279
|
+
# Merge any additional keys in components.
|
|
280
|
+
extra_keys = set(components_a.keys()).union(components_b.keys()) - component_types
|
|
281
|
+
for key in extra_keys:
|
|
282
|
+
merged_components[key] = {
|
|
283
|
+
**components_a.get(key, {}),
|
|
284
|
+
**components_b.get(key, {}),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return merged_components
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _ensure_hashable(obj, depth=0, max_depth=3):
|
|
291
|
+
"""
|
|
292
|
+
Recursively convert a Python object into a hashable representation up to a maximum depth.
|
|
293
|
+
If the depth limit is reached, return str(obj).
|
|
294
|
+
|
|
295
|
+
- Lists are converted to tuples.
|
|
296
|
+
- Dictionaries are converted to tuples of sorted (key, value) pairs.
|
|
297
|
+
- Other types are returned as-is.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
obj: The object to convert.
|
|
301
|
+
depth (int): Current recursion depth.
|
|
302
|
+
max_depth (int): Maximum recursion depth.
|
|
303
|
+
"""
|
|
304
|
+
if depth >= max_depth:
|
|
305
|
+
return str(obj)
|
|
306
|
+
if isinstance(obj, typing.Sequence):
|
|
307
|
+
return tuple(_ensure_hashable(e, depth + 1, max_depth) for e in obj)
|
|
308
|
+
if isinstance(obj, typing.Mapping):
|
|
309
|
+
return tuple(
|
|
310
|
+
sorted(
|
|
311
|
+
(k, _ensure_hashable(v, depth + 1, max_depth)) for k, v in obj.items()
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
return obj
|
|
@@ -4,8 +4,13 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import pathlib
|
|
6
6
|
import threading
|
|
7
|
+
import typing
|
|
7
8
|
from collections.abc import Mapping, Sequence
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
from typing_extensions import TypedDict
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from langgraph_api.config import HttpConfig
|
|
9
14
|
|
|
10
15
|
logging.basicConfig(level=logging.INFO)
|
|
11
16
|
logger = logging.getLogger(__name__)
|
|
@@ -147,7 +152,8 @@ def run_server(
|
|
|
147
152
|
reload_excludes: Sequence[str] | None = None,
|
|
148
153
|
store: StoreConfig | None = None,
|
|
149
154
|
auth: AuthConfig | None = None,
|
|
150
|
-
|
|
155
|
+
http: typing.Optional["HttpConfig"] = None,
|
|
156
|
+
**kwargs: typing.Any,
|
|
151
157
|
):
|
|
152
158
|
"""Run the LangGraph API server."""
|
|
153
159
|
|
|
@@ -263,6 +269,7 @@ For production use, please use LangGraph Cloud.
|
|
|
263
269
|
LANGSERVE_GRAPHS=json.dumps(graphs) if graphs else None,
|
|
264
270
|
LANGSMITH_LANGGRAPH_API_VARIANT="local_dev",
|
|
265
271
|
LANGGRAPH_AUTH=json.dumps(auth) if auth else None,
|
|
272
|
+
LANGGRAPH_HTTP=json.dumps(http) if http else None,
|
|
266
273
|
# See https://developer.chrome.com/blog/private-network-access-update-2024-03
|
|
267
274
|
ALLOW_PRIVATE_NETWORK="true",
|
|
268
275
|
**(env_vars or {}),
|
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
from os import environ, getenv
|
|
2
|
+
from typing import TypedDict
|
|
2
3
|
|
|
3
4
|
import orjson
|
|
4
5
|
from starlette.config import Config, undefined
|
|
5
6
|
from starlette.datastructures import CommaSeparatedStrings
|
|
6
7
|
|
|
8
|
+
# types
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CorsConfig(TypedDict, total=False):
|
|
12
|
+
allow_origins: list[str]
|
|
13
|
+
allow_methods: list[str]
|
|
14
|
+
allow_headers: list[str]
|
|
15
|
+
allow_credentials: bool
|
|
16
|
+
allow_origin_regex: str
|
|
17
|
+
expose_headers: list[str]
|
|
18
|
+
max_age: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpConfig(TypedDict, total=False):
|
|
22
|
+
app: str
|
|
23
|
+
"""Import path for a custom Starlette/FastAPI app to mount"""
|
|
24
|
+
disable_assistants: bool
|
|
25
|
+
"""Disable /assistants routes"""
|
|
26
|
+
disable_threads: bool
|
|
27
|
+
"""Disable /threads routes"""
|
|
28
|
+
disable_runs: bool
|
|
29
|
+
"""Disable /runs routes"""
|
|
30
|
+
disable_store: bool
|
|
31
|
+
"""Disable /store routes"""
|
|
32
|
+
disable_meta: bool
|
|
33
|
+
"""Disable /ok, /info, /metrics, and /docs routes"""
|
|
34
|
+
cors: CorsConfig | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# env
|
|
38
|
+
|
|
7
39
|
env = Config()
|
|
8
40
|
|
|
9
41
|
|
|
@@ -17,7 +49,6 @@ def _parse_json(json: str | None) -> dict | None:
|
|
|
17
49
|
|
|
18
50
|
|
|
19
51
|
STATS_INTERVAL_SECS = env("STATS_INTERVAL_SECS", cast=int, default=60)
|
|
20
|
-
HTTP_CONCURRENCY = env("HTTP_CONCURRENCY", cast=int, default=10)
|
|
21
52
|
|
|
22
53
|
# storage
|
|
23
54
|
|
|
@@ -36,9 +67,12 @@ ALLOW_PRIVATE_NETWORK = env("ALLOW_PRIVATE_NETWORK", cast=bool, default=False)
|
|
|
36
67
|
See https://developer.chrome.com/blog/private-network-access-update-2024-03
|
|
37
68
|
"""
|
|
38
69
|
|
|
70
|
+
HTTP_CONFIG: HttpConfig | None = env("LANGGRAPH_HTTP", cast=_parse_json, default=None)
|
|
39
71
|
CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
|
|
40
|
-
|
|
41
|
-
CORS_CONFIG =
|
|
72
|
+
if HTTP_CONFIG and HTTP_CONFIG.get("cors"):
|
|
73
|
+
CORS_CONFIG = HTTP_CONFIG["cors"]
|
|
74
|
+
else:
|
|
75
|
+
CORS_CONFIG: CorsConfig | None = env("CORS_CONFIG", cast=_parse_json, default=None)
|
|
42
76
|
"""
|
|
43
77
|
{
|
|
44
78
|
"type": "object",
|
|
@@ -336,6 +336,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
336
336
|
env={
|
|
337
337
|
"LANGSERVE_GRAPHS": paths_str,
|
|
338
338
|
"LANGCHAIN_CALLBACKS_BACKGROUND": "true",
|
|
339
|
+
"NODE_ENV": "development" if watch else "production",
|
|
339
340
|
"CHOKIDAR_USEPOLLING": "true",
|
|
340
341
|
**os.environ,
|
|
341
342
|
},
|
|
@@ -296,6 +296,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
296
296
|
env={
|
|
297
297
|
"LANGSERVE_GRAPHS": paths_str,
|
|
298
298
|
"LANGCHAIN_CALLBACKS_BACKGROUND": "true",
|
|
299
|
+
"NODE_ENV": "development" if watch else "production",
|
|
299
300
|
"CHOKIDAR_USEPOLLING": "true",
|
|
300
301
|
**os.environ,
|
|
301
302
|
},
|