diff --git a/.gitignore b/.gitignore index 7cf97c0d0..cc960f9fa 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,7 @@ __pypackages__/ # Environments .env +.env.* .envrc *.crt *.key diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py index 100e1e2d3..c407f26f1 100644 --- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py @@ -7,7 +7,7 @@ import logging import re import threading from collections import defaultdict -from collections.abc import Callable, Iterable, Iterator, Sequence +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from contextlib import contextmanager from datetime import datetime from typing import ( @@ -354,7 +354,7 @@ class BasePostgresStore(Generic[C]): ( _namespace_to_text(op.namespace), op.key, - Jsonb(cast(dict, op.value)), + Jsonb(dict(cast(Mapping[str, Any], op.value))), ) ) if op.ttl is not None: diff --git a/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py b/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py index 70d4481cd..fbc23beda 100644 --- a/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py +++ b/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py @@ -7,7 +7,7 @@ import re import sqlite3 import threading from collections import defaultdict -from collections.abc import Callable, Iterable, Iterator, Sequence +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from contextlib import contextmanager from typing import Any, Literal, NamedTuple, cast @@ -387,7 +387,7 @@ class BaseSqliteStore: [ _namespace_to_text(op.namespace), op.key, - orjson.dumps(cast(dict, op.value)), + orjson.dumps(dict(cast(Mapping[str, Any], op.value))), expires_at, op.ttl, ] diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py index 08fe74525..727da8987 100644 --- a/libs/checkpoint/langgraph/store/base/__init__.py +++ b/libs/checkpoint/langgraph/store/base/__init__.py @@ -12,7 +12,7 @@ Core types: from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from datetime import datetime from typing import ( Any, @@ -473,10 +473,10 @@ class PutOp(NamedTuple): the full path would effectively be `"documents/user123/report1"` """ - value: dict[str, Any] | None + value: Mapping[str, Any] | None """The data to store, or `None` to mark the item for deletion. - The value must be a dictionary with string keys and JSON-serializable values. + The value must be a mapping with string keys and JSON-serializable values. Setting this to `None` signals that the item should be deleted. Example: @@ -857,7 +857,7 @@ class BaseStore(ABC): self, namespace: tuple[str, ...], key: str, - value: dict[str, Any], + value: Mapping[str, Any], index: Literal[False] | list[str] | None = None, *, ttl: float | None | NotProvided = NOT_PROVIDED, @@ -869,7 +869,7 @@ class BaseStore(ABC): Example: `("documents", "user123")` key: Unique identifier within the namespace. Together with namespace forms the complete path to the item. - value: Dictionary containing the item's data. Must contain string keys + value: Mapping containing the item's data. Must contain string keys and JSON-serializable values. index: Controls how the item's fields are indexed for search: @@ -1110,7 +1110,7 @@ class BaseStore(ABC): self, namespace: tuple[str, ...], key: str, - value: dict[str, Any], + value: Mapping[str, Any], index: Literal[False] | list[str] | None = None, *, ttl: float | None | NotProvided = NOT_PROVIDED, @@ -1122,7 +1122,7 @@ class BaseStore(ABC): Example: `("documents", "user123")` key: Unique identifier within the namespace. Together with namespace forms the complete path to the item. - value: Dictionary containing the item's data. Must contain string keys + value: Mapping containing the item's data. Must contain string keys and JSON-serializable values. index: Controls how the item's fields are indexed for search: diff --git a/libs/checkpoint/langgraph/store/base/batch.py b/libs/checkpoint/langgraph/store/base/batch.py index 64019d68f..9deb02e2a 100644 --- a/libs/checkpoint/langgraph/store/base/batch.py +++ b/libs/checkpoint/langgraph/store/base/batch.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio import functools import weakref -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Mapping from typing import Any, Literal, TypeVar from langgraph.store.base import ( @@ -132,7 +132,7 @@ class AsyncBatchedBaseStore(BaseStore): self, namespace: tuple[str, ...], key: str, - value: dict[str, Any], + value: Mapping[str, Any], index: Literal[False] | list[str] | None = None, *, ttl: float | None | NotProvided = NOT_PROVIDED, @@ -231,7 +231,7 @@ class AsyncBatchedBaseStore(BaseStore): self, namespace: tuple[str, ...], key: str, - value: dict[str, Any], + value: Mapping[str, Any], index: Literal[False] | list[str] | None = None, *, ttl: float | None | NotProvided = NOT_PROVIDED, diff --git a/libs/checkpoint/langgraph/store/base/embed.py b/libs/checkpoint/langgraph/store/base/embed.py index 4255886e2..98baac865 100644 --- a/libs/checkpoint/langgraph/store/base/embed.py +++ b/libs/checkpoint/langgraph/store/base/embed.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio import functools import json -from collections.abc import Awaitable, Callable, Sequence +from collections.abc import Awaitable, Callable, Mapping, Sequence from typing import Any from langchain_core.embeddings import Embeddings @@ -244,6 +244,9 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]: - Multi-field selection: "{field1,field2}" - Nested paths in multi-field: "{field1,nested.field2}" """ + if isinstance(obj, Mapping) and not isinstance(obj, dict): + obj = dict(obj) + if not path or path == "$": return [json.dumps(obj, sort_keys=True, ensure_ascii=False)] diff --git a/libs/checkpoint/langgraph/store/memory/__init__.py b/libs/checkpoint/langgraph/store/memory/__init__.py index b156c457d..6ac8a392a 100644 --- a/libs/checkpoint/langgraph/store/memory/__init__.py +++ b/libs/checkpoint/langgraph/store/memory/__init__.py @@ -408,7 +408,7 @@ class InMemoryStore(BaseStore): self._vectors[namespace].pop(key, None) else: self._data[namespace][key] = Item( - value=op.value, + value=dict(op.value), key=key, namespace=namespace, created_at=datetime.now(timezone.utc), diff --git a/libs/checkpoint/tests/test_store.py b/libs/checkpoint/tests/test_store.py index 42e7a7697..9079aeecc 100644 --- a/libs/checkpoint/tests/test_store.py +++ b/libs/checkpoint/tests/test_store.py @@ -1,7 +1,9 @@ import asyncio import json -from collections.abc import Iterable +from collections import UserDict +from collections.abc import Iterable, Mapping from datetime import datetime +from types import MappingProxyType from typing import Any import pytest @@ -137,6 +139,18 @@ def test_get_text_at_path() -> None: assert get_text_at_path(nested_data, "nested[{invalid}]") == [] +@pytest.mark.parametrize( + "mapping", + [ + UserDict({"text": "searchable"}), + MappingProxyType({"text": "searchable"}), + ], +) +def test_get_text_at_path_with_non_dict_mapping(mapping: Mapping[str, str]) -> None: + assert get_text_at_path(mapping, "$") == ['{"text": "searchable"}'] + assert get_text_at_path(mapping, "text") == ["searchable"] + + async def test_async_batch_store(mocker: MockerFixture) -> None: abatch = mocker.stub()