langgraph-api 0.0.23__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.

Files changed (98) hide show
  1. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/PKG-INFO +2 -2
  2. langgraph_api-0.0.25/langgraph_api/api/__init__.py +135 -0
  3. langgraph_api-0.0.25/langgraph_api/api/openapi.py +314 -0
  4. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/custom.py +2 -0
  5. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/cli.py +9 -2
  6. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/config.py +37 -3
  7. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/remote_new.py +1 -0
  8. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/remote_old.py +1 -0
  9. langgraph_api-0.0.25/langgraph_api/server.py +139 -0
  10. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/utils.py +29 -0
  11. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/openapi.json +1 -0
  12. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/pyproject.toml +4 -3
  13. langgraph_api-0.0.23/langgraph_api/api/__init__.py +0 -64
  14. langgraph_api-0.0.23/langgraph_api/api/openapi.py +0 -69
  15. langgraph_api-0.0.23/langgraph_api/server.py +0 -67
  16. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/LICENSE +0 -0
  17. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/README.md +0 -0
  18. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/__init__.py +0 -0
  19. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/api/assistants.py +0 -0
  20. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/api/meta.py +0 -0
  21. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/api/runs.py +0 -0
  22. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/api/store.py +0 -0
  23. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/api/threads.py +0 -0
  24. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/asyncio.py +0 -0
  25. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/__init__.py +0 -0
  26. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/langsmith/__init__.py +0 -0
  27. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/langsmith/backend.py +0 -0
  28. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/langsmith/client.py +0 -0
  29. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/middleware.py +0 -0
  30. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/noop.py +0 -0
  31. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/auth/studio_user.py +0 -0
  32. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/cron_scheduler.py +0 -0
  33. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/errors.py +0 -0
  34. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/graph.py +0 -0
  35. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/http.py +0 -0
  36. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/.gitignore +0 -0
  37. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/base.py +0 -0
  38. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/build.mts +0 -0
  39. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/client.mts +0 -0
  40. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/client.new.mts +0 -0
  41. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/errors.py +0 -0
  42. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/global.d.ts +0 -0
  43. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/package.json +0 -0
  44. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/remote.py +0 -0
  45. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/schema.py +0 -0
  46. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/server_sent_events.py +0 -0
  47. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/graph.mts +0 -0
  48. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/hooks.mjs +0 -0
  49. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/parser/parser.mts +0 -0
  50. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
  51. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/schema/types.mts +0 -0
  52. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/schema/types.template.mts +0 -0
  53. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/utils/importMap.mts +0 -0
  54. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  55. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/src/utils/serde.mts +0 -0
  56. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/api.test.mts +0 -0
  57. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/compose-postgres.yml +0 -0
  58. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/.gitignore +0 -0
  59. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/agent.mts +0 -0
  60. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/delay.mts +0 -0
  61. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/error.mts +0 -0
  62. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
  63. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/nested.mts +0 -0
  64. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/package.json +0 -0
  65. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/weather.mts +0 -0
  66. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/graphs/yarn.lock +0 -0
  67. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/parser.test.mts +0 -0
  68. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/tests/utils.mts +0 -0
  69. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/js/yarn.lock +0 -0
  70. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/lifespan.py +0 -0
  71. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/logging.py +0 -0
  72. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/metadata.py +0 -0
  73. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/middleware/__init__.py +0 -0
  74. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/middleware/http_logger.py +0 -0
  75. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/middleware/private_network.py +0 -0
  76. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/models/__init__.py +0 -0
  77. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/models/run.py +0 -0
  78. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/patch.py +0 -0
  79. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/queue.py +0 -0
  80. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/route.py +0 -0
  81. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/schema.py +0 -0
  82. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/serde.py +0 -0
  83. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/sse.py +0 -0
  84. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/state.py +0 -0
  85. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/stream.py +0 -0
  86. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_api/validation.py +0 -0
  87. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_license/__init__.py +0 -0
  88. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_license/middleware.py +0 -0
  89. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_license/validation.py +0 -0
  90. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/__init__.py +0 -0
  91. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/checkpoint.py +0 -0
  92. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/database.py +0 -0
  93. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/ops.py +0 -0
  94. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/queue.py +0 -0
  95. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/retry.py +0 -0
  96. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/store.py +0 -0
  97. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/langgraph_storage/ttl_dict.py +0 -0
  98. {langgraph_api-0.0.23 → langgraph_api-0.0.25}/logging.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-api
3
- Version: 0.0.23
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.7,<3.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
@@ -375,6 +375,8 @@ def _solve_fastapi_dependencies(
375
375
 
376
376
  @functools.lru_cache(maxsize=1)
377
377
  def _depends() -> Any:
378
+ if "fastapi" not in sys.modules:
379
+ return None
378
380
  try:
379
381
  from fastapi.params import Depends
380
382
 
@@ -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
- from typing import Any, TypedDict
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
- **kwargs: Any,
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 = env("CORS_CONFIG", cast=_parse_json, default=None)
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
  },