Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn 823ed55849 Add generics tests
Signed-off-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2025-04-21 15:05:19 -07: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
17 changed files with 773 additions and 453 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.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(
+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:
+547 -388
View File
File diff suppressed because it is too large Load Diff
+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:
+126 -23
View File
@@ -24,12 +24,14 @@ from typing import (
Any,
Dict,
Generator,
Generic,
Iterator,
List,
Literal,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
get_type_hints,
)
@@ -37,11 +39,7 @@ from typing import (
import httpx
import pytest
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.runnables import (
RunnableConfig,
RunnableLambda,
RunnablePassthrough,
)
from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough
from langchain_core.runnables.graph import Edge
from langsmith import traceable
from pytest_mock import MockerFixture
@@ -1339,22 +1337,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:
@@ -3105,10 +3107,10 @@ def test_nested_pydantic_models(version: str) -> None:
# Import necessary modules
if version == "v1":
from pydantic.v1 import ( # type: ignore
from pydantic.v1 import (
BaseModel,
ByteSize,
Field,
Field, # type: ignore
SecretStr,
confloat,
conint,
@@ -3116,10 +3118,10 @@ def test_nested_pydantic_models(version: str) -> None:
constr,
)
else:
from pydantic import ( # type: ignore
from pydantic import (
BaseModel,
ByteSize,
Field,
Field, # type: ignore
SecretStr,
confloat,
conint,
@@ -3355,6 +3357,107 @@ 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!"
def test_pydantic_generics():
from pydantic import BaseModel
class A(BaseModel):
a: str
class B(BaseModel):
b: str
AorB = TypeVar("AorB", A, B)
class C(BaseModel, Generic[AorB]):
c: AorB
class State(BaseModel):
text: str
count: int
c: C[A]
input_state = {"text": "1", "count": 0, "c": {"c": {"a": "1"}}}
expected_input = State.model_validate(input_state)
def process_node(state: State):
assert state == expected_input
new_text = ", the type of c is " + str(type(state.c.c))
return {"text": state.text + new_text, "count": state.count + 1}
builder = StateGraph(State)
builder.add_node("process", process_node)
builder.add_edge(START, "process")
g = builder.compile()
g.invoke(input_state)
g.invoke(expected_input)
@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.69",
"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?: "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 (
+6
View File
@@ -1043,6 +1043,8 @@ class ThreadsClient:
status: Optional[ThreadStatus] = None,
limit: int = 10,
offset: int = 0,
sort_by: Literal["id", "status", "created_at", "updated_at"] = "created_at",
sort_order: Literal["asc", "desc"] = "asc",
headers: Optional[dict[str, str]] = None,
) -> list[Thread]:
"""Search for threads.
@@ -1079,6 +1081,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.62"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"