Compare commits

...
12 Commits
Author SHA1 Message Date
William FHandGitHub a5f5d0c4df Expose --tunnel flag to dev command (#4370)
Signed-off-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2025-04-22 14:23:09 +00:00
lc-arjunandGitHub 7486adabdf fix: threads search sorting defaults (#4365)
Removes default from https://github.com/langchain-ai/langgraph/pull/4362
2025-04-21 20:49:16 -04:00
William FHandGitHub 12ad47e4e8 Use model_validate if needed (#4363)
If the state schema uses validators, skip the model construct
optimization.

For context, pydantic state can be significantly slower to run than
typed dict and dataclass states due to the full recursive validation.

We have some optimizations to reduce the impact of this (using cached
validators with model_construct), but this doesn't handle things like
field_validator.

We prefer correctness over performance, obviously.

Resolves: https://github.com/langchain-ai/langgraph/issues/4074

Signed-off-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2025-04-21 21:53:58 +00:00
lc-arjunandGitHub 90f7f776cf feat: threads sorting sdk spec (#4362) 2025-04-21 14:42:43 -07:00
William FHandGitHub c7306f7aed Add log json env var (#4348) 2025-04-18 20:44:50 +00:00
William FHandGitHub 20bd71e289 Bump lockfile (#4346) 2025-04-18 08:43:58 -07:00
William Fu-Hinthorn 283485753f Format notebook 2025-04-18 08:35:31 -07:00
ba7f9975fa Fix text fields naming (#4345)
The configuration expects the key "fields", not "text_fields": I had
failed to update across all implementations in the original PR

Thank you to Vincent Min for the fix!
---------

Co-authored-by: Vincent Min <93780551+VMinB12@users.noreply.github.com>
2025-04-18 08:21:46 -07:00
David DuongandGitHub 8c4904bee9 fix(sdk-js): make sure to wrap client component in UseStreamContext (#4338) 2025-04-18 01:08:38 +02:00
Tat Dat Duong 6bb06b8702 fix(sdk-js): make sure to wrap client component in UseStreamContext 2025-04-18 01:07:13 +02:00
Vadym BardaandGitHub 7a16e33833 docs: fix notebook runner (#4337) 2025-04-17 22:43:41 +00:00
Vadym BardaandGitHub 3da5c73a04 checkpoint-postgres: release 2.0.20 (#4335) 2025-04-17 17:08:18 -04:00
19 changed files with 749 additions and 450 deletions
+21 -2
View File
@@ -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(
+8
View File
@@ -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`.
+1 -7
View File
@@ -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()))"
]
},
{
+6 -8
View File
@@ -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"
+1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.19"
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(
+1 -1
View File
@@ -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:
+10
View File
@@ -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,
)
+551 -392
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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 }
+19 -1
View File
@@ -70,6 +70,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 +88,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 +104,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:
+82 -14
View File
@@ -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:
@@ -3355,6 +3359,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
+1 -1
View File
@@ -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
View File
@@ -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,
},
});
}
+5 -1
View File
@@ -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 (
+10
View File
@@ -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 -1
View File
@@ -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"