mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-04 23:45:08 +02:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf26d5f592 | ||
|
|
654c5f21e4 | ||
|
|
38d806733d | ||
|
|
86ddd8da10 | ||
|
|
a5f5d0c4df | ||
|
|
7486adabdf | ||
|
|
12ad47e4e8 | ||
|
|
90f7f776cf | ||
|
|
c7306f7aed | ||
|
|
20bd71e289 | ||
|
|
283485753f | ||
|
|
ba7f9975fa | ||
|
|
8c4904bee9 | ||
|
|
6bb06b8702 | ||
|
|
7a16e33833 |
@@ -20,7 +20,6 @@ BLOCKLIST_COMMANDS = (
|
||||
|
||||
NOTEBOOKS_NO_CASSETTES = (
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/how-tos/many-tools.ipynb"
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_EXECUTION = [
|
||||
@@ -49,7 +48,10 @@ NOTEBOOKS_NO_EXECUTION = [
|
||||
"docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/tutorials/tot/tot.ipynb",
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
"docs/how-tos/streaming-specific-nodes.ipynb",
|
||||
"docs/tutorials/llm-compiler/LLMCompiler.ipynb",
|
||||
"docs/tutorials/customer-support/customer-support.ipynb", # relies on openai embeddings, doesn't play well w/ VCR
|
||||
"docs/how-tos/many-tools.ipynb", # relies on openai embeddings, doesn't play well w/ VCR
|
||||
]
|
||||
|
||||
|
||||
@@ -86,6 +88,12 @@ def has_blocklisted_command(code: str, metadata: dict) -> bool:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_mermaid_retries(code: str) -> str:
|
||||
return code.replace(
|
||||
"draw_mermaid_png()",
|
||||
"draw_mermaid_png(max_retries=10, retry_delay=2.0)"
|
||||
)
|
||||
|
||||
|
||||
def add_vcr_to_notebook(
|
||||
notebook: nbformat.NotebookNode, cassette_prefix: str
|
||||
@@ -180,6 +188,15 @@ def add_vcr_to_notebook(
|
||||
return notebook
|
||||
|
||||
|
||||
def add_mermaid_retries_to_notebook(notebook: nbformat.NotebookNode) -> nbformat.NotebookNode:
|
||||
for cell in notebook.cells:
|
||||
if cell.cell_type != "code":
|
||||
continue
|
||||
|
||||
cell.source = add_mermaid_retries(cell.source)
|
||||
return notebook
|
||||
|
||||
|
||||
def process_notebooks(should_comment_install_cells: bool) -> None:
|
||||
for directory in NOTEBOOK_DIRS:
|
||||
for root, _, files in os.walk(directory):
|
||||
@@ -201,6 +218,8 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
|
||||
notebook, cassette_prefix=cassette_prefix
|
||||
)
|
||||
|
||||
notebook = add_mermaid_retries_to_notebook(notebook)
|
||||
|
||||
if notebook_path in NOTEBOOKS_NO_EXECUTION:
|
||||
# Add a cell at the beginning to indicate that this notebook should not be executed
|
||||
warning_cell = nbformat.v4.new_markdown_cell(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# LangGraph Studio With Local Deployment
|
||||
|
||||
!!! warning "Browser Compatibility"
|
||||
Viewing the studio page of a local LangGraph deployment does not work in Safari. Use Chrome instead.
|
||||
Safari blocks `localhost` connections to Studio. To work around this, start the server with `--tunnel` and you’ll be able to access Studio from Safari via a secure tunnel.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -10,9 +10,6 @@ The LangGraph command line interface includes commands to build and run a LangGr
|
||||
=== "Python"
|
||||
```bash
|
||||
pip install langgraph-cli
|
||||
|
||||
# Install via Homebrew
|
||||
brew install langgraph-cli
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
@@ -298,6 +295,11 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
| `--no-reload` | | Disable auto-reload |
|
||||
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
|
||||
| `--debug-port INTEGER` | | Port for debugger to listen on |
|
||||
| `--wait-for-client` | `False` | Wait for a debugger client to connect to the debug port before starting the server |
|
||||
| `--no-browser` | | Skip automatically opening the browser when the server starts |
|
||||
| `--studio-url TEXT` | | URL of the LangGraph Studio instance to connect to. Defaults to https://smith.langchain.com |
|
||||
| `--allow-blocking` | `False` | Do not raise errors for synchronous I/O blocking operations in your code (added in `0.2.6`) |
|
||||
| `--tunnel` | `False` | Expose the local server via a public tunnel (Cloudflare) for remote frontend access. This avoids issues with browsers like Safari or networks blocking localhost connections |
|
||||
| `--help` | | Display command documentation |
|
||||
|
||||
|
||||
@@ -321,6 +323,11 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
| `--no-reload` | | Disable auto-reload |
|
||||
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
|
||||
| `--debug-port INTEGER` | | Port for debugger to listen on |
|
||||
| `--wait-for-client` | `False` | Wait for a debugger client to connect to the debug port before starting the server |
|
||||
| `--no-browser` | | Skip automatically opening the browser when the server starts |
|
||||
| `--studio-url TEXT` | | URL of the LangGraph Studio instance to connect to. Defaults to https://smith.langchain.com |
|
||||
| `--allow-blocking` | `False` | Do not raise errors for synchronous I/O blocking operations in your code |
|
||||
| `--tunnel` | `False` | Expose the local server via a public tunnel (Cloudflare) for remote frontend access. This avoids issues with browsers or networks blocking localhost connections |
|
||||
| `--help` | | Display command documentation |
|
||||
|
||||
### `build`
|
||||
|
||||
@@ -55,6 +55,14 @@ Set `LANGSMITH_TRACING` to `false` to disable tracing to LangSmith.
|
||||
|
||||
Configure [log level](https://docs.python.org/3/library/logging.html#logging-levels). Defaults to `INFO`.
|
||||
|
||||
## `LOG_JSON`
|
||||
|
||||
Set `LOG_JSON` to `true` to render all log messages as JSON objects using the configured `JSONRenderer`. This produces structured logs that can be easily parsed or ingested by log management systems. Defaults to `false`.
|
||||
|
||||
## `LOG_COLOR`
|
||||
|
||||
This is mainly relevant in the context of using the dev server via the `langgraph dev` command. Set `LOG_COLOR` to `true` to enable ANSI-colored console output when using the default console renderer. Disabling color output by setting this variable to `false` produces monochrome logs. Defaults to `true`.
|
||||
|
||||
## `N_JOBS_PER_WORKER`
|
||||
|
||||
Number of jobs per worker for the LangGraph Server task queue. Defaults to `10`.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.safari {
|
||||
color: #0070C9;
|
||||
}
|
||||
@@ -14,3 +14,4 @@ Errors referenced below will have an `lc_error_code` property corresponding to o
|
||||
These guides provide troubleshooting information for errors that are specific to the LangGraph Platform.
|
||||
|
||||
- [INVALID_LICENSE](./INVALID_LICENSE.md)
|
||||
- [Studio Errors](../studio.md)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Troubleshooting LangGraph Studio
|
||||
|
||||
## :fontawesome-brands-safari:{ .safari } Safari connection error with local dev server
|
||||
|
||||
Safari blocks plain‑HTTP traffic on localhost. If you start Studio with a vanilla
|
||||
`langgraph dev`, the page may report a "Failed to load assistants" error (or something similar) and the browser DevTools will show network errors.
|
||||
|
||||
#### Quick fix — run Studio through a secure Cloudflare tunnel
|
||||
|
||||
=== "Python"
|
||||
|
||||
```shell
|
||||
pip install -U langgraph-cli>=0.2.6 # Python
|
||||
langgraph dev --tunnel
|
||||
```
|
||||
=== "JS"
|
||||
|
||||
```shell
|
||||
# Requires @langchain/langgraph-cli>=0.0.26
|
||||
npx @langchain/langgraph-cli dev
|
||||
```
|
||||
|
||||
The command prints a URL like:
|
||||
|
||||
```shell
|
||||
https://smith.langchain.com/studio/?baseUrl=https://hamilton-praise-heart-costumes.trycloudflare.com
|
||||
```
|
||||
where
|
||||
```shell
|
||||
?baseUrl=https://hamilton-praise-heart-costumes.trycloudflare.com
|
||||
```
|
||||
indicates the endpoint where your agent server is exposed.
|
||||
|
||||
Open that URL in Safari and Studio should load immediately.
|
||||
|
||||
#### Alternative — use a Chromium‑based browser
|
||||
|
||||
Chrome, Edge, and Brave allow HTTP on localhost, so a plain `langgraph dev` should work without extra steps.
|
||||
|
||||
#### If it’s still not loading
|
||||
|
||||
1. Make sure the `baseUrl` query parameter in the studio URL points to the **tunnel URL** NOT to localhost.
|
||||
2. Confirm your CLI version with `langgraph --version`.
|
||||
|
||||
No other configuration, certificates, or CORS tweaks are required.
|
||||
@@ -741,13 +741,7 @@
|
||||
"from IPython.display import Image, display\n",
|
||||
"from langchain_core.runnables.graph import MermaidDrawMethod\n",
|
||||
"\n",
|
||||
"display(\n",
|
||||
" Image(\n",
|
||||
" app.get_graph().draw_mermaid_png(\n",
|
||||
" draw_method=MermaidDrawMethod.API,\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
")"
|
||||
"display(Image(app.get_graph().draw_mermaid_png()))"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
site_name: ""
|
||||
site_description: Build language agents as graphs
|
||||
site_name: "LangGraph"
|
||||
site_description: Build reliable, stateful AI systems, without giving up control
|
||||
site_url: https://langchain-ai.github.io/langgraph/
|
||||
repo_url: https://github.com/langchain-ai/langgraph
|
||||
edit_uri: edit/main/docs/docs/
|
||||
@@ -400,6 +400,7 @@ nav:
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
- troubleshooting/studio.md
|
||||
- LangGraph Academy Course: https://academy.langchain.com/courses/intro-to-langgraph
|
||||
|
||||
- Agents:
|
||||
@@ -549,3 +550,4 @@ copyright: >
|
||||
Copyright © 2025 LangChain, Inc | <a href="#__consent">Consent Preferences</a>
|
||||
extra_css:
|
||||
- stylesheets/version_admonitions.css
|
||||
- stylesheets/logos.css
|
||||
|
||||
Generated
+6
-8
@@ -3387,14 +3387,14 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.52"
|
||||
version = "0.3.54"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["docs", "test"]
|
||||
files = [
|
||||
{file = "langchain_core-0.3.52-py3-none-any.whl", hash = "sha256:cd137109c1e3d04f5a582c2cae9539b2cd5e4b795f486b58969dbc3d0387fe7c"},
|
||||
{file = "langchain_core-0.3.52.tar.gz", hash = "sha256:f1981ec9efa4fceb11ff5ca57f5f9c8e22859cea3a94f8a044e6de8815afbd57"},
|
||||
{file = "langchain_core-0.3.54-py3-none-any.whl", hash = "sha256:cd42155d9089e2fd4695ee02a4b2bc6daf55b9d4e1a37639647cf2455ed4fa04"},
|
||||
{file = "langchain_core-0.3.54.tar.gz", hash = "sha256:55ce38939038e19b1271f36f512335462d7f64057b531598b3651d2b403e1b42"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3530,7 +3530,7 @@ langchain-core = ">=0.3.45,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
optional = false
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
@@ -3541,7 +3541,7 @@ develop = true
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.1,<0.4"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-prebuilt = ">=0.1.1,<0.2"
|
||||
langgraph-prebuilt = ">=0.1.8,<0.2"
|
||||
langgraph-sdk = "^0.1.42"
|
||||
xxhash = "^3.5.0"
|
||||
|
||||
@@ -5987,7 +5987,6 @@ optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
|
||||
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
|
||||
]
|
||||
|
||||
@@ -5999,7 +5998,6 @@ optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
|
||||
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
|
||||
]
|
||||
|
||||
@@ -8902,4 +8900,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "45bbc644a3b878063f5cbb75eed56540423315784f8dd42cfd3937c910dfc9c5"
|
||||
content-hash = "36d7e4c4eba50d5e4dfb2e99964d7b51fe17d36238a912765cca8fc360216079"
|
||||
|
||||
@@ -43,6 +43,7 @@ langchain-cohere = "^0.4.2"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
langchain = "^0.3.8"
|
||||
langchain-core = "^0.3.54"
|
||||
langchain-openai = "^0.3.7"
|
||||
langchain-anthropic = "^0.3.8"
|
||||
langchain-nomic = "^0.1.3"
|
||||
|
||||
@@ -1320,7 +1320,7 @@ def _ensure_index_config(
|
||||
index_config = index_config.copy()
|
||||
tokenized: list[tuple[str, Union[Literal["$"], list[str]]]] = []
|
||||
tot = 0
|
||||
text_fields = index_config.get("text_fields") or ["$"]
|
||||
text_fields = index_config.get("fields") or ["$"]
|
||||
if isinstance(text_fields, str):
|
||||
text_fields = [text_fields]
|
||||
if not isinstance(text_fields, list):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.20"
|
||||
version = "2.0.21"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -377,7 +377,7 @@ async def _create_vector_store(
|
||||
"vector_type": vector_type,
|
||||
},
|
||||
"distance_type": distance_type,
|
||||
"text_fields": text_fields,
|
||||
"fields": text_fields,
|
||||
}
|
||||
|
||||
async with await AsyncConnection.connect(
|
||||
|
||||
@@ -401,7 +401,7 @@ def _create_vector_store(
|
||||
"vector_type": vector_type,
|
||||
},
|
||||
"distance_type": distance_type,
|
||||
"text_fields": text_fields,
|
||||
"fields": text_fields,
|
||||
}
|
||||
|
||||
with Connection.connect(admin_conn_string, autocommit=True) as conn:
|
||||
|
||||
@@ -572,6 +572,14 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
|
||||
help="Don't raise errors for synchronous I/O blocking operations in your code.",
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"--tunnel",
|
||||
is_flag=True,
|
||||
help="Expose the local server via a public tunnel (in this case, Cloudflare) "
|
||||
"for remote frontend access. This avoids issues with browsers "
|
||||
"or networks blocking localhost connections.",
|
||||
default=False,
|
||||
)
|
||||
@cli.command(
|
||||
"dev",
|
||||
help="🏃♀️➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
|
||||
@@ -588,6 +596,7 @@ def dev(
|
||||
wait_for_client: bool,
|
||||
studio_url: Optional[str],
|
||||
allow_blocking: bool,
|
||||
tunnel: bool,
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
@@ -655,6 +664,7 @@ def dev(
|
||||
ui_config=config_json.get("ui_config"),
|
||||
studio_url=studio_url,
|
||||
allow_blocking=allow_blocking,
|
||||
tunnel=tunnel,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Generated
+551
-392
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
click = "^8.1.7"
|
||||
langgraph-api = { version = ">=0.1.0,<0.2.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-api = { version = ">=0.1.12,<0.2.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-runtime-inmem = { version = ">=0.0.1,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-sdk = { version = ">=0.1.0,<0.2.0", optional = true, python = ">=3.11,<4.0" }
|
||||
python-dotenv = { version = ">=0.8.0", optional = true }
|
||||
|
||||
@@ -5,23 +5,25 @@ from inspect import isclass
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Hashable,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Discriminator
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Annotated
|
||||
from typing_extensions import Annotated, Literal
|
||||
|
||||
__all__ = ["SchemaCoercionMapper"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_cache: weakref.WeakKeyDictionary[Type[Any], dict[int, "SchemaCoercionMapper"]] = (
|
||||
weakref.WeakKeyDictionary()
|
||||
)
|
||||
@@ -61,7 +63,9 @@ class SchemaCoercionMapper:
|
||||
self.type_hints = (
|
||||
type_hints
|
||||
if type_hints is not None
|
||||
else get_type_hints(schema, localns={schema.__name__: schema})
|
||||
else get_type_hints(
|
||||
schema, localns={schema.__name__: schema}, include_extras=True
|
||||
)
|
||||
)
|
||||
|
||||
if issubclass(schema, BaseModelV1):
|
||||
@@ -70,6 +74,17 @@ class SchemaCoercionMapper:
|
||||
for n, f in schema.__fields__.items()
|
||||
}
|
||||
self._construct = schema.construct
|
||||
unhandled_attrs = (
|
||||
"__pre_root_validators__",
|
||||
"__post_root_validators__",
|
||||
"__validators__",
|
||||
)
|
||||
if any(getattr(schema, c, None) for c in unhandled_attrs):
|
||||
self.coerce: Callable[[Any, Any], Union[BaseModelV1, BaseModel]] = (
|
||||
lambda v, _: schema(**v)
|
||||
)
|
||||
else:
|
||||
self.coerce = self._coerce
|
||||
|
||||
elif issubclass(schema, BaseModel):
|
||||
self._fields = {
|
||||
@@ -77,6 +92,13 @@ class SchemaCoercionMapper:
|
||||
for n, f in schema.model_fields.items()
|
||||
}
|
||||
self._construct: Callable[..., Any] = schema.model_construct # type: ignore
|
||||
unhandled_attrs = ("validators", "field_validators", "root_validators")
|
||||
if (decorators := getattr(schema, "__pydantic_decorators__", None)) and any(
|
||||
getattr(decorators, attr, None) for attr in unhandled_attrs
|
||||
):
|
||||
self.coerce = lambda v, _: schema.model_validate(v)
|
||||
else:
|
||||
self.coerce = self._coerce
|
||||
|
||||
else:
|
||||
raise TypeError("Schema is neither a Pydantic v1 nor v2 model.")
|
||||
@@ -86,7 +108,7 @@ class SchemaCoercionMapper:
|
||||
def __call__(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
return self.coerce(input_data, depth)
|
||||
|
||||
def coerce(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
def _coerce(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
if depth is None:
|
||||
depth = self.max_depth
|
||||
if not isinstance(input_data, dict) or depth <= 0:
|
||||
@@ -109,15 +131,38 @@ class SchemaCoercionMapper:
|
||||
if depth == 0:
|
||||
return self._passthrough
|
||||
|
||||
field_type, metadata = self._unwrap_annotated(field_type)
|
||||
origin = get_origin(field_type)
|
||||
|
||||
if (field_type in _IDENTITY_TYPES) or (origin in _IDENTITY_TYPES):
|
||||
return self._passthrough
|
||||
|
||||
if origin is Annotated:
|
||||
real_type, *_ = get_args(field_type)
|
||||
sub = self._build_coercer(real_type, depth - 1)
|
||||
return lambda v, d: sub(v, d)
|
||||
if isinstance(field_type, TypeVar):
|
||||
concrete = self.type_hints.get(field_type) # type: ignore
|
||||
if concrete is not None:
|
||||
return self._build_coercer(concrete, depth - 1)
|
||||
return self._passthrough
|
||||
|
||||
if hasattr(field_type, "__parameters__") and hasattr(
|
||||
field_type, "model_fields"
|
||||
):
|
||||
try:
|
||||
type_hints = self.resolve_concrete_type_hints(field_type)
|
||||
|
||||
def generic_model_coercer(v: Any, d: int) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError(
|
||||
f"Expected dict for {field_type}, got {type(v)}"
|
||||
)
|
||||
return v
|
||||
mapper = SchemaCoercionMapper(field_type, type_hints, max_depth=d)
|
||||
return mapper.coerce(v, d)
|
||||
|
||||
return generic_model_coercer
|
||||
except Exception as e:
|
||||
logger.debug(f"Generic type resolution failed: {e}")
|
||||
return self._passthrough
|
||||
|
||||
if isclass(field_type):
|
||||
# This is needed bcs. of issubclass issues on older versions of python
|
||||
@@ -162,6 +207,7 @@ class SchemaCoercionMapper:
|
||||
return {sub(x, d - 1) for x in v}
|
||||
|
||||
return set_coercer
|
||||
|
||||
if origin is dict or field_type is dict:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 2:
|
||||
@@ -200,27 +246,75 @@ class SchemaCoercionMapper:
|
||||
)
|
||||
|
||||
if origin is Union:
|
||||
uargs = get_args(field_type)
|
||||
subs, none_in_union = [], False
|
||||
for ix, arg in enumerate(uargs):
|
||||
args = get_args(field_type)
|
||||
discriminator_key = self._extract_discriminator_key(metadata)
|
||||
none_in_union = False
|
||||
discriminator_map = {}
|
||||
|
||||
for arg in args:
|
||||
if arg is type(None):
|
||||
none_in_union = True
|
||||
else:
|
||||
subs.append(
|
||||
self._build_coercer(arg, depth - 1, throw=ix < len(uargs) - 1)
|
||||
)
|
||||
continue
|
||||
base_type = arg
|
||||
if get_origin(arg) is Annotated:
|
||||
base_type, _ = get_args(arg)[0], get_args(arg)[1:]
|
||||
try:
|
||||
hint = get_type_hints(base_type)
|
||||
lit = hint.get(discriminator_key)
|
||||
if get_origin(lit) is Literal:
|
||||
for val in get_args(lit):
|
||||
discriminator_map[val] = base_type
|
||||
except Exception as e:
|
||||
if throw:
|
||||
raise e
|
||||
else:
|
||||
logger.debug(f"Failed to extract discriminator: {e}")
|
||||
|
||||
def union_coercer(v: Any, d: Any) -> Any:
|
||||
if v is None and none_in_union:
|
||||
return None
|
||||
err = None
|
||||
for sp in subs:
|
||||
|
||||
tag = None
|
||||
if callable(discriminator_key):
|
||||
try:
|
||||
return sp(v, d - 1)
|
||||
except TypeError as e:
|
||||
err = e
|
||||
if err:
|
||||
raise err
|
||||
tag = discriminator_key(v)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to call discriminator func: {e}")
|
||||
elif (
|
||||
isinstance(v, dict)
|
||||
and isinstance(discriminator_key, str)
|
||||
and discriminator_key in v
|
||||
):
|
||||
tag = v[discriminator_key]
|
||||
|
||||
if tag is not None:
|
||||
for arg in args:
|
||||
base_type = arg
|
||||
if get_origin(arg) is Annotated:
|
||||
base_type, _ = get_args(arg)[0], get_args(arg)[1:]
|
||||
|
||||
try:
|
||||
if issubclass(base_type, (BaseModel, BaseModelV1)):
|
||||
return SchemaCoercionMapper(
|
||||
base_type, max_depth=d
|
||||
).coerce(v, d)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"Coercion with {base_type} failed for tag={tag}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
# fallback: try coercing each branch
|
||||
for arg in args:
|
||||
try:
|
||||
sub = self._build_coercer(arg, d - 1)
|
||||
return sub(v, d - 1)
|
||||
except Exception as e:
|
||||
if throw:
|
||||
raise e
|
||||
else:
|
||||
logger.debug(f"Fallback coercion failed for arg={arg}: {e}")
|
||||
|
||||
return v
|
||||
|
||||
return union_coercer
|
||||
@@ -232,10 +326,58 @@ class SchemaCoercionMapper:
|
||||
def _passthrough(v: Any, _d: Any) -> Any: # noqa: D401
|
||||
return v
|
||||
|
||||
@staticmethod
|
||||
def _extract_discriminator_key(meta: list[Any]) -> str | Callable[[Any], Hashable]:
|
||||
"""Extract discriminator field name or function from Annotated metadata"""
|
||||
for m in meta:
|
||||
if isinstance(m, FieldInfo):
|
||||
disc = getattr(m, "discriminator", None)
|
||||
if isinstance(disc, Discriminator):
|
||||
return disc.discriminator
|
||||
elif isinstance(disc, str):
|
||||
return disc
|
||||
return "type"
|
||||
|
||||
@staticmethod
|
||||
def _unwrap_annotated(tp: Any) -> tuple[Any, list[Any]]:
|
||||
"""Unwrap nested Annotated types, extracting the base type and all metadata"""
|
||||
metadata = []
|
||||
while get_origin(tp) is Annotated:
|
||||
tp, *meta = get_args(tp)
|
||||
metadata.extend(meta)
|
||||
return tp, metadata
|
||||
|
||||
@staticmethod
|
||||
def resolve_concrete_type_hints(generic_model_type: Any) -> dict[Any, Any]:
|
||||
"""Resolve concrete type hints in a generic model"""
|
||||
origin = get_origin(generic_model_type)
|
||||
args = get_args(generic_model_type)
|
||||
param_names = getattr(origin, "__parameters__", [])
|
||||
|
||||
if not args or not param_names:
|
||||
return {}
|
||||
|
||||
type_map = dict(zip(param_names, args))
|
||||
result = {}
|
||||
|
||||
for field_name, model_field in origin.model_fields.items():
|
||||
anno = model_field.annotation
|
||||
if get_origin(anno) is Annotated:
|
||||
base, *meta = get_args(anno)
|
||||
if isinstance(base, TypeVar) and base in type_map:
|
||||
result[field_name] = Annotated[type_map[base], *meta]
|
||||
else:
|
||||
result[field_name] = anno
|
||||
elif isinstance(anno, TypeVar) and anno in type_map:
|
||||
result[field_name] = type_map[anno]
|
||||
else:
|
||||
result[field_name] = anno
|
||||
|
||||
return result
|
||||
|
||||
|
||||
_adapter_cache: dict[Any, Callable[[Any], Any]] = {}
|
||||
|
||||
|
||||
_IDENTITY_TYPES: tuple[type[Any], ...] = (
|
||||
int,
|
||||
float,
|
||||
|
||||
@@ -1339,22 +1339,26 @@ def test_pending_writes_resume(
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"]
|
||||
if checkpoint_during
|
||||
else AnyStr(),
|
||||
"checkpoint_id": (
|
||||
checkpoints[2].config["configurable"]["checkpoint_id"]
|
||||
if checkpoint_during
|
||||
else AnyStr()
|
||||
),
|
||||
}
|
||||
},
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
(AnyStr(), "value", 3),
|
||||
)
|
||||
if checkpoint_during
|
||||
else UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
# the write against the previous checkpoint is not saved, as it is
|
||||
# produced in a run where only the next checkpoint (the last) is saved
|
||||
pending_writes=(
|
||||
UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
(AnyStr(), "value", 3),
|
||||
)
|
||||
if checkpoint_during
|
||||
else UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
# the write against the previous checkpoint is not saved, as it is
|
||||
# produced in a run where only the next checkpoint (the last) is saved
|
||||
)
|
||||
),
|
||||
)
|
||||
if not checkpoint_during:
|
||||
@@ -3119,8 +3123,10 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
from pydantic import ( # type: ignore
|
||||
BaseModel,
|
||||
ByteSize,
|
||||
Discriminator,
|
||||
Field,
|
||||
SecretStr,
|
||||
Tag,
|
||||
confloat,
|
||||
conint,
|
||||
conlist,
|
||||
@@ -3169,6 +3175,16 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
conlist_type = conlist(item_type=int, min_length=2, max_length=5)
|
||||
else:
|
||||
conlist_type = conlist(item_type=int, min_items=2, max_items=5)
|
||||
if version == "v2":
|
||||
FuncDiscriminatorPet = Annotated[
|
||||
Union[
|
||||
Annotated[Dog, Tag(tag="dog")],
|
||||
Annotated[Cat, Tag(tag="cat")],
|
||||
],
|
||||
Field(discriminator=Discriminator(lambda obj: obj.get("pet_type"))),
|
||||
]
|
||||
else:
|
||||
FuncDiscriminatorPet = Union[Dog, Cat]
|
||||
|
||||
class State(BaseModel):
|
||||
# Basic nested model tests
|
||||
@@ -3208,6 +3224,7 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
pattern: re.Pattern
|
||||
secret: SecretStr
|
||||
file_size: ByteSize
|
||||
discriminated_pet: FuncDiscriminatorPet
|
||||
|
||||
# Constrained types
|
||||
positive_value: PositiveInt
|
||||
@@ -3279,6 +3296,7 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
"pattern": "^test$",
|
||||
"secret": "password123",
|
||||
"file_size": 1024,
|
||||
"discriminated_pet": {"pet_type": "cat", "meow": "indubitably"},
|
||||
# Constrained types
|
||||
"positive_value": 42,
|
||||
"non_negative": 0.0,
|
||||
@@ -3355,6 +3373,70 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
assert {**new_inputs, **update} == graph.invoke(new_inputs.copy())
|
||||
|
||||
|
||||
def test_pydantic_state_field_validator():
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
|
||||
class State(BaseModel):
|
||||
name: str
|
||||
text: str = ""
|
||||
only_root: int = 13
|
||||
|
||||
@field_validator("name", mode="after")
|
||||
@classmethod
|
||||
def validate_name(cls, value):
|
||||
if value[0].islower():
|
||||
raise ValueError("Name must start with a capital letter")
|
||||
return "Validated " + value
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_amodel(cls, values: "State"):
|
||||
return values | {"only_root": 392}
|
||||
|
||||
input_state = {"name": "John"}
|
||||
|
||||
def process_node(state: State):
|
||||
assert State.model_validate(input_state) == state
|
||||
return {"text": "Hello, " + state.name + "!"}
|
||||
|
||||
builder = StateGraph(state_schema=State)
|
||||
builder.add_node("process", process_node)
|
||||
builder.add_edge(START, "process")
|
||||
builder.add_edge("process", END)
|
||||
g = builder.compile()
|
||||
res = g.invoke(input_state)
|
||||
assert res["text"] == "Hello, Validated John!"
|
||||
|
||||
|
||||
def test_pydantic_v1_state_root_validator():
|
||||
from pydantic.v1 import BaseModel, root_validator
|
||||
|
||||
class State(BaseModel):
|
||||
name: str
|
||||
text: str = ""
|
||||
only_root: int = 13
|
||||
|
||||
@root_validator(pre=True)
|
||||
@classmethod
|
||||
def validate(cls, values: dict):
|
||||
values["name"] = "Validated " + values["name"]
|
||||
return values | {"only_root": 396}
|
||||
|
||||
input_state = {"name": "John"}
|
||||
|
||||
def process_node(state: State):
|
||||
assert State(**input_state) == state
|
||||
return {"text": "Hello, " + state.name + "!"}
|
||||
|
||||
builder = StateGraph(state_schema=State)
|
||||
builder.add_node("process", process_node)
|
||||
builder.add_edge(START, "process")
|
||||
builder.add_edge("process", END)
|
||||
g = builder.compile()
|
||||
res = g.invoke(input_state)
|
||||
assert res["text"] == "Hello, Validated John!"
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
from typing import Dict, Generic, List, Literal, Optional, Set, Tuple, TypeVar, Union
|
||||
|
||||
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langgraph.graph.schema_utils import SchemaCoercionMapper
|
||||
|
||||
|
||||
def test_any_message():
|
||||
class MyMessage(BaseModel):
|
||||
msg: List[AnyMessage]
|
||||
|
||||
data = {
|
||||
"msg": [
|
||||
{"type": "human", "content": "Hello"},
|
||||
{"type": "ai", "content": "Hi there!"},
|
||||
]
|
||||
}
|
||||
|
||||
MyMessage.model_validate(data)
|
||||
|
||||
mapper = SchemaCoercionMapper(MyMessage)
|
||||
result = mapper(data)
|
||||
assert isinstance(result, MyMessage)
|
||||
assert isinstance(result.msg, list)
|
||||
assert len(result.msg) == 2
|
||||
assert isinstance(result.msg[0], (HumanMessage))
|
||||
assert isinstance(result.msg[1], (AIMessage))
|
||||
|
||||
|
||||
# ==== 基础模型 ====
|
||||
class SimpleModel(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
def test_simple_model():
|
||||
data = {"name": "Alice", "age": 30}
|
||||
mapper = SchemaCoercionMapper(SimpleModel)
|
||||
result = mapper(data)
|
||||
assert isinstance(result, SimpleModel)
|
||||
assert result.name == "Alice"
|
||||
assert result.age == 30
|
||||
|
||||
|
||||
# ==== 容器类型 ====
|
||||
class ContainerModel(BaseModel):
|
||||
items: List[int]
|
||||
mapping: Dict[str, float]
|
||||
tags: Set[str]
|
||||
coords: Tuple[int, int]
|
||||
|
||||
|
||||
def test_container_model():
|
||||
data = {
|
||||
"items": [1, 2, 3],
|
||||
"mapping": {"a": 1.1},
|
||||
"tags": ["x", "y"],
|
||||
"coords": [10, 20],
|
||||
}
|
||||
mapper = SchemaCoercionMapper(ContainerModel)
|
||||
result = mapper(data)
|
||||
assert isinstance(result.items, list)
|
||||
assert isinstance(result.mapping, dict)
|
||||
assert isinstance(result.tags, set)
|
||||
assert isinstance(result.coords, tuple)
|
||||
|
||||
|
||||
# ==== 泛型 ====
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Wrapper(BaseModel, Generic[T]):
|
||||
value: T
|
||||
|
||||
|
||||
def test_generic_model():
|
||||
class IntWrapper(Wrapper[int]):
|
||||
pass
|
||||
|
||||
data = {"value": 123}
|
||||
mapper = SchemaCoercionMapper(IntWrapper)
|
||||
result = mapper(data)
|
||||
assert result.value == 123
|
||||
|
||||
|
||||
# ==== Union 类型 ====
|
||||
class Dog(BaseModel):
|
||||
type: Literal["dog"]
|
||||
age: int
|
||||
|
||||
|
||||
class Cat(BaseModel):
|
||||
type: Literal["cat"]
|
||||
name: str
|
||||
|
||||
|
||||
Pet = Union[Dog, Cat]
|
||||
|
||||
|
||||
class Owner(BaseModel):
|
||||
pet: Pet
|
||||
|
||||
|
||||
def test_union_type():
|
||||
data = {"pet": {"type": "dog", "age": 5}}
|
||||
mapper = SchemaCoercionMapper(Owner)
|
||||
result = mapper(data)
|
||||
assert isinstance(result.pet, Dog)
|
||||
|
||||
|
||||
# ==== Annotated + Tag + discriminator ====
|
||||
TaggedPet = Annotated[
|
||||
Union[
|
||||
Annotated[Dog, Tag(tag="dog")],
|
||||
Annotated[Cat, Tag(tag="cat")],
|
||||
],
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
|
||||
class TaggedOwner(BaseModel):
|
||||
pet: TaggedPet
|
||||
|
||||
|
||||
def test_tagged_union():
|
||||
data = {"pet": {"type": "cat", "name": "Mimi"}}
|
||||
mapper = SchemaCoercionMapper(TaggedOwner)
|
||||
result = mapper(data)
|
||||
assert isinstance(result.pet, Cat)
|
||||
|
||||
|
||||
# ==== Annotated + Field(discriminator=Discriminator(func)) ====
|
||||
def _get_type(obj):
|
||||
return obj.get("type")
|
||||
|
||||
|
||||
FuncDiscriminatorPet = Annotated[
|
||||
Union[
|
||||
Annotated[Dog, Tag(tag="dog")],
|
||||
Annotated[Cat, Tag(tag="cat")],
|
||||
],
|
||||
Field(discriminator=Discriminator(_get_type)),
|
||||
]
|
||||
|
||||
|
||||
class FuncOwner(BaseModel):
|
||||
pet: FuncDiscriminatorPet
|
||||
|
||||
|
||||
def test_func_discriminator():
|
||||
data = {"pet": {"type": "dog", "age": 9}}
|
||||
mapper = SchemaCoercionMapper(FuncOwner)
|
||||
result = mapper(data)
|
||||
assert isinstance(result.pet, Dog)
|
||||
|
||||
|
||||
# ==== Optional + 泛型 + 多态嵌套 ====
|
||||
class Box(BaseModel, Generic[T]):
|
||||
content: Optional[T]
|
||||
|
||||
|
||||
class Crate(BaseModel, Generic[T]):
|
||||
payload: Box[T]
|
||||
|
||||
|
||||
class Zoo(BaseModel):
|
||||
animal: Box[TaggedPet]
|
||||
|
||||
|
||||
class Warehouse(BaseModel):
|
||||
cage: Crate[TaggedPet]
|
||||
|
||||
|
||||
def test_nested_optional_generic_union():
|
||||
# Box[TaggedPet]
|
||||
data1 = {"animal": {"content": {"type": "cat", "name": "Kitty"}}}
|
||||
mapper1 = SchemaCoercionMapper(Zoo)
|
||||
result1 = mapper1(data1)
|
||||
assert isinstance(result1.animal.content, Cat)
|
||||
|
||||
# Crate[TaggedPet]
|
||||
data2 = {"cage": {"payload": {"content": {"type": "dog", "age": 8}}}}
|
||||
mapper2 = SchemaCoercionMapper(Warehouse)
|
||||
result2 = mapper2(data2)
|
||||
assert isinstance(result2.cage.payload.content, Dog)
|
||||
|
||||
# Optional None
|
||||
data3 = {"animal": {"content": None}}
|
||||
result3 = mapper1(data3)
|
||||
assert result3.animal.content is None
|
||||
|
||||
# deeply nested Optional
|
||||
data4 = {"cage": {"payload": {"content": None}}}
|
||||
result4 = mapper2(data4)
|
||||
assert result4.cage.payload.content is None
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.67",
|
||||
"version": "0.0.70",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
+27
-17
@@ -1,42 +1,41 @@
|
||||
import {
|
||||
Assistant,
|
||||
AssistantGraph,
|
||||
AssistantVersion,
|
||||
CancelAction,
|
||||
Checkpoint,
|
||||
Config,
|
||||
Cron,
|
||||
CronCreateForThreadResponse,
|
||||
CronCreateResponse,
|
||||
DefaultValues,
|
||||
GraphSchema,
|
||||
Item,
|
||||
ListNamespaceResponse,
|
||||
Metadata,
|
||||
Run,
|
||||
RunStatus,
|
||||
SearchItemsResponse,
|
||||
Subgraphs,
|
||||
Thread,
|
||||
ThreadState,
|
||||
Cron,
|
||||
AssistantVersion,
|
||||
Subgraphs,
|
||||
Checkpoint,
|
||||
SearchItemsResponse,
|
||||
ListNamespaceResponse,
|
||||
Item,
|
||||
ThreadStatus,
|
||||
CronCreateResponse,
|
||||
CronCreateForThreadResponse,
|
||||
} from "./schema.js";
|
||||
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js";
|
||||
import { IterableReadableStream } from "./utils/stream.js";
|
||||
import type {
|
||||
Command,
|
||||
CronsCreatePayload,
|
||||
OnConflictBehavior,
|
||||
RunsCreatePayload,
|
||||
RunsStreamPayload,
|
||||
RunsWaitPayload,
|
||||
StreamEvent,
|
||||
CronsCreatePayload,
|
||||
OnConflictBehavior,
|
||||
Command,
|
||||
} from "./types.js";
|
||||
import { mergeSignals } from "./utils/signals.js";
|
||||
import type { StreamMode, TypedAsyncGenerator } from "./types.stream.js";
|
||||
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js";
|
||||
import { getEnvironmentVariable } from "./utils/env.js";
|
||||
import { _getFetchImplementation } from "./singletons/fetch.js";
|
||||
import type { TypedAsyncGenerator, StreamMode } from "./types.stream.js";
|
||||
import { mergeSignals } from "./utils/signals.js";
|
||||
import { BytesLineDecoder, SSEDecoder } from "./utils/sse.js";
|
||||
import { IterableReadableStream } from "./utils/stream.js";
|
||||
/**
|
||||
* Get the API key from the environment.
|
||||
* Precedence:
|
||||
@@ -619,6 +618,15 @@ export class ThreadsClient<
|
||||
* Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
*/
|
||||
status?: ThreadStatus;
|
||||
/**
|
||||
* Sort by.
|
||||
*/
|
||||
sortBy?: "thread_id" | "status" | "created_at" | "updated_at";
|
||||
/**
|
||||
* Sort order.
|
||||
* Must be one of 'asc' or 'desc'.
|
||||
*/
|
||||
sortOrder?: "asc" | "desc";
|
||||
}): Promise<Thread<ValuesType>[]> {
|
||||
return this.fetch<Thread<ValuesType>[]>("/threads/search", {
|
||||
method: "POST",
|
||||
@@ -627,6 +635,8 @@ export class ThreadsClient<
|
||||
limit: query?.limit ?? 10,
|
||||
offset: query?.offset ?? 0,
|
||||
status: query?.status,
|
||||
sort_by: query?.sortBy,
|
||||
sort_order: query?.sortOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,7 +175,11 @@ export function LoadExternalComponent({
|
||||
}, [uiClient, uiNamespace, message.name, shadowRootId, hasClientComponent]);
|
||||
|
||||
if (hasClientComponent) {
|
||||
return React.createElement(clientComponent, message.props);
|
||||
return (
|
||||
<UseStreamContext.Provider value={{ stream, meta }}>
|
||||
{React.createElement(clientComponent, message.props)}
|
||||
</UseStreamContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1043,6 +1043,10 @@ class ThreadsClient:
|
||||
status: Optional[ThreadStatus] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
sort_by: Optional[
|
||||
Literal["thread_id", "status", "created_at", "updated_at"]
|
||||
] = None,
|
||||
sort_order: Optional[Literal["asc", "desc"]] = None,
|
||||
headers: Optional[dict[str, str]] = None,
|
||||
) -> list[Thread]:
|
||||
"""Search for threads.
|
||||
@@ -1054,6 +1058,8 @@ class ThreadsClient:
|
||||
Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
limit: Limit on number of threads to return.
|
||||
offset: Offset in threads table to start search from.
|
||||
sort_by: Sort by field.
|
||||
sort_order: Sort order.
|
||||
headers: Optional custom headers to include with the request.
|
||||
|
||||
Returns:
|
||||
@@ -1079,6 +1085,10 @@ class ThreadsClient:
|
||||
payload["values"] = values
|
||||
if status:
|
||||
payload["status"] = status
|
||||
if sort_by:
|
||||
payload["sort_by"] = sort_by
|
||||
if sort_order:
|
||||
payload["sort_order"] = sort_order
|
||||
return await self.http.post(
|
||||
"/threads/search",
|
||||
json=payload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.61"
|
||||
version = "0.1.63"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user