langgraph-runtime-pg 0.11.1__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.
- langgraph_runtime_pg/__init__.py +38 -0
- langgraph_runtime_pg/checkpoint.py +83 -0
- langgraph_runtime_pg/database.py +480 -0
- langgraph_runtime_pg/lifespan.py +198 -0
- langgraph_runtime_pg/metrics.py +7 -0
- langgraph_runtime_pg/migrate.py +101 -0
- langgraph_runtime_pg/migrations/__init__.py +1 -0
- langgraph_runtime_pg/migrations/env.py +51 -0
- langgraph_runtime_pg/migrations/script.py.mako +27 -0
- langgraph_runtime_pg/migrations/versions/001_initial_schema.py +331 -0
- langgraph_runtime_pg/migrations/versions/__init__.py +1 -0
- langgraph_runtime_pg/models.py +196 -0
- langgraph_runtime_pg/ops.py +3538 -0
- langgraph_runtime_pg/queue.py +270 -0
- langgraph_runtime_pg/redis_stream.py +894 -0
- langgraph_runtime_pg/retry.py +39 -0
- langgraph_runtime_pg/routes.py +9 -0
- langgraph_runtime_pg/store.py +183 -0
- langgraph_runtime_pg-0.11.1.dist-info/METADATA +47 -0
- langgraph_runtime_pg-0.11.1.dist-info/RECORD +23 -0
- langgraph_runtime_pg-0.11.1.dist-info/WHEEL +4 -0
- langgraph_runtime_pg-0.11.1.dist-info/entry_points.txt +2 -0
- langgraph_runtime_pg-0.11.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""DB retry helpers matching the inmem surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from typing import ParamSpec, TypeVar
|
|
9
|
+
|
|
10
|
+
P = ParamSpec("P")
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RetryableException(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
RETRIABLE_EXCEPTIONS: tuple[type[BaseException], ...] = (RetryableException,)
|
|
19
|
+
OVERLOADED_EXCEPTIONS: tuple[type[BaseException], ...] = ()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def retry_db(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
23
|
+
"""Retry ``RETRIABLE_EXCEPTIONS`` up to 3 times."""
|
|
24
|
+
|
|
25
|
+
@functools.wraps(func)
|
|
26
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
27
|
+
last: BaseException | None = None
|
|
28
|
+
for i in range(3):
|
|
29
|
+
try:
|
|
30
|
+
return await func(*args, **kwargs)
|
|
31
|
+
except RETRIABLE_EXCEPTIONS as exc:
|
|
32
|
+
last = exc
|
|
33
|
+
if i == 2:
|
|
34
|
+
raise
|
|
35
|
+
await asyncio.sleep(0.01)
|
|
36
|
+
assert last is not None # pragma: no cover
|
|
37
|
+
raise last # pragma: no cover
|
|
38
|
+
|
|
39
|
+
return wrapper
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Store factory: AsyncPostgresStore for the API, PgStore for tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from langgraph.store.postgres.aio import AsyncPostgresStore
|
|
10
|
+
from psycopg.rows import dict_row
|
|
11
|
+
from psycopg_pool import AsyncConnectionPool
|
|
12
|
+
from sqlalchemy import delete, select as sa_select
|
|
13
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
15
|
+
|
|
16
|
+
from langgraph_runtime_pg.database import to_psycopg_uri
|
|
17
|
+
from langgraph_runtime_pg.models import StoreItemRow
|
|
18
|
+
|
|
19
|
+
_STORE_CONFIG: dict | None = None
|
|
20
|
+
_STORE: AsyncPostgresStore | None = None
|
|
21
|
+
_STORE_POOL: AsyncConnectionPool[Any] | None = None
|
|
22
|
+
_STORE_INSTANCE: PgStore | None = None
|
|
23
|
+
_SETUP_LOCK = asyncio.Lock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_store_config(config: dict) -> None:
|
|
27
|
+
global _STORE_CONFIG
|
|
28
|
+
_STORE_CONFIG = config
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def setup_store() -> AsyncPostgresStore:
|
|
32
|
+
"""Open AsyncPostgresStore and initialize its tables."""
|
|
33
|
+
global _STORE, _STORE_POOL
|
|
34
|
+
async with _SETUP_LOCK:
|
|
35
|
+
if _STORE is not None:
|
|
36
|
+
return _STORE
|
|
37
|
+
if _STORE_POOL is not None:
|
|
38
|
+
try:
|
|
39
|
+
await _STORE_POOL.close()
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
_STORE_POOL = None
|
|
43
|
+
|
|
44
|
+
uri = to_psycopg_uri()
|
|
45
|
+
pool = AsyncConnectionPool(
|
|
46
|
+
conninfo=uri,
|
|
47
|
+
min_size=1,
|
|
48
|
+
max_size=10,
|
|
49
|
+
open=False,
|
|
50
|
+
kwargs={
|
|
51
|
+
"autocommit": True,
|
|
52
|
+
"prepare_threshold": 0,
|
|
53
|
+
"row_factory": dict_row,
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
await pool.open()
|
|
58
|
+
store = AsyncPostgresStore(cast(Any, pool))
|
|
59
|
+
await store.setup()
|
|
60
|
+
except Exception:
|
|
61
|
+
try:
|
|
62
|
+
await pool.close()
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
raise
|
|
66
|
+
_STORE_POOL = pool
|
|
67
|
+
_STORE = store
|
|
68
|
+
return _STORE
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def teardown_store() -> None:
|
|
72
|
+
global _STORE, _STORE_POOL
|
|
73
|
+
store, pool = _STORE, _STORE_POOL
|
|
74
|
+
_STORE = None
|
|
75
|
+
_STORE_POOL = None
|
|
76
|
+
if store is not None:
|
|
77
|
+
try:
|
|
78
|
+
stop = getattr(store, "stop_ttl_sweeper", None)
|
|
79
|
+
if stop is not None:
|
|
80
|
+
await asyncio.wait_for(stop(), timeout=2.0)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
if pool is not None:
|
|
84
|
+
try:
|
|
85
|
+
await asyncio.wait_for(pool.close(), timeout=5.0)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PgStore:
|
|
91
|
+
"""Minimal prefix/key store over the store_items table (tests)."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
|
|
94
|
+
self._sf = session_factory
|
|
95
|
+
|
|
96
|
+
async def start_ttl_sweeper(self) -> asyncio.Task:
|
|
97
|
+
return asyncio.create_task(asyncio.sleep(0))
|
|
98
|
+
|
|
99
|
+
async def aput(self, prefix: str, key: str, value: dict) -> None:
|
|
100
|
+
now = datetime.now(UTC)
|
|
101
|
+
async with self._sf() as session:
|
|
102
|
+
await session.execute(
|
|
103
|
+
pg_insert(StoreItemRow)
|
|
104
|
+
.values(
|
|
105
|
+
prefix=prefix,
|
|
106
|
+
key=key,
|
|
107
|
+
value=value,
|
|
108
|
+
created_at=now,
|
|
109
|
+
updated_at=now,
|
|
110
|
+
)
|
|
111
|
+
.on_conflict_do_update(
|
|
112
|
+
index_elements=["prefix", "key"],
|
|
113
|
+
set_={"value": value, "updated_at": now},
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
await session.commit()
|
|
117
|
+
|
|
118
|
+
async def aget(self, prefix: str, key: str) -> dict | None:
|
|
119
|
+
async with self._sf() as session:
|
|
120
|
+
row = (
|
|
121
|
+
await session.execute(
|
|
122
|
+
sa_select(StoreItemRow).where(
|
|
123
|
+
StoreItemRow.prefix == prefix,
|
|
124
|
+
StoreItemRow.key == key,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
).scalar_one_or_none()
|
|
128
|
+
return row.value if row else None
|
|
129
|
+
|
|
130
|
+
async def adelete(self, prefix: str, key: str) -> None:
|
|
131
|
+
async with self._sf() as session:
|
|
132
|
+
await session.execute(
|
|
133
|
+
delete(StoreItemRow).where(
|
|
134
|
+
StoreItemRow.prefix == prefix,
|
|
135
|
+
StoreItemRow.key == key,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
await session.commit()
|
|
139
|
+
|
|
140
|
+
async def alist(self, prefix: str) -> list[dict]:
|
|
141
|
+
async with self._sf() as session:
|
|
142
|
+
rows = (
|
|
143
|
+
(
|
|
144
|
+
await session.execute(
|
|
145
|
+
sa_select(StoreItemRow).where(StoreItemRow.prefix == prefix)
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
.scalars()
|
|
149
|
+
.all()
|
|
150
|
+
)
|
|
151
|
+
return [{"key": r.key, "value": r.value} for r in rows]
|
|
152
|
+
|
|
153
|
+
def close(self) -> None:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def Store(*args: Any, **kwargs: Any) -> AsyncPostgresStore:
|
|
158
|
+
"""Return the process-wide AsyncPostgresStore (requires setup_store)."""
|
|
159
|
+
if _STORE is None:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
"Store requires setup_store()/start_pool() first (DATABASE_URI required)"
|
|
162
|
+
)
|
|
163
|
+
return _STORE
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def pg_store_for_tests() -> PgStore:
|
|
167
|
+
"""Return the test PgStore singleton over store_items."""
|
|
168
|
+
global _STORE_INSTANCE
|
|
169
|
+
if _STORE_INSTANCE is None:
|
|
170
|
+
from langgraph_runtime_pg.database import _SESSION_FACTORY
|
|
171
|
+
|
|
172
|
+
if _SESSION_FACTORY is None:
|
|
173
|
+
raise RuntimeError("Call start_pool() first")
|
|
174
|
+
_STORE_INSTANCE = PgStore(_SESSION_FACTORY)
|
|
175
|
+
return _STORE_INSTANCE
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def reset_store() -> None:
|
|
179
|
+
"""Reset the test PgStore singleton."""
|
|
180
|
+
global _STORE_INSTANCE
|
|
181
|
+
if _STORE_INSTANCE is not None:
|
|
182
|
+
_STORE_INSTANCE.close()
|
|
183
|
+
_STORE_INSTANCE = None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langgraph-runtime-pg
|
|
3
|
+
Version: 0.11.1
|
|
4
|
+
Summary: Self-hosted LangSmith Deployments alternative: MIT Postgres+Redis runtime (edition=pg) for stock langgraph-api
|
|
5
|
+
Project-URL: Homepage, https://github.com/langhost/langhost
|
|
6
|
+
Project-URL: Repository, https://github.com/langhost/langhost
|
|
7
|
+
Project-URL: Issues, https://github.com/langhost/langhost/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/langhost/langhost/releases
|
|
9
|
+
Author-email: Mohankumar Ramachandran <mail@mohanram.dev>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-protocol,agent-server,agents,ai-agents,asgi,checkpoint,langchain,langgraph,langgraph-api,langgraph-checkpoint,langgraph-sdk,langsmith,langsmith-deployments,llm,multi-agent,open-source,postgres,postgresql,redis,runtime,self-hosted,uvicorn
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: alembic>=1.14
|
|
29
|
+
Requires-Dist: asyncpg>=0.29
|
|
30
|
+
Requires-Dist: croniter>=1.0
|
|
31
|
+
Requires-Dist: langgraph-api==0.11.1
|
|
32
|
+
Requires-Dist: langgraph-checkpoint-postgres==3.1.0
|
|
33
|
+
Requires-Dist: orjson>=3.9
|
|
34
|
+
Requires-Dist: psycopg[binary]>=3.3.4
|
|
35
|
+
Requires-Dist: redis>=5.0
|
|
36
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.51
|
|
37
|
+
Requires-Dist: starlette>=0.37
|
|
38
|
+
Requires-Dist: structlog>=24.0
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# langgraph-runtime-pg
|
|
42
|
+
|
|
43
|
+
MIT Postgres + Redis runtime (`LANGGRAPH_RUNTIME_EDITION=pg`) for stock
|
|
44
|
+
[`langgraph-api`](https://pypi.org/project/langgraph-api/).
|
|
45
|
+
|
|
46
|
+
See the [repository README](https://github.com/langhost/langhost)
|
|
47
|
+
for setup, CLI (`langhost`), and architecture.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
langgraph_runtime_pg/__init__.py,sha256=ZUcjd1kcny4doy9RCGYC660IGUY4Kncym5kNOsoLCpo,697
|
|
2
|
+
langgraph_runtime_pg/checkpoint.py,sha256=CDbfl0rpiU5obyWN5844qchR3yUxuredYiQ04-X0SW4,2413
|
|
3
|
+
langgraph_runtime_pg/database.py,sha256=aKuaURsEZOH5MMwBBCDrSrHDgPedmOz1D-S4S9i1wbg,14389
|
|
4
|
+
langgraph_runtime_pg/lifespan.py,sha256=j7RIbIE-9ASfL60dG8AYF1DitLZOsOpw2Sgfirz4Xdg,6603
|
|
5
|
+
langgraph_runtime_pg/metrics.py,sha256=IUwlI2xAhwnYwiOR-ltg-aO655aEX0RouHKNJbRWlBc,128
|
|
6
|
+
langgraph_runtime_pg/migrate.py,sha256=eq0GcE6EhapEOYv6jeNyQlm--G7qcwvN5AJbXqDAJHs,3061
|
|
7
|
+
langgraph_runtime_pg/models.py,sha256=Fblz8JONqGpkPCaiU6kk4yPyrOUdh99MbkULhFcBons,8474
|
|
8
|
+
langgraph_runtime_pg/ops.py,sha256=JAtM02a-81ALZrMocftvvin_jawr2bUG7vHHgrDoAPM,133432
|
|
9
|
+
langgraph_runtime_pg/queue.py,sha256=MjDRKtEGUp9Sk-n6caQATiYnH7e-_7Dw_o8ofMVY9oo,10485
|
|
10
|
+
langgraph_runtime_pg/redis_stream.py,sha256=Z-UQNyNRHkdOlZSMP2EigE6CvNooUE8Kp-2SAc57l6E,31813
|
|
11
|
+
langgraph_runtime_pg/retry.py,sha256=uRx9QtuWKVdw4KGyfiI8NBklqVensGXYcTiun8joMYQ,1074
|
|
12
|
+
langgraph_runtime_pg/routes.py,sha256=r2hK0E1RKyi1D4PBF3-dsk7_otu0BZZZfNzaa3TPEwo,144
|
|
13
|
+
langgraph_runtime_pg/store.py,sha256=hFbQTVJLAPhFELwVdABAHvShENDztppD8VJ1v5QyaS4,5654
|
|
14
|
+
langgraph_runtime_pg/migrations/__init__.py,sha256=M-gt0gXAGxVsYXJgZouNInvcPmk291foMrYnhooVaS0,56
|
|
15
|
+
langgraph_runtime_pg/migrations/env.py,sha256=fWLULVAKqqmSvCpVHJ61WyJzuBYnqCSm8eCWgUbjHCY,1329
|
|
16
|
+
langgraph_runtime_pg/migrations/versions/001_initial_schema.py,sha256=MnzQ8mEl0xCMhT5RBC4NujyEKUIkC8rhxHM_HWoXXYc,11747
|
|
17
|
+
langgraph_runtime_pg/migrations/versions/__init__.py,sha256=4paf33VBK_l7fudrFVmQbL68tQNDGVNFUGgdjxBSq5s,32
|
|
18
|
+
langgraph_runtime_pg/migrations/script.py.mako,sha256=kyVbIw4oMjKYXbLX7wA29--0PK_UU--SdXx-my0Znls,645
|
|
19
|
+
langgraph_runtime_pg-0.11.1.dist-info/METADATA,sha256=y7Ysm6w5tXlDcsGiZFDD3pQg-C41X7lQINCUBLdzY5k,2194
|
|
20
|
+
langgraph_runtime_pg-0.11.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
21
|
+
langgraph_runtime_pg-0.11.1.dist-info/entry_points.txt,sha256=8S_Q8wEvQtqbTxqgbHazjiCnmY2NDOEH2dLDVIHd3WA,83
|
|
22
|
+
langgraph_runtime_pg-0.11.1.dist-info/licenses/LICENSE,sha256=VOzjTuqsyEVtwd7ylP1eoLjPAAvdB6iC9a7JBGFJUgE,1080
|
|
23
|
+
langgraph_runtime_pg-0.11.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohankumar Ramachandran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|