langgraph-api 0.0.3__tar.gz → 0.0.5__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 (85) hide show
  1. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/PKG-INFO +2 -2
  2. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/assistants.py +3 -3
  3. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/store.py +2 -0
  4. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/cli.py +96 -10
  5. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/graph.py +145 -4
  6. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/package.json +1 -1
  7. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/yarn.lock +4 -4
  8. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/server.py +0 -1
  9. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/stream.py +13 -9
  10. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/database.py +8 -0
  11. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/ops.py +7 -3
  12. langgraph_api-0.0.5/langgraph_storage/store.py +62 -0
  13. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/openapi.json +15 -6
  14. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/pyproject.toml +2 -2
  15. langgraph_api-0.0.3/langgraph_storage/store.py +0 -28
  16. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/LICENSE +0 -0
  17. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/README.md +0 -0
  18. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/__init__.py +0 -0
  19. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/__init__.py +0 -0
  20. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/meta.py +0 -0
  21. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/openapi.py +0 -0
  22. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/runs.py +0 -0
  23. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/api/threads.py +0 -0
  24. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/asyncio.py +0 -0
  25. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/auth/__init__.py +0 -0
  26. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/auth/langsmith/__init__.py +0 -0
  27. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/auth/langsmith/backend.py +0 -0
  28. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/auth/langsmith/client.py +0 -0
  29. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/auth/middleware.py +0 -0
  30. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/auth/noop.py +0 -0
  31. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/config.py +0 -0
  32. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/cron_scheduler.py +0 -0
  33. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/errors.py +0 -0
  34. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/http.py +0 -0
  35. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/http_logger.py +0 -0
  36. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/.gitignore +0 -0
  37. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/build.mts +0 -0
  38. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/client.mts +0 -0
  39. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/global.d.ts +0 -0
  40. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/remote.py +0 -0
  41. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/server_sent_events.py +0 -0
  42. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/graph.mts +0 -0
  43. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/hooks.mjs +0 -0
  44. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/parser/parser.mts +0 -0
  45. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
  46. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/schema/types.mts +0 -0
  47. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/schema/types.template.mts +0 -0
  48. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/utils/importMap.mts +0 -0
  49. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  50. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/src/utils/serde.mts +0 -0
  51. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/api.test.mts +0 -0
  52. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/compose-postgres.yml +0 -0
  53. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/.gitignore +0 -0
  54. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/agent.mts +0 -0
  55. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/error.mts +0 -0
  56. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
  57. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/nested.mts +0 -0
  58. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/package.json +0 -0
  59. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/weather.mts +0 -0
  60. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/graphs/yarn.lock +0 -0
  61. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/parser.test.mts +0 -0
  62. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/js/tests/utils.mts +0 -0
  63. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/lifespan.py +0 -0
  64. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/logging.py +0 -0
  65. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/metadata.py +0 -0
  66. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/models/__init__.py +0 -0
  67. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/models/run.py +0 -0
  68. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/patch.py +0 -0
  69. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/queue.py +0 -0
  70. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/route.py +0 -0
  71. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/schema.py +0 -0
  72. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/serde.py +0 -0
  73. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/sse.py +0 -0
  74. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/state.py +0 -0
  75. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/utils.py +0 -0
  76. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_api/validation.py +0 -0
  77. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_license/__init__.py +0 -0
  78. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_license/middleware.py +0 -0
  79. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_license/validation.py +0 -0
  80. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/__init__.py +0 -0
  81. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/checkpoint.py +0 -0
  82. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/queue.py +0 -0
  83. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/retry.py +0 -0
  84. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/langgraph_storage/ttl_dict.py +0 -0
  85. {langgraph_api-0.0.3 → langgraph_api-0.0.5}/logging.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-api
3
- Version: 0.0.3
3
+ Version: 0.0.5
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.52,<0.3.0)
18
- Requires-Dist: langgraph-checkpoint (>=2.0.5,<3.0)
18
+ Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0)
19
19
  Requires-Dist: langsmith (>=0.1.63,<0.2.0)
20
20
  Requires-Dist: orjson (>=3.10.1)
21
21
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -121,7 +121,7 @@ async def get_assistant_graph(
121
121
  assistant_ = await Assistants.get(conn, assistant_id)
122
122
  assistant = await fetchone(assistant_)
123
123
  config = await ajson_loads(assistant["config"])
124
- graph = get_graph(assistant["graph_id"], config)
124
+ graph = await get_graph(assistant["graph_id"], config)
125
125
 
126
126
  xray: bool | int = False
127
127
  xray_query = request.query_params.get("xray")
@@ -156,7 +156,7 @@ async def get_assistant_subgraphs(
156
156
  assistant_ = await Assistants.get(conn, assistant_id)
157
157
  assistant = await fetchone(assistant_)
158
158
  config = await ajson_loads(assistant["config"])
159
- graph = get_graph(assistant["graph_id"], config)
159
+ graph = await get_graph(assistant["graph_id"], config)
160
160
  namespace = request.path_params.get("namespace")
161
161
 
162
162
  if isinstance(graph, RemotePregel):
@@ -191,7 +191,7 @@ async def get_assistant_schemas(
191
191
  assistant_ = await Assistants.get(conn, assistant_id)
192
192
  assistant = await fetchone(assistant_)
193
193
  config = await ajson_loads(assistant["config"])
194
- graph = get_graph(assistant["graph_id"], config)
194
+ graph = await get_graph(assistant["graph_id"], config)
195
195
 
196
196
  if isinstance(graph, RemotePregel):
197
197
  schemas = await graph.fetch_state_schema()
@@ -73,12 +73,14 @@ async def search_items(request: ApiRequest):
73
73
  filter = payload.get("filter")
74
74
  limit = payload.get("limit") or 10
75
75
  offset = payload.get("offset") or 0
76
+ query = payload.get("query")
76
77
  async with connect() as conn:
77
78
  items = await Store(conn).asearch(
78
79
  namespace_prefix,
79
80
  filter=filter,
80
81
  limit=limit,
81
82
  offset=offset,
83
+ query=query,
82
84
  )
83
85
  return ApiResponse({"items": [item.dict() for item in items]})
84
86
 
@@ -5,11 +5,34 @@ import os
5
5
  import pathlib
6
6
  import threading
7
7
  from collections.abc import Mapping, Sequence
8
+ from typing import Any, TypedDict
8
9
 
9
10
  logging.basicConfig(level=logging.INFO)
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
13
 
14
+ def _get_org_id() -> str | None:
15
+ from langsmith.client import Client
16
+ from langsmith.utils import tracing_is_enabled
17
+
18
+ # Yes, the organizationId is actually the workspace iD
19
+ # which is actually the tenantID which we actually get via
20
+ # the sessions endpoint
21
+ if not tracing_is_enabled():
22
+ return
23
+ client = Client()
24
+ try:
25
+ response = client.request_with_retries(
26
+ "GET", "/api/v1/sessions", params={"limit": 1}
27
+ )
28
+ result = response.json()
29
+ if result:
30
+ return result[0]["tenant_id"]
31
+ except Exception as e:
32
+ logger.debug("Failed to get organization ID: %s", str(e))
33
+ return None
34
+
35
+
13
36
  @contextlib.contextmanager
14
37
  def patch_environment(**kwargs):
15
38
  """Temporarily patch environment variables.
@@ -37,6 +60,44 @@ def patch_environment(**kwargs):
37
60
  os.environ[key] = value
38
61
 
39
62
 
63
+ class IndexConfig(TypedDict, total=False):
64
+ """Configuration for indexing documents for semantic search in the store."""
65
+
66
+ dims: int
67
+ """Number of dimensions in the embedding vectors.
68
+
69
+ Common embedding models have the following dimensions:
70
+ - OpenAI text-embedding-3-large: 256, 1024, or 3072
71
+ - OpenAI text-embedding-3-small: 512 or 1536
72
+ - OpenAI text-embedding-ada-002: 1536
73
+ - Cohere embed-english-v3.0: 1024
74
+ - Cohere embed-english-light-v3.0: 384
75
+ - Cohere embed-multilingual-v3.0: 1024
76
+ - Cohere embed-multilingual-light-v3.0: 384
77
+ """
78
+
79
+ embed: str
80
+ """Either a path to an embedding model (./path/to/file.py:embedding_model)
81
+ or a name of an embedding model (openai:text-embedding-3-small)
82
+
83
+ Note: LangChain is required to use the model format specification.
84
+ """
85
+
86
+ fields: list[str] | None
87
+ """Fields to extract text from for embedding generation.
88
+
89
+ Defaults to the root ["$"], which embeds the json object as a whole.
90
+ """
91
+
92
+
93
+ class StoreConfig(TypedDict, total=False):
94
+ index: IndexConfig
95
+
96
+
97
+ class Config(TypedDict, total=False):
98
+ store: StoreConfig | None
99
+
100
+
40
101
  def run_server(
41
102
  host: str = "127.0.0.1",
42
103
  port: int = 2024,
@@ -49,6 +110,8 @@ def run_server(
49
110
  env: str | pathlib.Path | Mapping[str, str] | None = None,
50
111
  reload_includes: Sequence[str] | None = None,
51
112
  reload_excludes: Sequence[str] | None = None,
113
+ config: Config | None = None,
114
+ **kwargs: Any,
52
115
  ):
53
116
  """Run the LangGraph API server."""
54
117
  import uvicorn
@@ -92,19 +155,41 @@ def run_server(
92
155
  studio_url = f"https://smith.langchain.com/studio/?baseUrl={local_url}"
93
156
 
94
157
  def _open_browser():
158
+ nonlocal studio_url
95
159
  import time
96
160
  import urllib.request
97
161
  import webbrowser
162
+ from concurrent.futures import ThreadPoolExecutor
163
+
164
+ thread_logger = logging.getLogger("browser_opener")
165
+ if not thread_logger.handlers:
166
+ handler = logging.StreamHandler()
167
+ handler.setFormatter(logging.Formatter("%(message)s"))
168
+ thread_logger.addHandler(handler)
98
169
 
99
- while True:
100
- try:
101
- with urllib.request.urlopen(f"{local_url}/ok") as response:
102
- if response.status == 200:
103
- webbrowser.open(studio_url)
104
- return
105
- except urllib.error.URLError:
106
- pass
107
- time.sleep(0.1)
170
+ with ThreadPoolExecutor(max_workers=1) as executor:
171
+ org_id_future = executor.submit(_get_org_id)
172
+
173
+ while True:
174
+ try:
175
+ with urllib.request.urlopen(f"{local_url}/ok") as response:
176
+ if response.status == 200:
177
+ try:
178
+ org_id = org_id_future.result(timeout=3.0)
179
+ if org_id:
180
+ studio_url = f"https://smith.langchain.com/studio/?baseUrl={local_url}&organizationId={org_id}"
181
+ except TimeoutError as e:
182
+ thread_logger.debug(
183
+ f"Failed to get organization ID: {str(e)}"
184
+ )
185
+ pass
186
+ thread_logger.info("🎨 Opening Studio in your browser...")
187
+ thread_logger.info("URL: " + studio_url)
188
+ webbrowser.open(studio_url)
189
+ return
190
+ except urllib.error.URLError:
191
+ pass
192
+ time.sleep(0.1)
108
193
 
109
194
  welcome = f"""
110
195
 
@@ -128,13 +213,13 @@ For production use, please use LangGraph Cloud.
128
213
  DATABASE_URI=":memory:",
129
214
  REDIS_URI="fake",
130
215
  N_JOBS_PER_WORKER=str(n_jobs_per_worker if n_jobs_per_worker else 1),
216
+ LANGGRAPH_CONFIG=json.dumps(config) if config else None,
131
217
  LANGSERVE_GRAPHS=json.dumps(graphs) if graphs else None,
132
218
  LANGSMITH_LANGGRAPH_API_VARIANT="local_dev",
133
219
  **(env_vars or {}),
134
220
  ):
135
221
  if open_browser:
136
222
  threading.Thread(target=_open_browser, daemon=True).start()
137
-
138
223
  uvicorn.run(
139
224
  "langgraph_api.server:app",
140
225
  host=host,
@@ -158,6 +243,7 @@ For production use, please use LangGraph Cloud.
158
243
  },
159
244
  "root": {"handlers": ["console"]},
160
245
  },
246
+ **kwargs,
161
247
  )
162
248
 
163
249
 
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import functools
2
3
  import glob
3
4
  import importlib.util
4
5
  import inspect
@@ -8,7 +9,7 @@ import sys
8
9
  from collections.abc import Callable
9
10
  from itertools import filterfalse
10
11
  from random import choice
11
- from typing import NamedTuple
12
+ from typing import TYPE_CHECKING, NamedTuple
12
13
  from uuid import UUID, uuid5
13
14
 
14
15
  import structlog
@@ -22,6 +23,9 @@ from starlette.exceptions import HTTPException
22
23
  from langgraph_api.js.remote import RemotePregel
23
24
  from langgraph_api.schema import Config
24
25
 
26
+ if TYPE_CHECKING:
27
+ from langchain_core.embeddings import Embeddings
28
+
25
29
  logger = structlog.stdlib.get_logger(__name__)
26
30
 
27
31
  GraphFactoryFromConfig = Callable[[Config], Pregel | Graph]
@@ -55,7 +59,7 @@ async def register_graph(graph_id: str, graph: GraphValue, config: dict | None)
55
59
  )
56
60
 
57
61
 
58
- def get_graph(
62
+ async def get_graph(
59
63
  graph_id: str,
60
64
  config: Config,
61
65
  *,
@@ -67,6 +71,8 @@ def get_graph(
67
71
  value = GRAPHS[graph_id]
68
72
  if graph_id in FACTORY_ACCEPTS_CONFIG:
69
73
  value = value(config) if FACTORY_ACCEPTS_CONFIG[graph_id] else value()
74
+ if asyncio.iscoroutine(value):
75
+ value = await value
70
76
  if isinstance(value, Graph):
71
77
  value = value.compile()
72
78
  if not isinstance(value, Pregel) and not isinstance(value, RemotePregel):
@@ -269,7 +275,7 @@ def _graph_from_spec(spec: GraphSpec) -> GraphValue:
269
275
  sys.modules[modname] = module
270
276
  modspec.loader.exec_module(module)
271
277
  except ImportError as e:
272
- e.add_note(f"Could not import python module for graph: {spec}")
278
+ e.add_note(f"Could not import python module for graph:\n{spec}")
273
279
  if os.environ.get("LANGSMITH_LANGGRAPH_API_VARIANT") == "local_dev":
274
280
  e.add_note(
275
281
  "This error likely means you haven't installed your project and its dependencies yet. Before running the server, install your project:\n\n"
@@ -279,11 +285,44 @@ def _graph_from_spec(spec: GraphSpec) -> GraphValue:
279
285
  "python -m pip install -e .\n\n"
280
286
  "Make sure to run this command from your project's root directory (where your setup.py or pyproject.toml is located)"
281
287
  )
288
+ raise
289
+ except FileNotFoundError as e:
290
+ e.add_note(f"Could not find python file for graph: {spec}")
291
+ raise
282
292
  else:
283
293
  raise ValueError("Graph specification must have a path or module")
284
294
 
285
295
  if spec.variable:
286
- graph: GraphValue = getattr(module, spec.variable)
296
+ try:
297
+ graph: GraphValue = module.__dict__[spec.variable]
298
+ except KeyError as e:
299
+ available = [k for k in module.__dict__ if not k.startswith("__")]
300
+ suggestion = ""
301
+ if available:
302
+ likely = [
303
+ k
304
+ for k in available
305
+ if isinstance(module.__dict__[k], Graph | Pregel)
306
+ ]
307
+ if likely:
308
+ prefix = spec.module or spec.path
309
+ likely_ = "\n".join(
310
+ [f"\t- {prefix}:{k}" if prefix else k for k in likely]
311
+ )
312
+ suggestion = (
313
+ f"\nDid you mean to use one of the following?\n{likely_}"
314
+ )
315
+ elif available:
316
+ suggestion = (
317
+ f"\nFound the following exports: {', '.join(available)}"
318
+ )
319
+
320
+ raise ValueError(
321
+ f"Could not find graph '{spec.variable}' in '{spec.path}'. "
322
+ f"Please check that:\n"
323
+ f"1. The file exports a variable named '{spec.variable}'\n"
324
+ f"2. The variable name in your config matches the export name{suggestion}"
325
+ ) from e
287
326
  if callable(graph):
288
327
  sig = inspect.signature(graph)
289
328
  if not sig.parameters:
@@ -322,3 +361,105 @@ def _graph_from_spec(spec: GraphSpec) -> GraphValue:
322
361
  )
323
362
 
324
363
  return graph
364
+
365
+
366
+ def _load(path: str, variable: str) -> None:
367
+ modname = "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(24))
368
+ modspec = importlib.util.spec_from_file_location(modname, path)
369
+ if modspec is None:
370
+ raise ValueError(f"Could not find python file for embeddings: {path}")
371
+ module = importlib.util.module_from_spec(modspec)
372
+ sys.modules[modname] = module
373
+ modspec.loader.exec_module(module)
374
+ return module.__dict__[variable]
375
+
376
+
377
+ @functools.lru_cache
378
+ def _get_init_embeddings() -> Callable[[str, ...], "Embeddings"] | None:
379
+ try:
380
+ from langchain.embeddings import init_embeddings
381
+
382
+ return init_embeddings
383
+ except ImportError:
384
+ return None
385
+
386
+
387
+ def resolve_embeddings(index_config: dict) -> "Embeddings":
388
+ """Return embeddings from config.
389
+
390
+ Args:
391
+ index_config: Configuration for the vector store index
392
+ Must contain an "embed" key specifying either:
393
+ - A path to a Python file and function (e.g. "./embeddings.py:get_embeddings")
394
+ - A LangChain embeddings identifier (e.g. "openai:text-embedding-3-small")
395
+
396
+ Returns:
397
+ Embeddings: A LangChain embeddings instance
398
+
399
+ Raises:
400
+ ValueError: If embeddings cannot be loaded from the config
401
+ """
402
+ from langchain_core.embeddings import Embeddings
403
+ from langgraph.store.base import ensure_embeddings
404
+
405
+ embed: str = index_config["embed"]
406
+ if ".py:" in embed:
407
+ module_name, function = embed.rsplit(":", 1)
408
+ module_name = module_name.rstrip(":")
409
+
410
+ try:
411
+ if "/" in module_name:
412
+ # Load from file path
413
+ modname = "".join(
414
+ choice("abcdefghijklmnopqrstuvwxyz") for _ in range(24)
415
+ )
416
+ modspec = importlib.util.spec_from_file_location(modname, module_name)
417
+ if modspec is None:
418
+ raise ValueError(f"Could not find embeddings file: {module_name}")
419
+ module = importlib.util.module_from_spec(modspec)
420
+ sys.modules[modname] = module
421
+ modspec.loader.exec_module(module)
422
+ else:
423
+ # Load from Python module
424
+ module = importlib.import_module(module_name)
425
+
426
+ embedding_fn = getattr(module, function, None)
427
+ if embedding_fn is None:
428
+ raise ValueError(
429
+ f"Could not find embeddings function '{function}' in module: {module_name}"
430
+ )
431
+
432
+ if isinstance(embedding_fn, Embeddings):
433
+ return embedding_fn
434
+ elif not callable(embedding_fn):
435
+ raise ValueError(
436
+ f"Embeddings function '{function}' in module: {module_name} is not callable"
437
+ )
438
+
439
+ return ensure_embeddings(embedding_fn)
440
+
441
+ except ImportError as e:
442
+ e.add_note(f"Could not import embeddings module:\n{module_name}\n\n")
443
+ if os.environ.get("LANGSMITH_LANGGRAPH_API_VARIANT") == "local_dev":
444
+ e.add_note(
445
+ "If you're in development mode, make sure you've installed your project "
446
+ "and its dependencies:\n"
447
+ "- For requirements.txt: pip install -r requirements.txt\n"
448
+ "- For pyproject.toml: pip install -e .\n"
449
+ )
450
+ raise
451
+ except FileNotFoundError as e:
452
+ raise ValueError(f"Could not find embeddings file: {module_name}") from e
453
+
454
+ else:
455
+ # Load from LangChain embeddings
456
+ init_embeddings = _get_init_embeddings()
457
+ if init_embeddings is None:
458
+ raise ValueError(
459
+ f"Could not load LangChain embeddings '{embed}'. "
460
+ "Loading embeddings by provider:identifier requires the langchain package (>=0.3.9). "
461
+ "Install it with: pip install 'langchain>=0.3.9'"
462
+ " or specify 'embed' as a path to a "
463
+ "variable in a Python file instead."
464
+ )
465
+ return init_embeddings(embed)
@@ -10,7 +10,7 @@
10
10
  "@hono/node-server": "^1.12.0",
11
11
  "@hono/zod-validator": "^0.2.2",
12
12
  "@langchain/core": "^0.3.17",
13
- "@langchain/langgraph": "^0.2.20",
13
+ "@langchain/langgraph": "^0.2.23",
14
14
  "@types/json-schema": "^7.0.15",
15
15
  "@typescript/vfs": "^1.6.0",
16
16
  "dedent": "^1.5.3",
@@ -345,10 +345,10 @@
345
345
  p-retry "4"
346
346
  uuid "^9.0.0"
347
347
 
348
- "@langchain/langgraph@^0.2.20":
349
- version "0.2.22"
350
- resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.22.tgz#32834e147f16def2e9b491b2c09ecd349714d7f9"
351
- integrity sha512-iXA8p9xaWfGviBbZ6nOETqvGz2misjZ4xGxrWla98FF2aE8mq1jfMzWuEX37KZIfuQPCqt1iIh/Zit60T4mG3Q==
348
+ "@langchain/langgraph@^0.2.23":
349
+ version "0.2.23"
350
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.23.tgz#b892ae642c2ac49a44599ce18ab1897b14554f9f"
351
+ integrity sha512-aUD3G2cUSmrOb6xtJkd0AUctmHJXuyNPIzMR86JMEN591AtkHTVe2OhTxo15S8PPAQxJ7HrCJyMFd0EQECRASw==
352
352
  dependencies:
353
353
  "@langchain/langgraph-checkpoint" "~0.0.12"
354
354
  "@langchain/langgraph-sdk" "~0.0.21"
@@ -23,7 +23,6 @@ from langgraph_storage.retry import OVERLOADED_EXCEPTIONS
23
23
  logging.captureWarnings(True)
24
24
  logger = structlog.stdlib.get_logger(__name__)
25
25
 
26
-
27
26
  app = Starlette(
28
27
  routes=routes,
29
28
  lifespan=lifespan,
@@ -94,16 +94,16 @@ async def astream_state(
94
94
  ) -> AnyStream:
95
95
  """Stream messages from the runnable."""
96
96
  run_id = str(run["run_id"])
97
- await stack.enter_async_context(conn.pipeline())
97
+ pipe = await stack.enter_async_context(conn.pipeline())
98
98
  # extract args from run
99
99
  kwargs = run["kwargs"].copy()
100
100
  subgraphs = kwargs.get("subgraphs", False)
101
101
  temporary = kwargs.pop("temporary", False)
102
102
  config = kwargs.pop("config")
103
- graph = get_graph(
103
+ graph = await get_graph(
104
104
  config["configurable"]["graph_id"],
105
105
  config,
106
- store=None if not conn else Store(conn),
106
+ store=None if not conn else Store(conn, pipe=pipe),
107
107
  checkpointer=None if temporary else Checkpointer(conn),
108
108
  )
109
109
  input = kwargs.pop("input")
@@ -188,9 +188,11 @@ async def astream_state(
188
188
  messages[msg.id] = msg
189
189
  yield "messages/metadata", {msg.id: {"metadata": meta}}
190
190
  yield (
191
- "messages/partial"
192
- if isinstance(msg, BaseMessageChunk)
193
- else "messages/complete",
191
+ (
192
+ "messages/partial"
193
+ if isinstance(msg, BaseMessageChunk)
194
+ else "messages/complete"
195
+ ),
194
196
  [message_chunk_to_message(messages[msg.id])],
195
197
  )
196
198
  elif mode in stream_mode:
@@ -237,9 +239,11 @@ async def astream_state(
237
239
  messages[msg.id] = msg
238
240
  yield "messages/metadata", {msg.id: {"metadata": meta}}
239
241
  yield (
240
- "messages/partial"
241
- if isinstance(msg, BaseMessageChunk)
242
- else "messages/complete",
242
+ (
243
+ "messages/partial"
244
+ if isinstance(msg, BaseMessageChunk)
245
+ else "messages/complete"
246
+ ),
243
247
  [message_chunk_to_message(messages[msg.id])],
244
248
  )
245
249
  elif mode in stream_mode:
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import json
2
3
  import os
3
4
  import threading
4
5
  import uuid
@@ -13,6 +14,7 @@ import structlog
13
14
  from langgraph.checkpoint.memory import PersistentDict
14
15
 
15
16
  from langgraph_api.utils import AsyncConnectionProto
17
+ from langgraph_storage import store
16
18
  from langgraph_storage.queue import start_queue, stop_queue
17
19
 
18
20
  logger = structlog.stdlib.get_logger(__name__)
@@ -154,6 +156,12 @@ async def connect(*, __test__: bool = False) -> AsyncIterator[AsyncConnectionPro
154
156
 
155
157
 
156
158
  async def start_pool() -> None:
159
+ if store._STORE_CONFIG is None:
160
+ if (config_val := os.getenv("LANGGRAPH_CONFIG")) and config_val.strip():
161
+ config_ = json.loads(config_val.strip())
162
+ if "store" in config_:
163
+ store.set_store_config(config_["store"])
164
+
157
165
  if not os.path.exists(".langgraph_api"):
158
166
  os.mkdir(".langgraph_api")
159
167
  if os.path.exists(OPS_FILENAME):
@@ -719,7 +719,9 @@ class Threads:
719
719
  if graph_id := metadata.get("graph_id"):
720
720
  # format latest checkpoint for response
721
721
  checkpointer.latest_iter = checkpoint
722
- graph = get_graph(graph_id, thread_config, checkpointer=checkpointer)
722
+ graph = await get_graph(
723
+ graph_id, thread_config, checkpointer=checkpointer
724
+ )
723
725
  result = await graph.aget_state(config, subgraphs=subgraphs)
724
726
  if (
725
727
  result.metadata is not None
@@ -766,7 +768,9 @@ class Threads:
766
768
  config["configurable"].setdefault("graph_id", graph_id)
767
769
 
768
770
  checkpointer.latest_iter = checkpoint
769
- graph = get_graph(graph_id, thread_config, checkpointer=checkpointer)
771
+ graph = await get_graph(
772
+ graph_id, thread_config, checkpointer=checkpointer
773
+ )
770
774
  update_config = config.copy()
771
775
  update_config["configurable"] = {
772
776
  **config["configurable"],
@@ -820,7 +824,7 @@ class Threads:
820
824
  thread_config = thread["config"]
821
825
  # If graph_id exists, get state history
822
826
  if graph_id := thread_metadata.get("graph_id"):
823
- graph = get_graph(
827
+ graph = await get_graph(
824
828
  graph_id, thread_config, checkpointer=Checkpointer(conn)
825
829
  )
826
830
 
@@ -0,0 +1,62 @@
1
+ import os
2
+ from collections import defaultdict
3
+ from typing import Any
4
+
5
+ from langgraph.checkpoint.memory import PersistentDict
6
+ from langgraph.store.memory import InMemoryStore
7
+
8
+ from langgraph_api.graph import resolve_embeddings
9
+
10
+ _STORE_CONFIG = None
11
+
12
+
13
+ class DiskBackedInMemStore(InMemoryStore):
14
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
15
+ super().__init__(*args, **kwargs)
16
+ self._data = PersistentDict(dict, filename=_STORE_FILE)
17
+ self._vectors = PersistentDict(lambda: defaultdict(dict), filename=_VECTOR_FILE)
18
+ self._load_data(self._data, which="data")
19
+ self._load_data(self._vectors, which="vectors")
20
+
21
+ def _load_data(self, container: PersistentDict, which: str) -> None:
22
+ if not container.filename:
23
+ return
24
+ try:
25
+ container.load()
26
+ except FileNotFoundError:
27
+ # It's okay if the file doesn't exist yet
28
+ pass
29
+
30
+ except (EOFError, ValueError) as e:
31
+ raise RuntimeError(
32
+ f"Failed to load store {which} from {container.filename}. "
33
+ "This may be due to changes in the stored data structure. "
34
+ "Consider clearing the local store by running: rm -rf .langgraph_api"
35
+ ) from e
36
+ except Exception as e:
37
+ raise RuntimeError(
38
+ f"Unexpected error loading store {which} from {container.filename}: {str(e)}"
39
+ ) from e
40
+
41
+ def close(self) -> None:
42
+ self._data.close()
43
+ self._vectors.close()
44
+
45
+
46
+ _STORE_FILE = os.path.join(".langgraph_api", "store.pckl")
47
+ _VECTOR_FILE = os.path.join(".langgraph_api", "store.vectors.pckl")
48
+ os.makedirs(".langgraph_api", exist_ok=True)
49
+ STORE = DiskBackedInMemStore()
50
+
51
+
52
+ def set_store_config(config) -> None:
53
+ global _STORE_CONFIG, STORE
54
+ _STORE_CONFIG = config.copy()
55
+ _STORE_CONFIG["index"]["embed"] = resolve_embeddings(_STORE_CONFIG.get("index", {}))
56
+ # Re-create the store
57
+ STORE.close()
58
+ STORE = DiskBackedInMemStore(index=_STORE_CONFIG.get("index", {}))
59
+
60
+
61
+ def Store(*args: Any, **kwargs: Any) -> DiskBackedInMemStore:
62
+ return STORE
@@ -1557,8 +1557,11 @@
1557
1557
  "200": {
1558
1558
  "description": "Success",
1559
1559
  "content": {
1560
- "application/json": {
1561
- "schema": {}
1560
+ "text/event-stream": {
1561
+ "schema": {
1562
+ "type": "string",
1563
+ "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
1564
+ }
1562
1565
  }
1563
1566
  }
1564
1567
  },
@@ -1905,8 +1908,11 @@
1905
1908
  "200": {
1906
1909
  "description": "Success",
1907
1910
  "content": {
1908
- "application/json": {
1909
- "schema": {}
1911
+ "text/event-stream": {
1912
+ "schema": {
1913
+ "type": "string",
1914
+ "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
1915
+ }
1910
1916
  }
1911
1917
  }
1912
1918
  },
@@ -2143,8 +2149,11 @@
2143
2149
  "200": {
2144
2150
  "description": "Success",
2145
2151
  "content": {
2146
- "application/json": {
2147
- "schema": {}
2152
+ "text/event-stream": {
2153
+ "schema": {
2154
+ "type": "string",
2155
+ "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
2156
+ }
2148
2157
  }
2149
2158
  }
2150
2159
  },
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-api"
3
- version = "0.0.3"
3
+ version = "0.0.5"
4
4
  description = ""
5
5
  authors = [
6
6
  "Nuno Campos <nuno@langchain.dev>",
@@ -24,7 +24,7 @@ sse-starlette = "^2.1.0"
24
24
  starlette = ">=0.38.6"
25
25
  watchfiles = ">=0.13"
26
26
  langgraph = ">=0.2.52,<0.3.0"
27
- langgraph-checkpoint = ">=2.0.5,<3.0"
27
+ langgraph-checkpoint = ">=2.0.7,<3.0"
28
28
  orjson = ">=3.10.1"
29
29
  uvicorn = ">=0.26.0"
30
30
  langsmith = "^0.1.63"
@@ -1,28 +0,0 @@
1
- import os
2
-
3
- from langgraph.checkpoint.memory import PersistentDict
4
- from langgraph.store.memory import InMemoryStore
5
-
6
-
7
- class DiskBackedInMemStore(InMemoryStore):
8
- def __init__(self, *args, filename=None, **kwargs):
9
- super().__init__(*args, **kwargs)
10
- self.filename = filename
11
- self._data = PersistentDict(dict, filename=self.filename)
12
- try:
13
- self._data.load()
14
- except FileNotFoundError:
15
- pass
16
-
17
- def close(self):
18
- self._data.close()
19
-
20
-
21
- _STORE_FILE = os.path.join(".langgraph_api", "store.pckl")
22
- if not os.path.exists(".langgraph_api"):
23
- os.mkdir(".langgraph_api")
24
- STORE = DiskBackedInMemStore(filename=_STORE_FILE)
25
-
26
-
27
- def Store(*args, **kwargs):
28
- return STORE
File without changes
File without changes