Compare commits

..
Author SHA1 Message Date
Brian Vander Schaaf 0231165daa chore: typo fix 2025-03-02 22:57:50 -05:00
45 changed files with 339 additions and 2910 deletions
-38
View File
@@ -114,42 +114,6 @@ jobs:
- name: Run check_sdk_methods script
run: python .github/scripts/check_sdk_methods.py
check-schema:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check CLI schema hasn't changed #${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.11"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.11"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: schema-check-cli
- name: Install CLI dependencies
run: |
cd libs/cli
poetry install
- name: Generate schema and check for changes
run: |
cd libs/cli
# Create a temporary copy of the current schema
cp schemas/schema.json schemas/schema.current.json
# Generate new schema
poetry run python generate_schema.py
# Compare the new schema with the original
if ! diff -q schemas/schema.json schemas/schema.current.json > /dev/null; then
echo "Error: Langgraph.json configuration schema has changed. Please run 'poetry run python generate_schema.py' in the libs/cli directory and commit the changes."
diff schemas/schema.json schemas/schema.current.json
exit 1
fi
echo "Schema check passed - no changes detected"
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true'
@@ -216,8 +180,6 @@ jobs:
test,
test-langgraph,
test-scheduler-kafka,
check-sdk-methods,
check-schema,
integration-test,
test-js,
]
+1 -7
View File
@@ -23,10 +23,4 @@ packages:
description: "Build swarm-style multi-agent systems using LangGraph."
- name: "delve-taxonomy-generator"
repo: "andrestorres123/delve"
description: "A taxonomy generator for unstructured data"
- name: "nodeology"
repo: "xyin-anl/Nodeology"
description: "Enable researcher to build scientific workflows easily with simplified interface."
- name: "langgraph-bigtool"
repo: "langchain-ai/langgraph-bigtool"
description: "Build LangGraph agents with large numbers of tools."
description: "A taxonomy generator for unstructured data"
@@ -17,7 +17,7 @@ This guide explains how to add semantic search to your LangGraph deployment's cr
...
"store": {
"index": {
"embed": "openai:text-embedding-3-small",
"embed": "openai:text-embeddings-3-small",
"dims": 1536,
"fields": ["$"]
}
@@ -27,7 +27,7 @@ This guide explains how to add semantic search to your LangGraph deployment's cr
This configuration:
- Uses OpenAI's text-embedding-3-small model for generating embeddings
- Uses OpenAI's text-embeddings-3-small model for generating embeddings
- Sets the embedding dimension to 1536 (matching the model's output)
- Indexes all fields in your stored data (`["$"]` means index everything, or specify specific fields like `["text", "metadata.title"]`)
+1 -1
View File
@@ -532,7 +532,7 @@
"\n",
"Recall that **edges** route the control flow from one node to the next. **Conditional edges** usually contain \"if\" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next.\n",
"\n",
"Below, call define a router function called `route_tools`, that checks for tool_calls in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. \n",
"Below, define a router function called `route_tools` that checks for tool_calls in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. \n",
"\n",
"The condition will route to `tools` if tool calls are present and `END` if not.\n",
"\n",
@@ -20,7 +20,7 @@ from langgraph.store.base import (
)
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.postgres.base import (
PLACEHOLDER,
_PLACEHOLDER,
BasePostgresStore,
PoolConfig,
PostgresIndexConfig,
@@ -360,7 +360,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
for (idx, _), vector in zip(embedding_requests, vectors):
_paramslist = queries[idx][1]
for i in range(len(_paramslist)):
if _paramslist[i] is PLACEHOLDER:
if _paramslist[i] is _PLACEHOLDER:
_paramslist[i] = vector
for (idx, _), (query, params) in zip(search_ops, queries):
@@ -370,7 +370,7 @@ class BasePostgresStore(Generic[C]):
if op.query and self.index_config:
embedding_requests.append((idx, op.query))
score_operator, post_operator = get_distance_operator(self)
score_operator, post_operator = _get_distance_operator(self)
vector_type = (
cast(PostgresIndexConfig, self.index_config)
.get("ann_index_config", {})
@@ -430,10 +430,10 @@ class BasePostgresStore(Generic[C]):
OFFSET %s
"""
params = [
PLACEHOLDER, # Vector placeholder
_PLACEHOLDER, # Vector placeholder
*ns_args,
*filter_params,
PLACEHOLDER,
_PLACEHOLDER,
expanded_limit,
op.limit,
op.offset,
@@ -828,7 +828,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
for (idx, _), embedding in zip(embedding_requests, embeddings):
_paramslist = queries[idx][1]
for i in range(len(_paramslist)):
if _paramslist[i] is PLACEHOLDER:
if _paramslist[i] is _PLACEHOLDER:
_paramslist[i] = embedding
for (idx, _), (query, params) in zip(search_ops, queries):
@@ -1055,7 +1055,7 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
return tuple(namespace.split("."))
def get_distance_operator(store: Any) -> tuple[str, str]:
def _get_distance_operator(store: Any) -> tuple[str, str]:
"""Get the distance operator and score expression based on config."""
# Note: Today, we are not using ANN indices due to restrictions
# on PGVector's support for mixing vector and non-vector filters
@@ -1121,4 +1121,4 @@ def _ensure_index_config(
return embeddings, index_config
PLACEHOLDER = object()
_PLACEHOLDER = object()
@@ -487,12 +487,7 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
except Exception:
return cls.construct(**tup[2])
except Exception:
# for pydantic objects we can't find/reconstruct
# let's return the kwargs dict instead
try:
return tup[2]
except NameError:
return
return
elif code == EXT_PYDANTIC_V2:
try:
tup = msgpack.unpackb(
@@ -505,12 +500,7 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
except Exception:
return cls.model_construct(**tup[2])
except Exception:
# for pydantic objects we can't find/reconstruct
# let's return the kwargs dict instead
try:
return tup[2]
except NameError:
return
return
def _msgpack_enc(data: Any) -> bytes:
@@ -166,13 +166,6 @@ class GetOp(NamedTuple):
"doc456" # For a document
```
"""
refresh_ttl: bool = True
"""Whether to refresh TTLs for the returned item.
If no TTL was specified for the original item(s),
or if TTL support is not enabled for your adapter,
this argument is ignored.
"""
class SearchOp(NamedTuple):
@@ -267,13 +260,6 @@ class SearchOp(NamedTuple):
- "technical documentation about REST APIs"
- "machine learning papers from 2023"
"""
refresh_ttl: bool = True
"""Whether to refresh TTLs for the returned item.
If no TTL was specified for the original item(s),
or if TTL support is not enabled for your adapter,
this argument is ignored.
"""
# Type representing a namespace path that can include wildcards
@@ -477,15 +463,6 @@ class PutOp(NamedTuple):
]
```
"""
ttl: Optional[float] = None
"""Controls the TTL (time-to-live) for the item in minutes.
If provided, and if the store you are using supports this feature, the item
will expire this many minutes after it was last accessed. The expiration timer
refreshes on both read operations (get/search) and write operations (put/update).
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
Defaults to None (no expiration).
"""
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
@@ -635,13 +612,8 @@ class BaseStore(ABC):
by providing an `index` configuration at creation time. Without this
configuration, semantic search is disabled and any `index` arguments
to storage operations will have no effect.
Similarly, TTL (time-to-live) support is disabled by default.
Subclasses must explicitly set `supports_ttl = True` to enable this feature.
"""
supports_ttl = False
__slots__ = ("__weakref__",)
@abstractmethod
@@ -668,21 +640,17 @@ class BaseStore(ABC):
The order of results matches the order of input operations.
"""
def get(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
) -> Optional[Item]:
def get(self, namespace: tuple[str, ...], key: str) -> Optional[Item]:
"""Retrieve a single item.
Args:
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
refresh_ttl: Whether to refresh TTLs for the returned item.
If no TTL is specified, this argument is ignored.
Returns:
The retrieved item or None if not found.
"""
return self.batch([GetOp(namespace, key, refresh_ttl)])[0]
return self.batch([GetOp(namespace, key)])[0]
def search(
self,
@@ -693,7 +661,6 @@ class BaseStore(ABC):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
) -> list[SearchItem]:
"""Search for items within a namespace prefix.
@@ -703,8 +670,6 @@ class BaseStore(ABC):
filter: Key-value pairs to filter results.
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
refresh_ttl: Whether to refresh TTLs for the returned items.
If no TTL is specified, this argument is ignored.
Returns:
List of items matching the search criteria.
@@ -742,9 +707,7 @@ class BaseStore(ABC):
Note: Natural language search support depends on your store implementation
and requires proper embedding configuration.
"""
return self.batch(
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
)[0]
return self.batch([SearchOp(namespace_prefix, filter, limit, offset, query)])[0]
def put(
self,
@@ -752,8 +715,6 @@ class BaseStore(ABC):
key: str,
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
) -> None:
"""Store or update an item in the store.
@@ -774,20 +735,12 @@ class BaseStore(ABC):
- Nested fields: "metadata.title"
- Array access: "chapters[*].content" (each indexed separately)
- Specific indices: "authors[0].name"
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
If specified, the item will expire after this many minutes from when it was last accessed.
None means no expiration. Expired runs will be deleted opportunistically.
By default, the expiration timer refreshes on both read operations (get/search)
and write operations (put/update), whenever the item is included in the operation.
Note:
Indexing support depends on your store implementation.
If you do not initialize the store with indexing capabilities,
the `index` parameter will be ignored.
Similarly, TTL support depends on the specific store implementation.
Some implementations may not support expiration of items.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
```python
@@ -806,12 +759,7 @@ class BaseStore(ABC):
```
"""
_validate_namespace(namespace)
if ttl is not None and not self.supports_ttl:
raise NotImplementedError(
f"TTL is not supported by {self.__class__.__name__}. "
f"Use a store implementation that supports TTL or set ttl=None."
)
self.batch([PutOp(namespace, key, value, index=index, ttl=ttl)])
self.batch([PutOp(namespace, key, value, index=index)])
def delete(self, namespace: tuple[str, ...], key: str) -> None:
"""Delete an item.
@@ -820,7 +768,7 @@ class BaseStore(ABC):
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
"""
self.batch([PutOp(namespace, key, None, ttl=None)])
self.batch([PutOp(namespace, key, None)])
def list_namespaces(
self,
@@ -875,9 +823,7 @@ class BaseStore(ABC):
)
return self.batch([op])[0]
async def aget(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
) -> Optional[Item]:
async def aget(self, namespace: tuple[str, ...], key: str) -> Optional[Item]:
"""Asynchronously retrieve a single item.
Args:
@@ -887,7 +833,7 @@ class BaseStore(ABC):
Returns:
The retrieved item or None if not found.
"""
return (await self.abatch([GetOp(namespace, key, refresh_ttl)]))[0]
return (await self.abatch([GetOp(namespace, key)]))[0]
async def asearch(
self,
@@ -898,7 +844,6 @@ class BaseStore(ABC):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
) -> list[SearchItem]:
"""Asynchronously search for items within a namespace prefix.
@@ -908,9 +853,6 @@ class BaseStore(ABC):
filter: Key-value pairs to filter results.
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
refresh_ttl: Whether to refresh TTLs for the returned items.
Defaults to True. If no TTL is specified, this argument
is ignored.
Returns:
List of items matching the search criteria.
@@ -950,7 +892,7 @@ class BaseStore(ABC):
"""
return (
await self.abatch(
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
[SearchOp(namespace_prefix, filter, limit, offset, query)]
)
)[0]
@@ -960,8 +902,6 @@ class BaseStore(ABC):
key: str,
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
) -> None:
"""Asynchronously store or update an item in the store.
@@ -982,20 +922,12 @@ class BaseStore(ABC):
- Nested fields: "metadata.title"
- Array access: "chapters[*].content" (each indexed separately)
- Specific indices: "authors[0].name"
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
If specified, the item will expire after this many minutes from when it was last accessed.
None means no expiration. Expired runs will be deleted opportunistically.
By default, the expiration timer refreshes on both read operations (get/search)
and write operations (put/update), whenever the item is included in the operation.
Note:
Indexing support depends on your store implementation.
If you do not initialize the store with indexing capabilities,
the `index` parameter will be ignored.
Similarly, TTL support depends on the specific store implementation.
Some implementations may not support expiration of items.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
```python
@@ -1022,12 +954,7 @@ class BaseStore(ABC):
```
"""
_validate_namespace(namespace)
if ttl is not None and not self.supports_ttl:
raise NotImplementedError(
f"TTL is not supported by {self.__class__.__name__}. "
f"Use a store implementation that supports TTL or set ttl=None."
)
await self.abatch([PutOp(namespace, key, value, index=index, ttl=ttl)])
await self.abatch([PutOp(namespace, key, value, index=index)])
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
"""Asynchronously delete an item.
+12 -29
View File
@@ -65,11 +65,13 @@ class AsyncBatchedBaseStore(BaseStore):
pass
async def aget(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
self,
namespace: tuple[str, ...],
key: str,
) -> Optional[Item]:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue.put_nowait((fut, GetOp(namespace, key, refresh_ttl=refresh_ttl)))
self._aqueue.put_nowait((fut, GetOp(namespace, key)))
return await fut
async def asearch(
@@ -81,22 +83,11 @@ class AsyncBatchedBaseStore(BaseStore):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
) -> list[SearchItem]:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue.put_nowait(
(
fut,
SearchOp(
namespace_prefix,
filter,
limit,
offset,
query,
refresh_ttl=refresh_ttl,
),
)
(fut, SearchOp(namespace_prefix, filter, limit, offset, query))
)
return await fut
@@ -106,13 +97,11 @@ class AsyncBatchedBaseStore(BaseStore):
key: str,
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
) -> None:
assert not self._task.done()
_validate_namespace(namespace)
fut = self._loop.create_future()
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index, ttl=ttl)))
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index)))
return await fut
async def adelete(
@@ -157,10 +146,12 @@ class AsyncBatchedBaseStore(BaseStore):
@_check_loop
def get(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
self,
namespace: tuple[str, ...],
key: str,
) -> Optional[Item]:
return asyncio.run_coroutine_threadsafe(
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
self.aget(namespace, key=key), self._loop
).result()
@_check_loop
@@ -173,16 +164,10 @@ class AsyncBatchedBaseStore(BaseStore):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
) -> list[SearchItem]:
return asyncio.run_coroutine_threadsafe(
self.asearch(
namespace_prefix,
query=query,
filter=filter,
limit=limit,
offset=offset,
refresh_ttl=refresh_ttl,
namespace_prefix, query=query, filter=filter, limit=limit, offset=offset
),
self._loop,
).result()
@@ -194,12 +179,10 @@ class AsyncBatchedBaseStore(BaseStore):
key: str,
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
) -> None:
_validate_namespace(namespace)
asyncio.run_coroutine_threadsafe(
self.aput(namespace, key=key, value=value, index=index, ttl=ttl), self._loop
self.aput(namespace, key=key, value=value, index=index), self._loop
).result()
@_check_loop
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.17"
version = "2.0.16"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+4 -4
View File
@@ -148,8 +148,8 @@ async def test_async_batch_store(mocker: MockerFixture) -> None:
assert abatch.call_count == 1
assert [tuple(c.args[0]) for c in abatch.call_args_list] == [
(
GetOp(("a",), "b", refresh_ttl=True),
GetOp(("c",), "d", refresh_ttl=True),
GetOp(("a",), "b"),
GetOp(("c",), "d"),
),
]
@@ -467,8 +467,8 @@ async def test_async_batch_store_deduplication(mocker: MockerFixture) -> None:
assert len(abatch.call_args_list) == 1
ops = list(abatch.call_args_list[0].args[1])
assert len(ops) == 2
assert GetOp(("test",), "same", refresh_ttl=True) in ops
assert GetOp(("test",), "different", refresh_ttl=True) in ops
assert GetOp(("test",), "same") in ops
assert GetOp(("test",), "different") in ops
abatch.reset_mock()
-226
View File
@@ -1,226 +0,0 @@
#!/usr/bin/env python3
"""
Script to generate a JSON schema for the langgraph-cli Config class.
This script creates a schema.json file that can be referenced in langgraph.json files
to provide IDE autocompletion and validation.
"""
import inspect
import json
import textwrap
from pathlib import Path
import msgspec
from langgraph_cli.config import (
AuthConfig,
Config,
CorsConfig,
HttpConfig,
IndexConfig,
SecurityConfig,
StoreConfig,
)
def add_descriptions_to_schema(schema, cls):
"""Add docstring descriptions to the schema properties."""
if schema.get("description"):
schema["description"] = inspect.cleandoc(schema["description"])
elif class_doc := inspect.getdoc(cls):
schema["description"] = inspect.cleandoc(class_doc)
# Get attribute docstrings from the class
attr_docs = {}
# Also check class annotations for docstrings
source_lines = inspect.getsourcelines(cls)[0]
current_attr = None
docstring_lines = []
for line in source_lines:
line = line.strip()
# Check for attribute definition (TypedDict style)
if ":" in line and not line.startswith("#") and not line.startswith('"""'):
parts = line.split(":", 1)
if len(parts) == 2 and parts[0].strip().isidentifier():
# If we were collecting a docstring, save it for the previous attribute
if current_attr and docstring_lines:
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
docstring_lines = []
current_attr = parts[0].strip()
# Check for docstring after attribute
elif line.startswith('"""') and current_attr:
# Start or end of a docstring
if len(line) > 3 and line.endswith('"""'):
# Single line docstring
attr_docs[current_attr] = line.strip('"')
current_attr = None
elif docstring_lines:
# End of multi-line docstring
docstring_lines.append(line.rstrip('"'))
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
docstring_lines = []
current_attr = None
else:
# Start of multi-line docstring
docstring_lines.append(line.lstrip('"'))
# Continue multi-line docstring
elif docstring_lines and current_attr:
docstring_lines.append(line.strip('"'))
# Add the last docstring if there is one
if current_attr and docstring_lines:
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
# Add descriptions to properties
if "properties" in schema:
for prop_name, prop_schema in schema["properties"].items():
# First try to get from attribute docstrings
if prop_name in attr_docs and "description" not in prop_schema:
prop_schema["description"] = textwrap.dedent(attr_docs[prop_name])
# Fall back to class docstring parsing
elif class_doc:
for line in class_doc.split("\n"):
if line.strip().startswith(
f"{prop_name}:"
) or line.strip().startswith(f'"{prop_name}"'):
description = line.split(":", 1)[1].strip()
if description and "description" not in prop_schema:
prop_schema["description"] = description
break
# Recursively process nested definitions
if "$defs" in schema:
for def_name, def_schema in schema["$defs"].items():
# Find the class that corresponds to this definition
for potential_cls in [
Config,
StoreConfig,
IndexConfig,
AuthConfig,
SecurityConfig,
HttpConfig,
CorsConfig,
]:
if potential_cls.__name__ == def_name:
add_descriptions_to_schema(def_schema, potential_cls)
break
return schema
def generate_schema():
"""Generate a JSON schema for the Config class using msgspec."""
# Generate the basic schema
schema = msgspec.json.schema(Config)
# Add title and description
schema["title"] = "LangGraph CLI Configuration"
schema["description"] = "Configuration schema for langgraph-cli"
# Add docstring descriptions
schema = add_descriptions_to_schema(schema, Config)
# Add constraint that only one of python_version or node_version should be specified
config_schema = schema["$defs"]["Config"]
# Create two subschemas: one with python_version and one with node_version
# Define properties specific to Python projects
python_specific_props = ["python_version", "pip_config_file"]
# Define properties specific to Node.js projects
node_specific_props = ["node_version"]
# Define properties common to both project types
common_props = [
k
for k in config_schema["properties"]
if k not in python_specific_props and k not in node_specific_props
]
# Create Python schema with python_version and pip_config_file
python_schema = {
"type": "object",
"properties": {
# Include Python-specific properties
**{k: config_schema["properties"][k].copy() for k in python_specific_props},
# Include common properties
**{k: config_schema["properties"][k].copy() for k in common_props},
},
"required": ["dependencies", "graphs"],
}
# Add enum constraint for python_version
if "python_version" in python_schema["properties"]:
python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12"]
# Create Node.js schema with node_version
node_schema = {
"type": "object",
"properties": {
# Include Node-specific properties
**{k: config_schema["properties"][k].copy() for k in node_specific_props},
# Include common properties
**{k: config_schema["properties"][k].copy() for k in common_props},
},
"required": ["node_version", "graphs"],
}
# Add enum constraint for node_version
if "node_version" in node_schema["properties"]:
node_schema["properties"]["node_version"]["anyOf"] = [
{"type": "string", "enum": ["20"]},
{"type": "null"},
]
# Replace the Config schema with a oneOf constraint
config_schema["oneOf"] = [python_schema, node_schema]
# Remove the properties field as it's now defined in the oneOf subschemas
if "properties" in config_schema:
del config_schema["properties"]
return schema
def main():
"""Generate the schema and write it to a file."""
schema = generate_schema()
# Add versioning to the schema
import importlib.metadata
try:
version = importlib.metadata.version("langgraph_cli").split(".")
schema_version = f"v{version[0]}"
except importlib.metadata.PackageNotFoundError:
schema_version = "v1"
# Add version to schema
schema["version"] = schema_version
config_dir = Path(__file__).parent / "schemas"
# Create versioned schema file
versioned_path = config_dir / f"schema.{schema_version}.json"
with open(versioned_path, "w") as f:
json.dump(schema, f, indent=2)
# Also create a latest version
latest_path = config_dir / "schema.json"
with open(latest_path, "w") as f:
json.dump(schema, f, indent=2)
print(f"Schema written to {versioned_path} and {latest_path}")
print(
f"You can now add '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json'"
f" or '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.{schema_version}.json'"
" to your langgraph.json files"
)
if __name__ == "__main__":
main()
+55 -246
View File
@@ -3,7 +3,7 @@ import os
import pathlib
import textwrap
from collections import Counter
from typing import Any, NamedTuple, Optional, TypedDict, Union
from typing import NamedTuple, Optional, TypedDict, Union
import click
@@ -12,18 +12,12 @@ MIN_PYTHON_VERSION = "3.11"
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
This governs how text is converted into embeddings and stored for vector-based lookups.
"""
"""Configuration for indexing documents for semantic search in the store."""
dims: int
"""Required. Dimensionality of the embedding vectors you will store.
"""Number of dimensions in the embedding vectors.
Must match the output dimension of your selected embedding model or custom embed function.
If mismatched, you will likely encounter shape/size errors when inserting or querying vectors.
Common embedding model output dimensions:
Common embedding models have the following dimensions:
- openai:text-embedding-3-large: 3072
- openai:text-embedding-3-small: 1536
- openai:text-embedding-ada-002: 1536
@@ -34,123 +28,42 @@ class IndexConfig(TypedDict, total=False):
"""
embed: str
"""Required. Identifier or reference to the embedding model or a custom embedding function.
"""Optional model (string) to generate embeddings from text or path to model or function.
The format can vary:
- "<provider>:<model_name>" for recognized providers (e.g., "openai:text-embedding-3-large")
- "path/to/module.py:function_name" for your own local embedding function
- "my_custom_embed" if it's a known alias in your system
Examples:
Examples:
- "openai:text-embedding-3-large"
- "cohere:embed-multilingual-v3.0"
- "src/app.py:embeddings"
Note: Must return embeddings of dimension `dims`.
- "src/app.py:embeddings
"""
fields: Optional[list[str]]
"""Optional. List of JSON fields to extract before generating embeddings.
"""Fields to extract text from for embedding generation.
Defaults to ["$"], which means the entire JSON object is embedded as one piece of text.
If you provide multiple fields (e.g. ["title", "content"]), each is extracted and embedded separately,
often saving token usage if you only care about certain parts of the data.
Example:
fields=["title", "abstract", "author.biography"]
Defaults to the root ["$"], which embeds the json object as a whole.
"""
class StoreConfig(TypedDict, total=False):
"""Configuration for the built-in long-term memory store.
This store can optionally perform semantic search. If you omit `index`,
the store will just handle traditional (non-embedded) data without vector lookups.
"""
index: Optional[IndexConfig]
"""Optional. Defines the vector-based semantic search configuration.
If provided, the store will:
- Generate embeddings according to `index.embed`
- Enforce the embedding dimension given by `index.dims`
- Embed only specified JSON fields (if any) from `index.fields`
If omitted, no vector index is initialized.
"""
embed: Optional[IndexConfig]
"""Configuration for vector embeddings in store."""
class SecurityConfig(TypedDict, total=False):
"""Configuration for OpenAPI security definitions and requirements.
Useful for specifying global or path-level authentication and authorization flows
(e.g., OAuth2, API key headers, etc.).
"""
securitySchemes: dict[str, dict[str, Any]]
"""Required. Dict describing each security scheme recognized by your OpenAPI spec.
Keys are scheme names (e.g. "OAuth2", "ApiKeyAuth") and values are their definitions.
Example:
{
"OAuth2": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {"read": "Read data", "write": "Write data"}
}
}
}
}
"""
security: list[dict[str, list[str]]]
"""Optional. Global security requirements across all endpoints.
Each element in the list maps a security scheme (e.g. "OAuth2") to a list of scopes (e.g. ["read", "write"]).
Example:
[
{"OAuth2": ["read", "write"]},
{"ApiKeyAuth": []}
]
"""
securitySchemes: dict
security: list
# path => {method => security}
paths: dict[str, dict[str, list[dict[str, list[str]]]]]
"""Optional. Path-specific security overrides.
Keys are path templates (e.g., "/items/{item_id}"), mapping to:
- Keys that are HTTP methods (e.g., "GET", "POST"),
- Values are lists of security definitions (just like `security`) for that method.
Example:
{
"/private_data": {
"GET": [{"OAuth2": ["read"]}],
"POST": [{"OAuth2": ["write"]}]
}
}
"""
paths: dict[str, dict[str, list]]
class AuthConfig(TypedDict, total=False):
"""Configuration for custom authentication logic and how it integrates into the OpenAPI spec."""
path: str
"""Required. Path to an instance of the Auth() class that implements custom authentication.
Format: "path/to/file.py:my_auth"
"""
"""Path to the authentication function in a Python file."""
disable_studio_auth: bool
"""Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.
Defaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header
value is a valid API key for the deployment's workspace. If True, all requests will go through your custom
authentication logic, regardless of origin of the request.
"""
"""Whether to disable auth when connecting from the LangSmith Studio."""
openapi: SecurityConfig
"""Required. Detailed security configuration that merges into your deployment's OpenAPI spec.
Example (OAuth2):
"""The schema to use for updating the openapi spec.
Example:
{
"securitySchemes": {
"OAuth2": {
@@ -158,185 +71,88 @@ class AuthConfig(TypedDict, total=False):
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {"me": "Read user info", "items": "Manage items"}
"scopes": {
"me": "Read information about the current user",
"items": "Access to create and manage items"
}
}
}
}
},
"security": [
{"OAuth2": ["me"]}
{"OAuth2": ["me"]} # Default security requirement for all endpoints
]
}
"""
class CorsConfig(TypedDict, total=False):
"""Specifies Cross-Origin Resource Sharing (CORS) rules for your server.
If omitted, defaults are typically very restrictive (often no cross-origin requests).
Configure carefully if you want to allow usage from browsers hosted on other domains.
"""
allow_origins: list[str]
"""Optional. List of allowed origins (e.g., "https://example.com").
Default is often an empty list (no external origins).
Use "*" only if you trust all origins, as that bypasses most restrictions.
"""
allow_methods: list[str]
"""Optional. HTTP methods permitted for cross-origin requests (e.g. ["GET", "POST"]).
Default might be ["GET", "POST", "OPTIONS"] depending on your server framework.
"""
allow_headers: list[str]
"""Optional. HTTP headers that can be used in cross-origin requests (e.g. ["Content-Type", "Authorization"])."""
allow_credentials: bool
"""Optional. If True, cross-origin requests can include credentials (cookies, auth headers).
Default False to avoid accidentally exposing secured endpoints to untrusted sites.
"""
allow_origin_regex: str
"""Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
Example: "^https://.*\.mycompany\.com$"
"""
expose_headers: list[str]
"""Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
max_age: int
"""Optional. How many seconds the browser may cache preflight responses.
Default might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.
"""
class HttpConfig(TypedDict, total=False):
"""Configuration for the built-in HTTP server that powers your deployment's routes and endpoints."""
app: str
"""Optional. Import path to a custom Starlette/FastAPI application to mount.
Format: "path/to/module.py:app_var"
If provided, it can override or extend the default routes.
"""
"""Import path for a custom Starlette/FastAPI app to mount"""
disable_assistants: bool
"""Optional. If True, /assistants routes are removed from the server.
Default is False (meaning /assistants is enabled).
"""
"""Disable /assistants routes"""
disable_threads: bool
"""Optional. If True, /threads routes are removed.
Default is False.
"""
"""Disable /threads routes"""
disable_runs: bool
"""Optional. If True, /runs routes are removed.
Default is False.
"""
"""Disable /runs routes"""
disable_store: bool
"""Optional. If True, /store routes are removed, disabling direct store interactions via HTTP.
Default is False.
"""
"""Disable /store routes"""
disable_meta: bool
"""Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.
Default is False.
"""
"""Disable /ok, /info, /metrics, and /docs routes"""
cors: Optional[CorsConfig]
"""Optional. Defines CORS restrictions. If omitted, no special rules are set and
cross-origin behavior depends on default server settings.
"""
"""Cross-Origin Resource Sharing (CORS) configuration"""
class Config(TypedDict, total=False):
"""Top-level config for langgraph-cli or similar deployment tooling."""
"""Configuration for langgraph-cli."""
python_version: str
"""Optional. Python version in 'major.minor' format (e.g. '3.11').
Must be at least 3.11 or greater for this deployment to function properly.
"""
"""Python version to use."""
node_version: Optional[str]
"""Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.
Must be >= 20 if provided.
"""
"""Node.js version to use."""
pip_config_file: Optional[str]
"""Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling
package installation (custom indices, credentials, etc.).
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
"""
"""Path to a pip configuration file."""
dockerfile_lines: list[str]
"""Optional. Additional Docker instructions that will be appended to your base Dockerfile.
Useful for installing OS packages, setting environment variables, etc.
Example:
dockerfile_lines=[
"RUN apt-get update && apt-get install -y libmagic-dev",
"ENV MY_CUSTOM_VAR=hello_world"
]
"""
"""Additional lines to add to the Dockerfile."""
dependencies: list[str]
"""List of Python dependencies to install, either from PyPI or local paths.
Examples:
- "." or "./src" if you have a local Python package
- str (aka "anthropic") for a PyPI package
- "git+https://github.com/org/repo.git@main" for a Git-based package
Defaults to an empty list, meaning no additional packages installed beyond your base environment.
"""
"""Additional Python dependencies to install."""
graphs: dict[str, str]
"""Optional. Named definitions of graphs, each pointing to a Python object.
Graphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context
managers that accept a single configuration argument (of type RunnableConfig) and return a pregel object
(instance of Stategraph, etc.).
Keys are graph names, values are "path/to/file.py:object_name".
Example:
{
"mygraph": "graphs/my_graph.py:graph_definition",
"anothergraph": "graphs/another.py:get_graph"
}
"""
"""Mapping of graph names to their definitions."""
env: Union[dict[str, str], str]
"""Optional. Environment variables to set for your deployment.
- If given as a dict, keys are variable names and values are their values.
- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.
Example as a dict:
env={"API_TOKEN": "abc123", "DEBUG": "true"}
Example as a file path:
env=".env"
"""Environment variables to set.
If a dictionary is provided, the keys are environment variable names
and the values are the corresponding environment variable values.
If a string is provided, it is interpreted as a path to a file containing
environment variables in the format KEY=VALUE, with one environment variable
per line.
"""
store: Optional[StoreConfig]
"""Optional. Configuration for the built-in long-term memory store, including semantic search indexing.
If omitted, no vector index is set up (the object store will still be present, however).
"""
"""Configuration for vector embeddings in store."""
auth: Optional[AuthConfig]
"""Optional. Custom authentication config, including the path to your Python auth logic and
the OpenAPI security definitions it uses.
"""
"""Configuration for authentication."""
http: Optional[HttpConfig]
"""Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed
and how cross-origin requests are handled.
"""
ui: Optional[dict[str, str]]
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
"""
"""Configuration for HTTP server."""
def _parse_version(version_str: str) -> tuple[int, int]:
@@ -373,7 +189,6 @@ def validate_config(config: Config) -> Config:
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
"ui": config.get("ui"),
}
if config.get("node_version")
else {
@@ -386,7 +201,6 @@ def validate_config(config: Config) -> Config:
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
"ui": config.get("ui"),
}
)
@@ -873,11 +687,9 @@ def python_config_to_docker(
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
if local_deps.pip_reqs:
pip_reqs_str = os.linesep.join(
(
f"COPY --from=__outer_{reqpath.name} requirements.txt {destpath}"
if reqpath.parent in local_deps.additional_contexts
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
)
f"COPY --from=__outer_{reqpath.name} requirements.txt {destpath}"
if reqpath.parent in local_deps.additional_contexts
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
for reqpath, destpath in local_deps.pip_reqs
)
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
@@ -912,15 +724,13 @@ RUN set -ex && \\
)
local_pkgs_str = os.linesep.join(
(
f"""# -- Adding local package {relpath} --
f"""# -- Adding local package {relpath} --
COPY --from={name} . /deps/{name}
# -- End of local package {relpath} --"""
if fullpath in local_deps.additional_contexts
else f"""# -- Adding local package {relpath} --
if fullpath in local_deps.additional_contexts
else f"""# -- Adding local package {relpath} --
ADD {relpath} /deps/{name}
# -- End of local package {relpath} --"""
)
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
)
@@ -1035,7 +845,6 @@ ADD . {faux_path}
RUN cd {faux_path} && {install_cmd}
{env_additional_config}
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
{f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'" if config.get("ui") else ""}
WORKDIR {faux_path}
+1 -53
View File
@@ -707,58 +707,6 @@ files = [
{file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"},
]
[[package]]
name = "msgspec"
version = "0.19.0"
description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML."
optional = false
python-versions = ">=3.9"
files = [
{file = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"},
{file = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"},
{file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"},
{file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"},
{file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"},
{file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"},
{file = "msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"},
{file = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"},
{file = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"},
{file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"},
{file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"},
{file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"},
{file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"},
{file = "msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"},
{file = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"},
{file = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"},
{file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"},
{file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"},
{file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"},
{file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"},
{file = "msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"},
{file = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"},
{file = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"},
{file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"},
{file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"},
{file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"},
{file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"},
{file = "msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"},
{file = "msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044"},
{file = "msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229"},
{file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12"},
{file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446"},
{file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19"},
{file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db"},
{file = "msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe"},
{file = "msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"},
]
[package.extras]
dev = ["attrs", "coverage", "eval-type-backport", "furo", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli_w"]
doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"]
test = ["attrs", "eval-type-backport", "msgpack", "pytest", "pyyaml", "tomli", "tomli_w"]
toml = ["tomli", "tomli_w"]
yaml = ["pyyaml"]
[[package]]
name = "mypy"
version = "1.15.0"
@@ -1717,4 +1665,4 @@ inmem = ["langgraph-api", "python-dotenv"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
content-hash = "d0e2bdcb600ad031867413025fcc58bb162609209359d63ca99a77060cf8cbb4"
content-hash = "04a0b0e203ae00f30cca7e454c632be0b41cbb4820d0c05315a346d514b5f98e"
+1 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.75"
version = "0.1.74"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -25,7 +25,6 @@ pytest-asyncio = "^0.21.1"
pytest-mock = "^3.11.1"
pytest-watch = "^4.2.0"
mypy = "^1.10.0"
msgspec = "^0.19.0"
[tool.poetry.extras]
inmem = ["langgraph-api", "python-dotenv"]
-438
View File
@@ -1,438 +0,0 @@
{
"$ref": "#/$defs/Config",
"$defs": {
"Config": {
"title": "Config",
"description": "Top-level config for langgraph-cli or similar deployment tooling.",
"type": "object",
"required": [],
"oneOf": [
{
"type": "object",
"properties": {
"python_version": {
"type": "string",
"description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n",
"enum": [
"3.11",
"3.12"
]
},
"pip_config_file": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
},
"auth": {
"anyOf": [
{
"$ref": "#/$defs/AuthConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
},
"dockerfile_lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"env": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
},
"graphs": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
},
"http": {
"anyOf": [
{
"$ref": "#/$defs/HttpConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
},
"store": {
"anyOf": [
{
"$ref": "#/$defs/StoreConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
},
"ui": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
}
},
"required": [
"dependencies",
"graphs"
]
},
{
"type": "object",
"properties": {
"node_version": {
"anyOf": [
{
"type": "string",
"enum": [
"20"
]
},
{
"type": "null"
}
],
"description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
},
"auth": {
"anyOf": [
{
"$ref": "#/$defs/AuthConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
},
"dockerfile_lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"env": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
},
"graphs": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
},
"http": {
"anyOf": [
{
"$ref": "#/$defs/HttpConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
},
"store": {
"anyOf": [
{
"$ref": "#/$defs/StoreConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
},
"ui": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
}
},
"required": [
"node_version",
"graphs"
]
}
]
},
"AuthConfig": {
"title": "AuthConfig",
"description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
"type": "object",
"properties": {
"disable_studio_auth": {
"type": "boolean",
"description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If True, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
},
"openapi": {
"$ref": "#/$defs/SecurityConfig",
"description": "Required. Detailed security configuration that merges into your deployment's OpenAPI spec.\n\n{\n}\n}\n}\n},\n]\n}\n"
},
"path": {
"type": "string",
"description": "Required. Path to an instance of the Auth() class that implements custom authentication.\n"
}
},
"required": []
},
"SecurityConfig": {
"title": "SecurityConfig",
"description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
"type": "object",
"properties": {
"paths": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"description": "Optional. Path-specific security overrides.\n\n- Keys that are HTTP methods (e.g., \"GET\", \"POST\"),\n- Values are lists of security definitions (just like `security`) for that method.\n"
},
"security": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"description": "Optional. Global security requirements across all endpoints.\n\nEach element in the list maps a security scheme (e.g. \"OAuth2\") to a list of scopes (e.g. [\"read\", \"write\"])."
},
"securitySchemes": {
"type": "object",
"additionalProperties": {
"type": "object"
},
"description": "Required. Dict describing each security scheme recognized by your OpenAPI spec.\n\nKeys are scheme names (e.g. \"OAuth2\", \"ApiKeyAuth\") and values are their definitions."
}
},
"required": []
},
"HttpConfig": {
"title": "HttpConfig",
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
"type": "object",
"properties": {
"app": {
"type": "string",
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
},
"cors": {
"anyOf": [
{
"$ref": "#/$defs/CorsConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
},
"disable_assistants": {
"type": "boolean",
"description": "Optional. If True, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
},
"disable_meta": {
"type": "boolean",
"description": "Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.\n\nDefault is False.\n"
},
"disable_runs": {
"type": "boolean",
"description": "Optional. If True, /runs routes are removed.\n\nDefault is False.\n"
},
"disable_store": {
"type": "boolean",
"description": "Optional. If True, /store routes are removed, disabling direct store interactions via HTTP.\n\nDefault is False.\n"
},
"disable_threads": {
"type": "boolean",
"description": "Optional. If True, /threads routes are removed.\n\nDefault is False.\n"
}
},
"required": []
},
"CorsConfig": {
"title": "CorsConfig",
"description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
"type": "object",
"properties": {
"allow_credentials": {
"type": "boolean",
"description": "Optional. If True, cross-origin requests can include credentials (cookies, auth headers).\n\nDefault False to avoid accidentally exposing secured endpoints to untrusted sites.\n"
},
"allow_headers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. HTTP headers that can be used in cross-origin requests (e.g. [\"Content-Type\", \"Authorization\"])."
},
"allow_methods": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. HTTP methods permitted for cross-origin requests (e.g. [\"GET\", \"POST\"]).\n\nDefault might be [\"GET\", \"POST\", \"OPTIONS\"] depending on your server framework.\n"
},
"allow_origin_regex": {
"type": "string",
"description": "Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.\n"
},
"allow_origins": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. List of allowed origins (e.g., \"https://example.com\").\n\nDefault is often an empty list (no external origins).\nUse \"*\" only if you trust all origins, as that bypasses most restrictions.\n"
},
"expose_headers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."
},
"max_age": {
"type": "integer",
"description": "Optional. How many seconds the browser may cache preflight responses.\n\nDefault might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.\n"
}
},
"required": []
},
"StoreConfig": {
"title": "StoreConfig",
"description": "Configuration for the built-in long-term memory store.\n\nThis store can optionally perform semantic search. If you omit `index`,\nthe store will just handle traditional (non-embedded) data without vector lookups.",
"type": "object",
"properties": {
"index": {
"anyOf": [
{
"$ref": "#/$defs/IndexConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
}
},
"required": []
},
"IndexConfig": {
"title": "IndexConfig",
"description": "Configuration for indexing documents for semantic search in the store.\n\nThis governs how text is converted into embeddings and stored for vector-based lookups.",
"type": "object",
"properties": {
"dims": {
"type": "integer",
"description": "Required. Dimensionality of the embedding vectors you will store.\n\nMust match the output dimension of your selected embedding model or custom embed function.\nIf mismatched, you will likely encounter shape/size errors when inserting or querying vectors.\n\n"
},
"embed": {
"type": "string",
"description": "Required. Identifier or reference to the embedding model or a custom embedding function.\n\n- \"my_custom_embed\" if it's a known alias in your system\n"
},
"fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional. List of JSON fields to extract before generating embeddings.\n\nDefaults to [\"$\"], which means the entire JSON object is embedded as one piece of text.\nIf you provide multiple fields (e.g. [\"title\", \"content\"]), each is extracted and embedded separately,\noften saving token usage if you only care about certain parts of the data.\n"
}
},
"required": []
}
},
"title": "LangGraph CLI Configuration",
"description": "Configuration schema for langgraph-cli",
"version": "v0"
}
-438
View File
@@ -1,438 +0,0 @@
{
"$ref": "#/$defs/Config",
"$defs": {
"Config": {
"title": "Config",
"description": "Top-level config for langgraph-cli or similar deployment tooling.",
"type": "object",
"required": [],
"oneOf": [
{
"type": "object",
"properties": {
"python_version": {
"type": "string",
"description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n",
"enum": [
"3.11",
"3.12"
]
},
"pip_config_file": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
},
"auth": {
"anyOf": [
{
"$ref": "#/$defs/AuthConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
},
"dockerfile_lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"env": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
},
"graphs": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
},
"http": {
"anyOf": [
{
"$ref": "#/$defs/HttpConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
},
"store": {
"anyOf": [
{
"$ref": "#/$defs/StoreConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
},
"ui": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
}
},
"required": [
"dependencies",
"graphs"
]
},
{
"type": "object",
"properties": {
"node_version": {
"anyOf": [
{
"type": "string",
"enum": [
"20"
]
},
{
"type": "null"
}
],
"description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
},
"auth": {
"anyOf": [
{
"$ref": "#/$defs/AuthConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
},
"dockerfile_lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"env": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
},
"graphs": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
},
"http": {
"anyOf": [
{
"$ref": "#/$defs/HttpConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
},
"store": {
"anyOf": [
{
"$ref": "#/$defs/StoreConfig"
},
{
"type": "null"
}
],
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
},
"ui": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
}
},
"required": [
"node_version",
"graphs"
]
}
]
},
"AuthConfig": {
"title": "AuthConfig",
"description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
"type": "object",
"properties": {
"disable_studio_auth": {
"type": "boolean",
"description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If True, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
},
"openapi": {
"$ref": "#/$defs/SecurityConfig",
"description": "Required. Detailed security configuration that merges into your deployment's OpenAPI spec.\n\n{\n}\n}\n}\n},\n]\n}\n"
},
"path": {
"type": "string",
"description": "Required. Path to an instance of the Auth() class that implements custom authentication.\n"
}
},
"required": []
},
"SecurityConfig": {
"title": "SecurityConfig",
"description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
"type": "object",
"properties": {
"paths": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"description": "Optional. Path-specific security overrides.\n\n- Keys that are HTTP methods (e.g., \"GET\", \"POST\"),\n- Values are lists of security definitions (just like `security`) for that method.\n"
},
"security": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"description": "Optional. Global security requirements across all endpoints.\n\nEach element in the list maps a security scheme (e.g. \"OAuth2\") to a list of scopes (e.g. [\"read\", \"write\"])."
},
"securitySchemes": {
"type": "object",
"additionalProperties": {
"type": "object"
},
"description": "Required. Dict describing each security scheme recognized by your OpenAPI spec.\n\nKeys are scheme names (e.g. \"OAuth2\", \"ApiKeyAuth\") and values are their definitions."
}
},
"required": []
},
"HttpConfig": {
"title": "HttpConfig",
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
"type": "object",
"properties": {
"app": {
"type": "string",
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
},
"cors": {
"anyOf": [
{
"$ref": "#/$defs/CorsConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
},
"disable_assistants": {
"type": "boolean",
"description": "Optional. If True, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
},
"disable_meta": {
"type": "boolean",
"description": "Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.\n\nDefault is False.\n"
},
"disable_runs": {
"type": "boolean",
"description": "Optional. If True, /runs routes are removed.\n\nDefault is False.\n"
},
"disable_store": {
"type": "boolean",
"description": "Optional. If True, /store routes are removed, disabling direct store interactions via HTTP.\n\nDefault is False.\n"
},
"disable_threads": {
"type": "boolean",
"description": "Optional. If True, /threads routes are removed.\n\nDefault is False.\n"
}
},
"required": []
},
"CorsConfig": {
"title": "CorsConfig",
"description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
"type": "object",
"properties": {
"allow_credentials": {
"type": "boolean",
"description": "Optional. If True, cross-origin requests can include credentials (cookies, auth headers).\n\nDefault False to avoid accidentally exposing secured endpoints to untrusted sites.\n"
},
"allow_headers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. HTTP headers that can be used in cross-origin requests (e.g. [\"Content-Type\", \"Authorization\"])."
},
"allow_methods": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. HTTP methods permitted for cross-origin requests (e.g. [\"GET\", \"POST\"]).\n\nDefault might be [\"GET\", \"POST\", \"OPTIONS\"] depending on your server framework.\n"
},
"allow_origin_regex": {
"type": "string",
"description": "Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.\n"
},
"allow_origins": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. List of allowed origins (e.g., \"https://example.com\").\n\nDefault is often an empty list (no external origins).\nUse \"*\" only if you trust all origins, as that bypasses most restrictions.\n"
},
"expose_headers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."
},
"max_age": {
"type": "integer",
"description": "Optional. How many seconds the browser may cache preflight responses.\n\nDefault might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.\n"
}
},
"required": []
},
"StoreConfig": {
"title": "StoreConfig",
"description": "Configuration for the built-in long-term memory store.\n\nThis store can optionally perform semantic search. If you omit `index`,\nthe store will just handle traditional (non-embedded) data without vector lookups.",
"type": "object",
"properties": {
"index": {
"anyOf": [
{
"$ref": "#/$defs/IndexConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
}
},
"required": []
},
"IndexConfig": {
"title": "IndexConfig",
"description": "Configuration for indexing documents for semantic search in the store.\n\nThis governs how text is converted into embeddings and stored for vector-based lookups.",
"type": "object",
"properties": {
"dims": {
"type": "integer",
"description": "Required. Dimensionality of the embedding vectors you will store.\n\nMust match the output dimension of your selected embedding model or custom embed function.\nIf mismatched, you will likely encounter shape/size errors when inserting or querying vectors.\n\n"
},
"embed": {
"type": "string",
"description": "Required. Identifier or reference to the embedding model or a custom embedding function.\n\n- \"my_custom_embed\" if it's a known alias in your system\n"
},
"fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional. List of JSON fields to extract before generating embeddings.\n\nDefaults to [\"$\"], which means the entire JSON object is embedded as one piece of text.\nIf you provide multiple fields (e.g. [\"title\", \"content\"]), each is extracted and embedded separately,\noften saving token usage if you only care about certain parts of the data.\n"
}
},
"required": []
}
},
"title": "LangGraph CLI Configuration",
"description": "Configuration schema for langgraph-cli",
"version": "v0"
}
-4
View File
@@ -33,7 +33,6 @@ def test_validate_config():
"store": None,
"auth": None,
"http": None,
"ui": None,
**expected_config,
}
actual_config = validate_config(expected_config)
@@ -53,7 +52,6 @@ def test_validate_config():
"store": None,
"auth": None,
"http": None,
"ui": None,
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
@@ -469,7 +467,6 @@ def test_config_to_docker_nodejs():
"node_version": "20",
"graphs": graphs,
"dockerfile_lines": ["ARG meow", "ARG foo"],
"ui": {"agent": "./graphs/agent.ui.jsx"},
}
),
"langchain/langgraphjs-api",
@@ -480,7 +477,6 @@ ARG foo
ADD . /deps/unit_tests
RUN cd /deps/unit_tests && npm i
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
WORKDIR /deps/unit_tests
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
+6 -14
View File
@@ -36,31 +36,23 @@ from langgraph.types import _DC_KWARGS, RetryPolicy, StreamMode
@overload
def task(
*,
name: Optional[str] = None,
retry: Optional[RetryPolicy] = None,
) -> Callable[
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
Callable[P, SyncAsyncFuture[T]],
]: ...
*, name: Optional[str] = None, retry: Optional[RetryPolicy] = None
) -> Callable[[Callable[P, T]], Callable[P, SyncAsyncFuture[T]]]: ...
@overload
def task(
__func_or_none__: Union[Callable[P, Awaitable[T]], Callable[P, T]],
__func_or_none__: Callable[P, T],
) -> Callable[P, SyncAsyncFuture[T]]: ...
def task(
__func_or_none__: Optional[Union[Callable[P, Awaitable[T]], Callable[P, T]]] = None,
__func_or_none__: Optional[Union[Callable[P, T], Callable[P, Awaitable[T]]]] = None,
*,
name: Optional[str] = None,
retry: Optional[RetryPolicy] = None,
) -> Union[
Callable[
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
Callable[P, SyncAsyncFuture[T]],
],
Callable[[Callable[P, T]], Callable[P, SyncAsyncFuture[T]]],
Callable[P, SyncAsyncFuture[T]],
]:
"""Define a LangGraph task using the `task` decorator.
@@ -353,7 +345,7 @@ class entrypoint:
value: R
"""Value to return. A value will always be returned even if it is None."""
save: S
"""The value for the state for the next checkpoint.
"""The value for the state for the next checkpoint.
A value will always be saved even if it is None.
"""
+3 -34
View File
@@ -18,7 +18,6 @@ from typing import (
Type,
Union,
cast,
get_type_hints,
overload,
)
from uuid import UUID, uuid5
@@ -120,7 +119,7 @@ from langgraph.utils.config import (
recast_checkpoint_ns,
)
from langgraph.utils.fields import get_enhanced_type_hints
from langgraph.utils.pydantic import create_model, is_supported_by_pydantic
from langgraph.utils.pydantic import create_model
from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-defined]
WriteValue = Union[Callable[[Input], Output], Any]
@@ -610,36 +609,6 @@ class Pregel(PregelProtocol):
]
]
def config_schema(
self, *, include: Optional[Sequence[str]] = None
) -> Type[BaseModel]:
# If the config type is not set explicitly, we will try to infer it.
# If the config type is provided, but isn't directly supported by pydantic
# (e.g., vanilla python class), we will also delegate to the parent class,
# which handles cases where Pydantic doesn't support the type.
if self.config_type is None or not is_supported_by_pydantic(self.config_type):
return super().config_schema(include=include)
include = include or []
fields = {
"configurable": (self.config_type, None),
**{
field_name: (field_type, None)
for field_name, field_type in get_type_hints(RunnableConfig).items()
if field_name in [i for i in include if i != "configurable"]
},
}
return create_model(self.get_name("Config"), field_definitions=fields)
def get_config_jsonschema(
self, *, include: Optional[Sequence[str]] = None
) -> Dict[str, Any]:
schema = self.config_schema(include=include)
if hasattr(schema, "model_json_schema"):
return schema.model_json_schema()
else:
return schema.schema()
@property
def InputType(self) -> Any:
if isinstance(self.input_channels, str):
@@ -665,7 +634,7 @@ class Pregel(PregelProtocol):
def get_input_jsonschema(
self, config: Optional[RunnableConfig] = None
) -> Dict[str, Any]:
) -> Dict[All, Any]:
schema = self.get_input_schema(config)
if hasattr(schema, "model_json_schema"):
return schema.model_json_schema()
@@ -697,7 +666,7 @@ class Pregel(PregelProtocol):
def get_output_jsonschema(
self, config: Optional[RunnableConfig] = None
) -> Dict[str, Any]:
) -> Dict[All, Any]:
schema = self.get_output_schema(config)
if hasattr(schema, "model_json_schema"):
return schema.model_json_schema()
+12 -8
View File
@@ -2,6 +2,7 @@ import asyncio
import concurrent.futures
from collections import defaultdict, deque
from contextlib import AsyncExitStack, ExitStack
from dataclasses import replace
from inspect import signature
from types import TracebackType
from typing import (
@@ -54,7 +55,6 @@ from langgraph.constants import (
ERROR,
INPUT,
INTERRUPT,
MISSING,
NS_SEP,
NULL_TASK_ID,
PUSH,
@@ -67,6 +67,7 @@ from langgraph.errors import (
EmptyInputError,
GraphDelegate,
GraphInterrupt,
ParentCommand,
)
from langgraph.managed.base import (
ManagedValueMapping,
@@ -565,13 +566,7 @@ class PregelLoop(LoopProtocol):
is_resuming = bool(self.checkpoint["channel_versions"]) and bool(
configurable.get(
CONFIG_KEY_RESUMING,
self.input is None
or isinstance(self.input, Command)
or (
not self.is_nested
and self.config.get("metadata", {}).get("run_id")
== self.checkpoint_metadata.get("run_id", MISSING)
),
self.input is None or isinstance(self.input, Command),
)
)
@@ -747,6 +742,15 @@ class PregelLoop(LoopProtocol):
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
# add current state to parent command
if isinstance(exc_value, ParentCommand):
cmd = exc_value.args[0]
state = (
[(self.output_keys, read_channels(self.channels, self.output_keys))]
if isinstance(self.output_keys, str)
else list(read_channels(self.channels, self.output_keys).items())
)
exc_value.args = (replace(cmd, update=[*state, *cmd._update_as_tuples()]),)
# suppress interrupt
suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested
if suppress:
@@ -1,9 +1,5 @@
import sys
import typing
from dataclasses import is_dataclass
from typing import Any, Dict, Optional, Union
import typing_extensions
from pydantic import BaseModel
from pydantic.v1 import BaseModel as BaseModelV1
@@ -39,31 +35,3 @@ def create_model(
v1_kwargs["__root__"] = root
return create_model(model_name, **v1_kwargs, **(field_definitions or {}))
def is_supported_by_pydantic(type_: Any) -> bool:
"""Check if a given "complex" type is supported by pydantic.
This will return False for primitive types like int, str, etc.
The check is meant for container types like dataclasses, TypedDicts, etc.
"""
if is_dataclass(type_):
return True
# Pydantic does not support mixing .v1 and root namespaces, so
# we only check for BaseModel (not pydantic.v1.BaseModel).
if isinstance(type_, type) and issubclass(type_, BaseModel):
return True
if hasattr(type_, "__orig_bases__"):
for base in type_.__orig_bases__:
if base is typing_extensions.TypedDict:
return True
elif base is typing.TypedDict: # noqa: TID251
# ignoring TID251 since it's OK to use typing.TypedDict in this case.
# Pydantic supports typing.TypedDict from Python 3.12
# For older versions, only typing_extensions.TypedDict is supported.
if sys.version_info >= (3, 12):
return True
return False
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.3.5"
version = "0.3.2"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -1217,426 +1217,6 @@
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[memory]
'''
graph TD;
__start__ --> rewrite_query;
analyzer_one --> retriever_one;
qa --> __end__;
retriever_one --> qa;
retriever_two --> qa;
rewrite_query --> analyzer_one;
rewrite_query -.-> retriever_two;
'''
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[memory].1
dict({
'$defs': dict({
'InnerObject': dict({
'properties': dict({
'yo': dict({
'title': 'Yo',
'type': 'integer',
}),
}),
'required': list([
'yo',
]),
'title': 'InnerObject',
'type': 'object',
}),
}),
'properties': dict({
'inner': dict({
'$ref': '#/$defs/InnerObject',
}),
'query': dict({
'title': 'Query',
'type': 'string',
}),
}),
'required': list([
'query',
'inner',
]),
'title': 'Input',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[memory].2
dict({
'properties': dict({
'answer': dict({
'title': 'Answer',
'type': 'string',
}),
'docs': dict({
'items': dict({
'type': 'string',
}),
'title': 'Docs',
'type': 'array',
}),
}),
'required': list([
'answer',
'docs',
]),
'title': 'Output',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres]
'''
graph TD;
__start__ --> rewrite_query;
analyzer_one --> retriever_one;
qa --> __end__;
retriever_one --> qa;
retriever_two --> qa;
rewrite_query --> analyzer_one;
rewrite_query -.-> retriever_two;
'''
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres].1
dict({
'$defs': dict({
'InnerObject': dict({
'properties': dict({
'yo': dict({
'title': 'Yo',
'type': 'integer',
}),
}),
'required': list([
'yo',
]),
'title': 'InnerObject',
'type': 'object',
}),
}),
'properties': dict({
'inner': dict({
'$ref': '#/$defs/InnerObject',
}),
'query': dict({
'title': 'Query',
'type': 'string',
}),
}),
'required': list([
'query',
'inner',
]),
'title': 'Input',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres].2
dict({
'properties': dict({
'answer': dict({
'title': 'Answer',
'type': 'string',
}),
'docs': dict({
'items': dict({
'type': 'string',
}),
'title': 'Docs',
'type': 'array',
}),
}),
'required': list([
'answer',
'docs',
]),
'title': 'Output',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_pipe]
'''
graph TD;
__start__ --> rewrite_query;
analyzer_one --> retriever_one;
qa --> __end__;
retriever_one --> qa;
retriever_two --> qa;
rewrite_query --> analyzer_one;
rewrite_query -.-> retriever_two;
'''
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_pipe].1
dict({
'$defs': dict({
'InnerObject': dict({
'properties': dict({
'yo': dict({
'title': 'Yo',
'type': 'integer',
}),
}),
'required': list([
'yo',
]),
'title': 'InnerObject',
'type': 'object',
}),
}),
'properties': dict({
'inner': dict({
'$ref': '#/$defs/InnerObject',
}),
'query': dict({
'title': 'Query',
'type': 'string',
}),
}),
'required': list([
'query',
'inner',
]),
'title': 'Input',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_pipe].2
dict({
'properties': dict({
'answer': dict({
'title': 'Answer',
'type': 'string',
}),
'docs': dict({
'items': dict({
'type': 'string',
}),
'title': 'Docs',
'type': 'array',
}),
}),
'required': list([
'answer',
'docs',
]),
'title': 'Output',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_pool]
'''
graph TD;
__start__ --> rewrite_query;
analyzer_one --> retriever_one;
qa --> __end__;
retriever_one --> qa;
retriever_two --> qa;
rewrite_query --> analyzer_one;
rewrite_query -.-> retriever_two;
'''
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_pool].1
dict({
'$defs': dict({
'InnerObject': dict({
'properties': dict({
'yo': dict({
'title': 'Yo',
'type': 'integer',
}),
}),
'required': list([
'yo',
]),
'title': 'InnerObject',
'type': 'object',
}),
}),
'properties': dict({
'inner': dict({
'$ref': '#/$defs/InnerObject',
}),
'query': dict({
'title': 'Query',
'type': 'string',
}),
}),
'required': list([
'query',
'inner',
]),
'title': 'Input',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_pool].2
dict({
'properties': dict({
'answer': dict({
'title': 'Answer',
'type': 'string',
}),
'docs': dict({
'items': dict({
'type': 'string',
}),
'title': 'Docs',
'type': 'array',
}),
}),
'required': list([
'answer',
'docs',
]),
'title': 'Output',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_shallow]
'''
graph TD;
__start__ --> rewrite_query;
analyzer_one --> retriever_one;
qa --> __end__;
retriever_one --> qa;
retriever_two --> qa;
rewrite_query --> analyzer_one;
rewrite_query -.-> retriever_two;
'''
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_shallow].1
dict({
'$defs': dict({
'InnerObject': dict({
'properties': dict({
'yo': dict({
'title': 'Yo',
'type': 'integer',
}),
}),
'required': list([
'yo',
]),
'title': 'InnerObject',
'type': 'object',
}),
}),
'properties': dict({
'inner': dict({
'$ref': '#/$defs/InnerObject',
}),
'query': dict({
'title': 'Query',
'type': 'string',
}),
}),
'required': list([
'query',
'inner',
]),
'title': 'Input',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[postgres_shallow].2
dict({
'properties': dict({
'answer': dict({
'title': 'Answer',
'type': 'string',
}),
'docs': dict({
'items': dict({
'type': 'string',
}),
'title': 'Docs',
'type': 'array',
}),
}),
'required': list([
'answer',
'docs',
]),
'title': 'Output',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[sqlite]
'''
graph TD;
__start__ --> rewrite_query;
analyzer_one --> retriever_one;
qa --> __end__;
retriever_one --> qa;
retriever_two --> qa;
rewrite_query --> analyzer_one;
rewrite_query -.-> retriever_two;
'''
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[sqlite].1
dict({
'$defs': dict({
'InnerObject': dict({
'properties': dict({
'yo': dict({
'title': 'Yo',
'type': 'integer',
}),
}),
'required': list([
'yo',
]),
'title': 'InnerObject',
'type': 'object',
}),
}),
'properties': dict({
'inner': dict({
'$ref': '#/$defs/InnerObject',
}),
'query': dict({
'title': 'Query',
'type': 'string',
}),
}),
'required': list([
'query',
'inner',
]),
'title': 'Input',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input[sqlite].2
dict({
'properties': dict({
'answer': dict({
'title': 'Answer',
'type': 'string',
}),
'docs': dict({
'items': dict({
'type': 'string',
}),
'title': 'Docs',
'type': 'array',
}),
}),
'required': list([
'answer',
'docs',
]),
'title': 'Output',
'type': 'object',
})
# ---
# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[memory]
'''
graph TD;
@@ -1948,7 +1528,7 @@
'''
# ---
# name: test_state_graph_w_config_inherited_state_keys
'{"$defs": {"Config": {"properties": {"tools": {"items": {"type": "string"}, "title": "Tools", "type": "array"}}, "title": "Config", "type": "object"}}, "properties": {"configurable": {"$ref": "#/$defs/Config", "default": null}}, "title": "LangGraphConfig", "type": "object"}'
'{"$defs": {"Configurable": {"properties": {"tools": {"default": null, "items": {"type": "string"}, "title": "Tools", "type": "array"}}, "title": "Configurable", "type": "object"}}, "properties": {"configurable": {"$ref": "#/$defs/Configurable", "default": null}}, "title": "LangGraphConfig", "type": "object"}'
# ---
# name: test_state_graph_w_config_inherited_state_keys.1
'{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "required": ["input"], "title": "LangGraphInput", "type": "object"}'
+163 -209
View File
@@ -10,7 +10,7 @@ import warnings
from collections import Counter, deque
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from dataclasses import dataclass, field
from dataclasses import dataclass
from random import randrange
from typing import (
Annotated,
@@ -275,61 +275,6 @@ def test_checkpoint_errors() -> None:
graph.invoke("", {"configurable": {"thread_id": "thread-1"}})
def test_config_json_schema() -> None:
"""Test that config json schema is generated properly."""
chain = Channel.subscribe_to("input") | Channel.write_to("output")
@dataclass
class Foo:
x: int
y: str = field(default="foo")
app = Pregel(
nodes={
"one": chain,
},
channels={
"ephemeral": EphemeralValue(Any),
"input": LastValue(int),
"output": LastValue(int),
},
input_channels=["input", "ephemeral"],
output_channels="output",
config_type=Foo,
)
assert app.get_config_jsonschema() == {
"$defs": {
"Foo": {
"properties": {
"x": {
"title": "X",
"type": "integer",
},
"y": {
"default": "foo",
"title": "Y",
"type": "string",
},
},
"required": [
"x",
],
"title": "Foo",
"type": "object",
},
},
"properties": {
"configurable": {
"$ref": "#/$defs/Foo",
"default": None,
},
},
"title": "LangGraphConfig",
"type": "object",
}
def test_node_schemas_custom_output() -> None:
class State(TypedDict):
hello: str
@@ -1499,7 +1444,7 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
mapper_calls = 0
class Configurable:
class Config:
model: str
@task()
@@ -1509,7 +1454,7 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non
time.sleep(input / 100)
return str(input) * 2
@entrypoint(checkpointer=checkpointer, config_schema=Configurable)
@entrypoint(checkpointer=checkpointer, config_schema=Config)
def graph(input: list[int]) -> list[str]:
futures = [mapper(i) for i in input]
mapped = [f.result() for f in futures]
@@ -2894,139 +2839,6 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2(
}
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_input(
snapshot: SnapshotAssertion,
mocker: MockerFixture,
request: pytest.FixtureRequest,
checkpointer_name: str,
) -> None:
from pydantic import BaseModel
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
def sorted_add(
x: list[str], y: Union[list[str], list[tuple[str, str]]]
) -> list[str]:
if isinstance(y[0], tuple):
for rem, _ in y:
x.remove(rem)
y = [t[1] for t in y]
return sorted(operator.add(x, y))
class InnerObject(BaseModel):
yo: int
class QueryModel(BaseModel):
query: str
class State(QueryModel):
inner: InnerObject
answer: Optional[str] = None
docs: Annotated[list[str], sorted_add]
class StateUpdate(BaseModel):
query: Optional[str] = None
answer: Optional[str] = None
docs: Optional[list[str]] = None
class Input(QueryModel):
inner: InnerObject
class Output(BaseModel):
answer: str
docs: list[str]
def rewrite_query(data: State) -> State:
return {"query": f"query: {data.query}"}
def analyzer_one(data: State) -> State:
return StateUpdate(query=f"analyzed: {data.query}")
def retriever_one(data: State) -> State:
return {"docs": ["doc1", "doc2"]}
def retriever_two(data: State) -> State:
time.sleep(0.1)
return {"docs": ["doc3", "doc4"]}
def qa(data: State) -> State:
return {"answer": ",".join(data.docs)}
def decider(data: State) -> str:
assert isinstance(data, State)
return "retriever_two"
workflow = StateGraph(State, input=Input, output=Output)
workflow.add_node("rewrite_query", rewrite_query)
workflow.add_node("analyzer_one", analyzer_one)
workflow.add_node("retriever_one", retriever_one)
workflow.add_node("retriever_two", retriever_two)
workflow.add_node("qa", qa)
workflow.set_entry_point("rewrite_query")
workflow.add_edge("rewrite_query", "analyzer_one")
workflow.add_edge("analyzer_one", "retriever_one")
workflow.add_conditional_edges(
"rewrite_query", decider, {"retriever_two": "retriever_two"}
)
workflow.add_edge(["retriever_one", "retriever_two"], "qa")
workflow.set_finish_point("qa")
app = workflow.compile()
assert app.invoke(
Input(query="what is weather in sf", inner=InnerObject(yo=1))
) == {
"docs": ["doc1", "doc2", "doc3", "doc4"],
"answer": "doc1,doc2,doc3,doc4",
}
assert [
*app.stream(Input(query="what is weather in sf", inner=InnerObject(yo=1)))
] == [
{"rewrite_query": {"query": "query: what is weather in sf"}},
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
{"retriever_two": {"docs": ["doc3", "doc4"]}},
{"retriever_one": {"docs": ["doc1", "doc2"]}},
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
]
app_w_interrupt = workflow.compile(
checkpointer=checkpointer,
interrupt_after=["retriever_one"],
)
config = {"configurable": {"thread_id": "1"}}
assert [
c
for c in app_w_interrupt.stream(
Input(query="what is weather in sf", inner=InnerObject(yo=1)), config
)
] == [
{"rewrite_query": {"query": "query: what is weather in sf"}},
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
{"retriever_two": {"docs": ["doc3", "doc4"]}},
{"retriever_one": {"docs": ["doc1", "doc2"]}},
{"__interrupt__": ()},
]
assert [c for c in app_w_interrupt.stream(None, config)] == [
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
]
assert app_w_interrupt.update_state(
config, {"docs": ["doc5"]}, as_node="rewrite_query"
) == {
"configurable": {
"thread_id": "1",
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
}
}
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_in_one_fan_out_state_graph_waiting_edge_plus_regular(
request: pytest.FixtureRequest, checkpointer_name: str
@@ -3093,24 +2905,14 @@ def test_in_one_fan_out_state_graph_waiting_edge_plus_regular(
"answer": "doc1,doc2,doc3,doc4",
}
assert [*app.stream({"query": "what is weather in sf"})] in (
[
{"rewrite_query": {"query": "query: what is weather in sf"}},
{"qa": {"answer": ""}},
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
{"retriever_two": {"docs": ["doc3", "doc4"]}},
{"retriever_one": {"docs": ["doc1", "doc2"]}},
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
],
[
{"rewrite_query": {"query": "query: what is weather in sf"}},
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
{"qa": {"answer": ""}},
{"retriever_two": {"docs": ["doc3", "doc4"]}},
{"retriever_one": {"docs": ["doc1", "doc2"]}},
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
],
)
assert [*app.stream({"query": "what is weather in sf"})] == [
{"rewrite_query": {"query": "query: what is weather in sf"}},
{"qa": {"answer": ""}},
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
{"retriever_two": {"docs": ["doc3", "doc4"]}},
{"retriever_one": {"docs": ["doc1", "doc2"]}},
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
]
app_w_interrupt = workflow.compile(
checkpointer=checkpointer,
@@ -5081,6 +4883,13 @@ def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str)
"source": "loop",
"writes": {
"alice": {
"messages": [
_AnyIdHumanMessage(
content="get user name",
additional_kwargs={},
response_metadata={},
),
],
"user_name": "Meow",
}
},
@@ -6420,6 +6229,151 @@ def test_multiple_subgraphs_checkpointer(
]
def test_merging_updates_command_parent():
# simple reducer
def append_unique(left, right):
combined = list(left)
for item in right:
if item in combined:
continue
else:
combined.append(item)
return combined
class State(TypedDict):
foo: str
bar: Annotated[list[str], append_unique]
# Define subgraph
def subgraph_node_1(state: State):
return Command(
goto="subgraph_node_2",
update={
"foo": "foo",
"bar": ["subgraph_node_1"],
},
)
def subgraph_node_2(state: State):
return Command(
goto="node_3",
update={"bar": ["subgraph_node_2"]},
graph=Command.PARENT,
)
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
# Define main graph
def node_1(state: State):
return Command(
goto="node_2",
update={"bar": ["node_1"]},
)
def node_3(state: State, store):
return Command(
update={"bar": ["node_3"]},
)
main_builder = StateGraph(State)
main_builder.add_node("node_1", node_1)
main_builder.add_node("node_2", subgraph_builder.compile())
main_builder.add_node("node_3", node_3)
main_builder.add_edge(START, "node_1")
main_builder.add_edge("node_2", "node_3")
main_graph = main_builder.compile()
assert main_graph.invoke({"foo": ""}) == {
"foo": "foo",
"bar": ["node_1", "subgraph_node_1", "subgraph_node_2", "node_3"],
}
assert list(
main_graph.stream({"foo": ""}, stream_mode="updates", subgraphs=True)
) == [
((), {"node_1": {"bar": ["node_1"]}}),
(
(AnyStr("node_2:"),),
{"subgraph_node_1": {"foo": "foo", "bar": ["subgraph_node_1"]}},
),
(
(),
{
"node_2": [
{"foo": "foo"},
{"bar": ["node_1", "subgraph_node_1"]},
{"bar": ["subgraph_node_2"]},
]
},
),
((), {"node_3": {"bar": ["node_3"]}}),
]
def test_merging_non_overlapping_updates_command_parent():
# simple reducer
def append_unique(left, right):
combined = list(left)
for item in right:
if item in combined:
continue
else:
combined.append(item)
return combined
class State(TypedDict):
foo: Annotated[list, append_unique]
# Define subgraph
def subgraph_node_1(state: State):
return Command(
goto="subgraph_node_2",
update={
"foo": ["bar"],
"bar": ["subgraph_node_1"],
},
)
def subgraph_node_2(state: State):
return Command(
goto="node_3",
update={"bar": ["subgraph_node_2"]},
graph=Command.PARENT,
)
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
# Define main graph
def node_1(state: State):
return Command(
goto="node_2",
update={"foo": ["foo"]},
)
def node_3(state: State, store):
return Command(
update={"foo": ["baz"]},
)
main_builder = StateGraph(State)
main_builder.add_node("node_1", node_1)
main_builder.add_node("node_2", subgraph_builder.compile())
main_builder.add_node("node_3", node_3)
main_builder.add_edge(START, "node_1")
main_builder.add_edge("node_2", "node_3")
main_graph = main_builder.compile()
assert main_graph.invoke({"foo": []}) == {
"foo": ["foo", "bar", "baz"],
}
def test_entrypoint_output_schema_with_return_and_save() -> None:
"""Test output schema inference with entrypoint.final."""
@@ -6148,6 +6148,13 @@ async def test_parent_command(checkpointer_name: str) -> None:
"source": "loop",
"writes": {
"alice": {
"messages": [
_AnyIdHumanMessage(
content="get user name",
additional_kwargs={},
response_metadata={},
),
],
"user_name": "Meow",
}
},
-43
View File
@@ -1,43 +0,0 @@
import sys
import typing
import pydantic
import typing_extensions
from langgraph.utils.pydantic import is_supported_by_pydantic
def test_is_supported_by_pydantic() -> None:
"""Test if types are supported by pydantic."""
class TypedDictExtensions(typing_extensions.TypedDict):
x: int
assert is_supported_by_pydantic(TypedDictExtensions) is True
class VanillaClass:
x: int
assert is_supported_by_pydantic(VanillaClass) is False
class BuiltinTypedDict(typing.TypedDict): # noqa: TID251
x: int
if sys.version_info >= (3, 12):
assert is_supported_by_pydantic(BuiltinTypedDict) is True
else:
assert is_supported_by_pydantic(BuiltinTypedDict) is False
class PydanticModel(pydantic.BaseModel):
x: int
assert is_supported_by_pydantic(PydanticModel) is True
if hasattr(pydantic, "v1"):
class PydanticModelV1(pydantic.v1.BaseModel):
x: int
assert is_supported_by_pydantic(PydanticModelV1) is False
assert is_supported_by_pydantic(int) is False
@@ -22,7 +22,6 @@ from langchain_core.runnables import (
Runnable,
RunnableBinding,
RunnableConfig,
RunnableSequence,
)
from langchain_core.tools import BaseTool
from pydantic import BaseModel
@@ -134,16 +133,6 @@ def _convert_modifier_to_prompt(func: F) -> F:
def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> bool:
if isinstance(model, RunnableSequence):
model = next(
(
step
for step in model.steps
if isinstance(step, (RunnableBinding, BaseChatModel))
),
model,
)
if not isinstance(model, RunnableBinding):
return True
@@ -179,16 +168,6 @@ def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> b
def _get_model(model: LanguageModelLike) -> BaseChatModel:
"""Get the underlying model from a RunnableBinding or return the model itself."""
if isinstance(model, RunnableSequence):
model = next(
(
step
for step in model.steps
if isinstance(step, (RunnableBinding, BaseChatModel))
),
model,
)
if isinstance(model, RunnableBinding):
model = model.bound
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-prebuilt"
version = "0.1.2"
version = "0.1.1"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
authors = []
license = "MIT"
-65
View File
@@ -35,8 +35,6 @@ from langgraph.prebuilt import (
)
from langgraph.prebuilt.chat_agent_executor import (
AgentState,
_get_model,
_should_bind_tools,
_validate_chat_history,
)
from langgraph.prebuilt.tool_node import (
@@ -1326,66 +1324,3 @@ def test_tool_node_node_interrupt(
ns=[AnyStr("tools:")],
),
)
@pytest.mark.parametrize("tool_style", ["openai", "anthropic"])
def test_should_bind_tools(tool_style: str) -> None:
@dec_tool
def some_tool(some_val: int) -> str:
"""Tool docstring."""
return "meow"
@dec_tool
def some_other_tool(some_val: int) -> str:
"""Tool docstring."""
return "meow"
model = FakeToolCallingModel(tool_style=tool_style)
# should bind when a regular model
assert _should_bind_tools(model, [])
assert _should_bind_tools(model, [some_tool])
# should bind when a seq
seq = model | RunnableLambda(lambda message: message)
assert _should_bind_tools(seq, [])
assert _should_bind_tools(seq, [some_tool])
# should not bind when a model with tools
assert not _should_bind_tools(model.bind_tools([some_tool]), [some_tool])
# should not bind when a seq with tools
seq_with_tools = model.bind_tools([some_tool]) | RunnableLambda(
lambda message: message
)
assert not _should_bind_tools(seq_with_tools, [some_tool])
# should raise on invalid inputs
with pytest.raises(ValueError):
_should_bind_tools(model.bind_tools([some_tool]), [])
with pytest.raises(ValueError):
_should_bind_tools(model.bind_tools([some_tool]), [some_other_tool])
with pytest.raises(ValueError):
_should_bind_tools(model.bind_tools([some_tool]), [some_tool, some_other_tool])
def test_get_model() -> None:
model = FakeToolCallingModel(tool_calls=[])
assert _get_model(model) == model
@dec_tool
def some_tool(some_val: int) -> str:
"""Tool docstring."""
return "meow"
model_with_tools = model.bind_tools([some_tool])
assert _get_model(model_with_tools) == model
seq = model | RunnableLambda(lambda message: message)
assert _get_model(seq) == model
seq_with_tools = model.bind_tools([some_tool]) | RunnableLambda(
lambda message: message
)
assert _get_model(seq_with_tools) == model
with pytest.raises(TypeError):
_get_model(RunnableLambda(lambda message: message))
-8
View File
@@ -10,14 +10,6 @@ react.cjs
react.js
react.d.ts
react.d.cts
react-ui.cjs
react-ui.js
react-ui.d.ts
react-ui.d.cts
react-ui/server.cjs
react-ui/server.js
react-ui/server.d.ts
react-ui/server.d.cts
node_modules
dist
.yarn
+1 -7
View File
@@ -11,13 +11,7 @@ function abs(relativePath) {
export const config = {
internals: [/react/],
entrypoints: {
index: "index",
client: "client",
react: "react/index",
"react-ui": "react-ui/index",
"react-ui/server": "react-ui/server/index",
},
entrypoints: { index: "index", client: "client", react: "react/index" },
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
cjsDestination: "./dist",
+5 -33
View File
@@ -1,13 +1,13 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.48",
"version": "0.0.45",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
"scripts": {
"clean": "rm -rf dist/ dist-cjs/",
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
"prepack": "yarn run build",
"prepublish": "yarn run build",
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts",
@@ -29,8 +29,7 @@
"@types/jest": "^29.5.12",
"@types/node": "^20.12.12",
"@types/uuid": "^9.0.1",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react": "18.3.2",
"concat-md": "^0.5.1",
"jest": "^29.7.0",
"prettier": "^3.2.5",
@@ -38,8 +37,7 @@
"typedoc": "^0.27.7",
"typedoc-plugin-markdown": "^4.4.2",
"typescript": "^5.4.5",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^18.3.1"
},
"peerDependencies": {
"react": "^18 || ^19",
@@ -81,24 +79,6 @@
"import": "./react.js",
"require": "./react.cjs"
},
"./react-ui": {
"types": {
"import": "./react-ui.d.ts",
"require": "./react-ui.d.cts",
"default": "./react-ui.d.ts"
},
"import": "./react-ui.js",
"require": "./react-ui.cjs"
},
"./react-ui/server": {
"types": {
"import": "./react-ui/server.d.ts",
"require": "./react-ui/server.d.cts",
"default": "./react-ui/server.d.ts"
},
"import": "./react-ui/server.js",
"require": "./react-ui/server.cjs"
},
"./package.json": "./package.json"
},
"files": [
@@ -114,14 +94,6 @@
"react.cjs",
"react.js",
"react.d.ts",
"react.d.cts",
"react-ui.cjs",
"react-ui.js",
"react-ui.d.ts",
"react-ui.d.cts",
"react-ui/server.cjs",
"react-ui/server.js",
"react-ui/server.d.ts",
"react-ui/server.d.cts"
"react.d.cts"
]
}
-1
View File
@@ -1,7 +1,6 @@
export { Client } from "./client.js";
export type {
AssistantBase,
Assistant,
AssistantVersion,
AssistantGraph,
-251
View File
@@ -1,251 +0,0 @@
import { useStream } from "../react/index.js";
import type { UIMessage } from "./types.js";
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as JsxRuntime from "react/jsx-runtime";
import type { UseStream } from "../react/stream.js";
const UseStreamContext = React.createContext<{
stream: ReturnType<typeof useStream>;
meta: unknown;
}>(null!);
type BagTemplate = {
ConfigurableType?: Record<string, unknown>;
InterruptType?: unknown;
CustomEventType?: unknown;
UpdateType?: unknown;
MetaType?: unknown;
};
type GetMetaType<Bag extends BagTemplate> = Bag extends { MetaType: unknown }
? Bag["MetaType"]
: unknown;
interface UseStreamContext<
StateType extends Record<string, unknown> = Record<string, unknown>,
Bag extends BagTemplate = BagTemplate,
> extends UseStream<StateType, Bag> {
meta?: GetMetaType<Bag>;
}
export function useStreamContext<
StateType extends Record<string, unknown> = Record<string, unknown>,
Bag extends {
ConfigurableType?: Record<string, unknown>;
InterruptType?: unknown;
CustomEventType?: unknown;
UpdateType?: unknown;
MetaType?: unknown;
} = BagTemplate,
>(): UseStreamContext<StateType, Bag> {
const ctx = React.useContext(UseStreamContext);
if (!ctx) {
throw new Error(
"useStreamContext must be used within a LoadExternalComponent",
);
}
return new Proxy(ctx, {
get(target, prop: keyof UseStreamContext<StateType, Bag>) {
if (prop === "meta") return target.meta;
return target.stream[prop];
},
}) as unknown as UseStreamContext<StateType, Bag>;
}
interface ComponentTarget {
comp: React.FunctionComponent | React.ComponentClass;
target: HTMLElement;
}
class ComponentStore {
private cache: Record<string, ComponentTarget> = {};
private boundCache: Record<
string,
{
subscribe: (onStoreChange: () => void) => () => void;
getSnapshot: () => ComponentTarget | undefined;
}
> = {};
private callbacks: Record<
string,
((
comp: React.FunctionComponent | React.ComponentClass,
el: HTMLElement,
) => void)[]
> = {};
respond(
shadowRootId: string,
comp: React.FunctionComponent | React.ComponentClass,
targetElement: HTMLElement,
) {
this.cache[shadowRootId] = { comp, target: targetElement };
this.callbacks[shadowRootId]?.forEach((c) => c(comp, targetElement));
}
getBoundStore(shadowRootId: string) {
this.boundCache[shadowRootId] ??= {
subscribe: (onStoreChange: () => void) => {
this.callbacks[shadowRootId] ??= [];
this.callbacks[shadowRootId].push(onStoreChange);
return () => {
this.callbacks[shadowRootId] = this.callbacks[shadowRootId].filter(
(c) => c !== onStoreChange,
);
};
},
getSnapshot: () => this.cache[shadowRootId],
};
return this.boundCache[shadowRootId];
}
}
const COMPONENT_STORE = new ComponentStore();
const COMPONENT_PROMISE_CACHE: Record<string, Promise<string> | undefined> = {};
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
interface LoadExternalComponentProps
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
/** API URL of the LangGraph Platform */
apiUrl?: string;
/** ID of the assistant */
assistantId: string;
/** Stream of the assistant */
stream: ReturnType<typeof useStream>;
/** UI message to be rendered */
message: UIMessage;
/** Additional context to be passed to the child component */
meta?: unknown;
/** Fallback to be rendered when the component is loading */
fallback?: React.ReactNode;
/**
* Map of components that can be rendered directly without fetching the UI code
* from the server.
*/
components?: Record<string, React.FunctionComponent | React.ComponentClass>;
}
function fetchComponent(
apiUrl: string,
assistantId: string,
agentName: string,
): Promise<string> {
const cacheKey = `${apiUrl}-${assistantId}-${agentName}`;
if (COMPONENT_PROMISE_CACHE[cacheKey] != null) {
return COMPONENT_PROMISE_CACHE[cacheKey] as Promise<string>;
}
const request: Promise<string> = fetch(`${apiUrl}/ui/${assistantId}`, {
headers: { Accept: "text/html", "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ name: agentName }),
}).then((a) => a.text());
COMPONENT_PROMISE_CACHE[cacheKey] = request;
return request;
}
export function LoadExternalComponent({
apiUrl = "http://localhost:2024",
assistantId,
stream,
message,
meta,
fallback,
components,
...props
}: LoadExternalComponentProps) {
const ref = React.useRef<HTMLDivElement>(null);
const id = React.useId();
const shadowRootId = `child-shadow-${id}`;
const store = React.useMemo(
() => COMPONENT_STORE.getBoundStore(shadowRootId),
[shadowRootId],
);
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
const clientComponent = components?.[message.name];
const hasClientComponent = clientComponent != null;
React.useEffect(() => {
if (hasClientComponent) return;
fetchComponent(apiUrl, assistantId, message.name).then((html) => {
const dom = ref.current;
if (!dom) return;
const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" });
const fragment = document
.createRange()
.createContextualFragment(
html.replace("{{shadowRootId}}", shadowRootId),
);
root.appendChild(fragment);
});
}, [apiUrl, assistantId, message.name, shadowRootId, hasClientComponent]);
if (hasClientComponent) {
return React.createElement(clientComponent, message.content);
}
return (
<>
<div id={shadowRootId} ref={ref} {...props} />
<UseStreamContext.Provider value={{ stream, meta }}>
{state?.target != null
? ReactDOM.createPortal(
React.createElement(state.comp, message.content),
state.target,
)
: fallback}
</UseStreamContext.Provider>
</>
);
}
declare global {
interface Window {
[EXT_STORE_SYMBOL]: ComponentStore;
[REQUIRE_SYMBOL]: (name: string) => unknown;
}
}
export function bootstrapUiContext() {
if (typeof window === "undefined") {
console.warn(
"Attempting to bootstrap UI context outside of browser environment. " +
"Avoid importing from `@langchain/langgraph-sdk/react-ui` in server context.",
);
return;
}
window[EXT_STORE_SYMBOL] = COMPONENT_STORE;
window[REQUIRE_SYMBOL] = (name: string) => {
if (name === "react") return React;
if (name === "react-dom") return ReactDOM;
if (name === "react/jsx-runtime") return JsxRuntime;
if (name === "@langchain/langgraph-sdk/react") return { useStream };
if (name === "@langchain/langgraph-sdk/react-ui") {
return {
useStreamContext,
LoadExternalComponent: () => {
throw new Error("Nesting LoadExternalComponent is not supported");
},
};
}
throw new Error(`Unknown module...: ${name}`);
};
}
-5
View File
@@ -1,5 +0,0 @@
import { bootstrapUiContext } from "./client.js";
bootstrapUiContext();
export { useStreamContext, LoadExternalComponent } from "./client.js";
export type { UIMessage, RemoveUIMessage } from "./types.js";
-6
View File
@@ -1,6 +0,0 @@
export { typedUi } from "./server.js";
export {
uiMessageReducer,
type UIMessage,
type RemoveUIMessage,
} from "../types.js";
-49
View File
@@ -1,49 +0,0 @@
import { v4 as uuidv4 } from "uuid";
import type { ComponentPropsWithoutRef, ElementType } from "react";
import type { RemoveUIMessage, UIMessage } from "../types.js";
export const typedUi = <Decl extends Record<string, ElementType>>(config: {
writer?: (chunk: unknown) => void;
runId?: string;
metadata?: Record<string, unknown>;
tags?: string[];
runName?: string;
}) => {
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
let collect: (UIMessage | RemoveUIMessage)[] = [];
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
if (!runId) throw new Error("run_id is required");
const metadata = {
...config.metadata,
tags: config.tags,
name: config.runName,
run_id: runId,
};
const create = <K extends keyof PropMap & string>(
name: K,
props: PropMap[K],
): UIMessage => ({
type: "ui" as const,
id: uuidv4(),
name,
content: props,
additional_kwargs: metadata,
});
const remove = (id: string): RemoveUIMessage => ({ type: "remove-ui", id });
return {
create,
remove,
collect,
write: <K extends keyof PropMap & string>(name: K, props: PropMap[K]) => {
const evt: UIMessage = create(name, props);
collect.push(evt);
config.writer?.(evt);
},
};
};
-40
View File
@@ -1,40 +0,0 @@
export interface UIMessage {
type: "ui";
id: string;
name: string;
content: Record<string, unknown>;
additional_kwargs: {
run_id: string;
[key: string]: unknown;
};
}
export interface RemoveUIMessage {
type: "remove-ui";
id: string;
}
export function uiMessageReducer(
state: UIMessage[],
update: UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[],
) {
const events = Array.isArray(update) ? update : [update];
let newState = state.slice();
for (const event of events) {
if (event.type === "remove-ui") {
newState = newState.filter((ui) => ui.id !== event.id);
continue;
}
const index = state.findIndex((ui) => ui.id === event.id);
if (index !== -1) {
newState[index] = event;
} else {
newState.push(event);
}
}
return newState;
}
+10 -34
View File
@@ -366,11 +366,11 @@ const useControllableThreadId = (options?: {
onThreadIdRef.current?.(threadId);
}, []);
if (!options || !("threadId" in options)) {
if (typeof options?.threadId === "undefined") {
return [localThreadId, onThreadId];
}
return [options.threadId ?? null, onThreadId];
return [options.threadId, onThreadId];
};
type BagTemplate = {
@@ -424,16 +424,6 @@ interface UseStreamOptions<
*/
apiKey?: ClientConfig["apiKey"];
/**
* Custom call options, such as custom fetch implementation.
*/
callerOptions?: ClientConfig["callerOptions"];
/**
* Default headers to send with requests.
*/
defaultHeaders?: ClientConfig["defaultHeaders"];
/**
* Specify the key within the state that contains messages.
* Defaults to "messages".
@@ -482,7 +472,7 @@ interface UseStreamOptions<
onThreadId?: (threadId: string) => void;
}
export interface UseStream<
interface UseStream<
StateType extends Record<string, unknown> = Record<string, unknown>,
Bag extends BagTemplate = BagTemplate,
> {
@@ -613,19 +603,8 @@ export function useStream<
messagesKey ??= "messages";
const client = useMemo(
() =>
new Client({
apiUrl: options.apiUrl,
apiKey: options.apiKey,
callerOptions: options.callerOptions,
defaultHeaders: options.defaultHeaders,
}),
[
options.apiKey,
options.apiUrl,
options.callerOptions,
options.defaultHeaders,
],
() => new Client({ apiUrl: options.apiUrl, apiKey: options.apiKey }),
[options.apiKey, options.apiUrl],
);
const [threadId, onThreadId] = useControllableThreadId(options);
@@ -644,12 +623,9 @@ export function useStream<
>([]);
const trackStreamMode = useCallback(
(...mode: Exclude<StreamMode, "debug" | "messages">[]) => {
for (const m of mode) {
if (!trackStreamModeRef.current.includes(m)) {
trackStreamModeRef.current.push(m);
}
}
(mode: Exclude<StreamMode, "debug" | "messages">) => {
if (!trackStreamModeRef.current.includes(mode))
trackStreamModeRef.current.push(mode);
},
[],
);
@@ -932,7 +908,7 @@ export function useStream<
},
get messages() {
trackStreamMode("messages-tuple", "values");
trackStreamMode("messages-tuple");
return getMessages(values);
},
@@ -940,7 +916,7 @@ export function useStream<
message: Message,
index?: number,
): MessageMetadata<StateType> | undefined {
trackStreamMode("messages-tuple", "values");
trackStreamMode("messages-tuple");
return messageMetadata?.find(
(m) => m.messageId === (message.id ?? index),
);
+3 -3
View File
@@ -110,9 +110,6 @@ export interface AssistantBase {
/** The version of the assistant. */
version: number;
/** The name of the assistant */
name: string;
}
export interface AssistantVersion extends AssistantBase {}
@@ -120,6 +117,9 @@ export interface AssistantVersion extends AssistantBase {}
export interface Assistant extends AssistantBase {
/** The last time the assistant was updated. */
updated_at: string;
/** The name of the assistant */
name: string;
}
export interface AssistantGraph {
+23 -25
View File
@@ -1005,16 +1005,17 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==
"@types/react-dom@^19.0.3":
version "19.0.3"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.3.tgz#0804dfd279a165d5a0ad8b53a5b9e65f338050a4"
integrity sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==
"@types/prop-types@*":
version "15.7.14"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2"
integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
"@types/react@^19.0.8":
version "19.0.9"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.9.tgz#675255eb7d978bdaf71f9d08f6b740d41b0b7f32"
integrity sha512-FedNTYgmMwSZmD1Sru/W1gJKuiYCN/3SuBkmZkcxX+FpO5zL76B22A9YNfAKg4HQO3Neh/30AiynP6BELdU0qQ==
"@types/react@18.3.2":
version "18.3.2"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.2.tgz#462ae4904973bc212fa910424d901e3d137dbfcd"
integrity sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/retry@0.12.0":
@@ -2895,7 +2896,7 @@ js-tiktoken@^1.0.12:
dependencies:
base64-js "^1.5.1"
js-tokens@^4.0.0:
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@@ -2990,6 +2991,13 @@ longest-streak@^2.0.0:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lru-cache@^10.2.0:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
@@ -3632,22 +3640,17 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
react-dom@^19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
dependencies:
scheduler "^0.25.0"
react-is@^18.0.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react@^19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
react@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
dependencies:
loose-envify "^1.1.0"
read-pkg-up@^7.0.1:
version "7.0.1"
@@ -3837,11 +3840,6 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
scheduler@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015"
integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
"semver@2 || 3 || 4 || 5":
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
+3 -5
View File
@@ -388,9 +388,7 @@ class AssistantsClient:
'created_at': '2024-06-25T17:10:33.109781+00:00',
'updated_at': '2024-06-25T17:10:33.109781+00:00',
'config': {},
'metadata': {'created_by': 'system'},
'version': 1,
'name': 'my_assistant'
'metadata': {'created_by': 'system'}
}
""" # noqa: E501
@@ -744,7 +742,7 @@ class AssistantsClient:
offset: The number of versions to skip.
Returns:
list[AssistantVersion]: A list of assistant versions.
list[Assistant]: A list of assistants.
Example Usage:
@@ -2519,7 +2517,7 @@ def encode_json(json: Any) -> tuple[dict[str, str], bytes]:
def decode_json(r: httpx.Response) -> Any:
body = r.read()
return orjson.loads(body) if body else None
return orjson.loads(body if body else None)
class SyncAssistantsClient:
+2 -2
View File
@@ -169,8 +169,6 @@ class AssistantBase(TypedDict):
"""The assistant metadata."""
version: int
"""The version of the assistant"""
name: str
"""The name of the assistant"""
class AssistantVersion(AssistantBase):
@@ -184,6 +182,8 @@ class Assistant(AssistantBase):
updated_at: datetime
"""The last time the assistant was updated."""
name: str
"""The name of the assistant"""
class Interrupt(TypedDict, total=False):
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.54"
version = "0.1.53"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"