langgraph-store-firestore 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ """Google Firestore implementation of the LangGraph long-term-memory ``BaseStore``.
2
+
3
+ ``FirestoreStore`` persists namespaced key/value memory in a single collection and
4
+ supports get / put / delete / search (namespace-prefix + filter) and
5
+ ``list_namespaces``. With an ``IndexConfig`` it also does **semantic search**
6
+ natively via Firestore vector search (``find_nearest``, cosine). Built on
7
+ ``langgraph-store-core``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import List, Optional, Tuple
14
+ from urllib.parse import quote
15
+
16
+ from google.cloud import firestore
17
+ from google.cloud.firestore_v1.base_query import FieldFilter
18
+ from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
19
+ from google.cloud.firestore_v1.vector import Vector
20
+
21
+ from langgraph_store_core import IndexConfig, KVStore
22
+
23
+ __all__ = ["FirestoreStore"]
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _FIELDS = ("prefix", "key", "value", "created_at", "updated_at")
28
+ _PREFIX_HIGH = chr(0xF8FF) # exclusive upper bound for a Firestore prefix scan
29
+
30
+
31
+ class FirestoreStore(KVStore):
32
+ """LangGraph ``BaseStore`` backed by Google Firestore.
33
+
34
+ Example:
35
+ ```python
36
+ from langgraph_store_firestore import FirestoreStore
37
+
38
+ store = FirestoreStore(project_id="my-gcp-project")
39
+ store.put(("users", "1"), "profile", {"name": "Kamal"})
40
+
41
+ # semantic search
42
+ store = FirestoreStore(project_id=..., index={"dims": 1024, "embed": my_embedder, "fields": ["text"]})
43
+ hits = store.search(("users", "1"), query="what does the user like?")
44
+ ```
45
+
46
+ Authentication uses Application Default Credentials. With ``index`` the
47
+ embedding is stored as a Firestore ``Vector`` on ``embedding`` and queries
48
+ use ``find_nearest`` (cosine). Firestore requires a **vector index** on the
49
+ collection; when ``create_index=True`` (default) the store creates a
50
+ composite index ``prefix ASC + embedding (flat, dims)`` via the Admin API if
51
+ one is missing (index build is asynchronous; the first queries may fail
52
+ until it is READY).
53
+ """
54
+
55
+ supports_native_vector_search = True
56
+
57
+ def __init__(
58
+ self,
59
+ *,
60
+ project_id: str,
61
+ collection: str = "langgraph_store",
62
+ client: Optional["firestore.Client"] = None,
63
+ index: Optional[IndexConfig] = None,
64
+ create_index: bool = True,
65
+ database: str = "(default)",
66
+ ) -> None:
67
+ super().__init__(index=index)
68
+ self._project = project_id
69
+ self._database = database
70
+ self._client = client or firestore.Client(project=project_id, database=database)
71
+ self._col = self._client.collection(collection)
72
+ self._collection_name = collection
73
+ if self.dims and create_index:
74
+ self._ensure_vector_index()
75
+
76
+ # ------------------------------------------------------------------ index bootstrap
77
+ def _ensure_vector_index(self) -> None:
78
+ """Create the composite vector index (prefix ASC, embedding flat) if absent."""
79
+ from google.cloud import firestore_admin_v1 as fa
80
+
81
+ admin = fa.FirestoreAdminClient()
82
+ parent = (
83
+ f"projects/{self._project}/databases/{self._database}"
84
+ f"/collectionGroups/{self._collection_name}"
85
+ )
86
+ for ix in admin.list_indexes(parent=parent):
87
+ paths = [f.field_path for f in ix.fields]
88
+ if "embedding" in paths and "prefix" in paths:
89
+ return
90
+ index = fa.Index(
91
+ query_scope=fa.Index.QueryScope.COLLECTION,
92
+ fields=[
93
+ fa.Index.IndexField(field_path="prefix", order=fa.Index.IndexField.Order.ASCENDING),
94
+ fa.Index.IndexField(
95
+ field_path="embedding",
96
+ vector_config=fa.Index.IndexField.VectorConfig(
97
+ dimension=self.dims, flat=fa.Index.IndexField.VectorConfig.FlatIndex()
98
+ ),
99
+ ),
100
+ ],
101
+ )
102
+ try:
103
+ admin.create_index(parent=parent, index=index)
104
+ logger.info("FirestoreStore: creating vector index on %s (async)", self._collection_name)
105
+ except Exception as exc: # already exists / racing creation
106
+ logger.info("FirestoreStore: vector index create skipped: %s", exc)
107
+
108
+ # ------------------------------------------------------------------ primitives
109
+ @staticmethod
110
+ def _docid(prefix: str, key: str) -> str:
111
+ # Firestore doc ids can't contain '/'; url-encode both parts, join safely.
112
+ return f"{quote(prefix, safe='')}__{quote(key, safe='')}"
113
+
114
+ def _row(self, d: dict) -> dict:
115
+ row = {f: d[f] for f in _FIELDS}
116
+ emb = d.get("embedding")
117
+ row["embedding"] = list(emb.to_map_value()["value"]) if isinstance(emb, Vector) else (
118
+ list(emb) if emb is not None else None
119
+ )
120
+ return row
121
+
122
+ def _read(self, prefix: str, key: str) -> Optional[dict]:
123
+ doc = self._col.document(self._docid(prefix, key)).get()
124
+ if not doc.exists:
125
+ return None
126
+ return self._row(doc.to_dict())
127
+
128
+ def _write(self, row: dict) -> None:
129
+ doc = {f: row[f] for f in _FIELDS}
130
+ if self.dims and row.get("embedding") is not None:
131
+ doc["embedding"] = Vector(row["embedding"])
132
+ self._col.document(self._docid(row["prefix"], row["key"])).set(doc)
133
+
134
+ def _remove(self, prefix: str, key: str) -> None:
135
+ self._col.document(self._docid(prefix, key)).delete()
136
+
137
+ def _prefix_query(self, prefix: str):
138
+ if prefix == "":
139
+ return self._col
140
+ return (
141
+ self._col.where(filter=FieldFilter("prefix", ">=", prefix))
142
+ .where(filter=FieldFilter("prefix", "<", prefix + _PREFIX_HIGH))
143
+ )
144
+
145
+ def _scan(self, prefix: str) -> List[dict]:
146
+ return [self._row(d.to_dict()) for d in self._prefix_query(prefix).stream()]
147
+
148
+ def _vector_search(
149
+ self, prefix: str, vector: List[float], filter: Optional[dict], limit: int
150
+ ) -> List[Tuple[dict, float]]:
151
+ """Native Firestore ``find_nearest`` (cosine) after a prefix range pre-filter.
152
+
153
+ Firestore pre-filters are limited to indexed equality/range fields, so
154
+ value filters are applied by core on the returned candidates; we
155
+ oversample to leave room for that.
156
+ """
157
+ oversample = limit * 4 if filter else limit
158
+ q = self._prefix_query(prefix).find_nearest(
159
+ vector_field="embedding",
160
+ query_vector=Vector(vector),
161
+ distance_measure=DistanceMeasure.COSINE,
162
+ limit=oversample,
163
+ distance_result_field="_distance",
164
+ )
165
+ out: List[Tuple[dict, float]] = []
166
+ for d in q.stream():
167
+ data = d.to_dict()
168
+ dist = data.pop("_distance", None)
169
+ row = self._row(data)
170
+ out.append((row, 1.0 - float(dist) if dist is not None else 0.0))
171
+ return out
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: langgraph-store-firestore
3
+ Version: 0.1.0
4
+ Summary: Google Firestore long-term-memory store (BaseStore) for LangGraph — namespaced key/value memory with filters, list_namespaces and native semantic (Firestore vector) search
5
+ Project-URL: Homepage, https://github.com/skamalj/langgraph-store
6
+ Project-URL: Repository, https://github.com/skamalj/langgraph-store.git
7
+ Author-email: Kamal <skamalj@gmail.com>
8
+ Keywords: agent-memory,basestore,firestore,gcp,langgraph,long-term-memory,store
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: google-cloud-firestore
15
+ Requires-Dist: langgraph-store-core>=0.1.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # langgraph-store-firestore
19
+
20
+ A **Google Firestore** long-term-memory store (`BaseStore`) for [LangGraph](https://langchain-ai.github.io/langgraph/) — namespaced key/value memory with prefix search, filters, `list_namespaces`, and **native semantic search via Firestore vector search**.
21
+
22
+ ```bash
23
+ pip install langgraph-store-firestore
24
+ ```
25
+
26
+ ```python
27
+ from langgraph_store_firestore import FirestoreStore
28
+
29
+ store = FirestoreStore(project_id="my-gcp-project")
30
+ store.put(("users", "1", "memories"), "food", {"text": "loves sushi", "kind": "pref"})
31
+ item = store.get(("users", "1", "memories"), "food")
32
+ hits = store.search(("users", "1"), filter={"kind": "pref"})
33
+ ```
34
+
35
+ Use it as a LangGraph store: `graph.compile(store=FirestoreStore(...))`. Async methods work too (sync calls run in a thread). Authentication uses Application Default Credentials (`gcloud auth application-default login`).
36
+
37
+ ## Semantic search (Firestore vector search)
38
+
39
+ Pass a LangGraph `IndexConfig` and the store embeds the configured fields on `put` (stored as a Firestore `Vector`) and ranks `search(query=...)` by cosine similarity using `find_nearest`:
40
+
41
+ ```python
42
+ from langgraph_store_core import bedrock_titan_embeddings
43
+ from langgraph_store_firestore import FirestoreStore
44
+
45
+ store = FirestoreStore(project_id="my-gcp-project", collection="memory",
46
+ index={"dims": 1024, "embed": bedrock_titan_embeddings(dimensions=1024), "fields": ["text"]})
47
+ store.put(("memories", "kamal"), "k1", {"text": "the user loves sushi", "kind": "pref"})
48
+ hits = store.search(("memories", "kamal"), query="what food does the user like?", filter={"kind": "pref"})
49
+ print(hits[0].score, hits[0].value)
50
+ ```
51
+
52
+ `embed` may be any LangChain `Embeddings`, a `list[str] -> list[list[float]]` callable, or a provider string. `fields` defaults to `["$"]` (whole value as JSON). `put(..., index=False)` skips embedding for one item.
53
+
54
+ Firestore requires a **vector index**. With `create_index=True` (default) the store creates a composite index `prefix ASC + embedding (flat, dims)` on the collection through the Firestore Admin API if one is missing; the build is asynchronous and `query` searches fail with `FAILED_PRECONDITION` until it is READY (usually a few minutes; check `gcloud firestore indexes composite list`). The caller needs `datastore.indexes.create` / `list` permissions, or create the index yourself and pass `create_index=False`:
55
+
56
+ ```bash
57
+ gcloud firestore indexes composite create --collection-group=memory --query-scope=COLLECTION \
58
+ --field-config=order=ASCENDING,field-path=prefix \
59
+ --field-config=vector-config='{"dimension":"1024","flat":"{}"}',field-path=embedding
60
+ ```
61
+
62
+ The namespace prefix is a range pre-filter on the vector query. Firestore pre-filters are limited to indexed equality/range fields, so value `filter`s are applied on the returned candidates (the store oversamples when a filter is given).
63
+
64
+ ## Data model
65
+
66
+ A single collection of documents `{prefix, key, value, created_at, updated_at[, embedding: Vector]}`, doc id = `enc(prefix)__enc(key)`. Search is a `prefix` range query; filters and namespace matching are evaluated in the [core](https://pypi.org/project/langgraph-store-core/).
67
+
68
+ Docs: <https://skamalj.github.io/agentstate-reducer/>
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,4 @@
1
+ langgraph_store_firestore/__init__.py,sha256=ShCoHiaY58TXzunNHZgwu-0ZjD-tJA9dB5H07ceRUME,7105
2
+ langgraph_store_firestore-0.1.0.dist-info/METADATA,sha256=7eWR6VqGOO0aDDt2ZwrQDxS8INnq8CkBeZhXzqmAXQg,4126
3
+ langgraph_store_firestore-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
4
+ langgraph_store_firestore-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any