mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-02 06:25:09 +02:00
174 lines
5.8 KiB
Python
174 lines
5.8 KiB
Python
import pickle
|
|
import sqlite3
|
|
from contextlib import AbstractContextManager, contextmanager
|
|
from types import TracebackType
|
|
from typing import Any, Iterator, Optional
|
|
|
|
from langchain_core.runnables import RunnableConfig
|
|
from typing_extensions import Self
|
|
|
|
from langgraph.checkpoint.base import (
|
|
BaseCheckpointSaver,
|
|
Checkpoint,
|
|
CheckpointAt,
|
|
CheckpointTuple,
|
|
SerializerProtocol,
|
|
)
|
|
from langgraph.serde.jsonplus import JsonPlusSerializer
|
|
|
|
|
|
# for backwards compat we continue to support loading pickled checkpoints
|
|
class JsonPlusSerializerCompat(JsonPlusSerializer):
|
|
def loads(self, data: bytes) -> Any:
|
|
if data.startswith(b"\x80") and data.endswith(b"."):
|
|
return pickle.loads(data)
|
|
return super().loads(data)
|
|
|
|
|
|
class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
|
|
serde = JsonPlusSerializerCompat()
|
|
|
|
conn: sqlite3.Connection
|
|
|
|
is_setup: bool
|
|
|
|
def __init__(
|
|
self,
|
|
conn: sqlite3.Connection,
|
|
*,
|
|
serde: Optional[SerializerProtocol] = None,
|
|
at: Optional[CheckpointAt] = None,
|
|
) -> None:
|
|
super().__init__(serde=serde, at=at)
|
|
self.conn = conn
|
|
self.is_setup = False
|
|
|
|
@classmethod
|
|
def from_conn_string(cls, conn_string: str) -> "SqliteSaver":
|
|
return SqliteSaver(conn=sqlite3.connect(conn_string))
|
|
|
|
def __enter__(self) -> Self:
|
|
return self
|
|
|
|
def __exit__(
|
|
self,
|
|
__exc_type: Optional[type[BaseException]],
|
|
__exc_value: Optional[BaseException],
|
|
__traceback: Optional[TracebackType],
|
|
) -> Optional[bool]:
|
|
return self.conn.close()
|
|
|
|
def setup(self) -> None:
|
|
if self.is_setup:
|
|
return
|
|
|
|
self.conn.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
thread_id TEXT NOT NULL,
|
|
thread_ts TEXT NOT NULL,
|
|
parent_ts TEXT,
|
|
checkpoint BLOB,
|
|
PRIMARY KEY (thread_id, thread_ts)
|
|
);
|
|
"""
|
|
)
|
|
|
|
self.is_setup = True
|
|
|
|
@contextmanager
|
|
def cursor(self, transaction: bool = True):
|
|
self.setup()
|
|
cur = self.conn.cursor()
|
|
try:
|
|
yield cur
|
|
finally:
|
|
if transaction:
|
|
self.conn.commit()
|
|
cur.close()
|
|
|
|
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
with self.cursor(transaction=False) as cur:
|
|
if config["configurable"].get("thread_ts"):
|
|
cur.execute(
|
|
"SELECT checkpoint, parent_ts FROM checkpoints WHERE thread_id = ? AND thread_ts = ?",
|
|
(
|
|
str(config["configurable"]["thread_id"]),
|
|
str(config["configurable"]["thread_ts"]),
|
|
),
|
|
)
|
|
if value := cur.fetchone():
|
|
return CheckpointTuple(
|
|
config,
|
|
self.serde.loads(value[0]),
|
|
{
|
|
"configurable": {
|
|
"thread_id": config["configurable"]["thread_id"],
|
|
"thread_ts": value[1],
|
|
}
|
|
}
|
|
if value[1]
|
|
else None,
|
|
)
|
|
else:
|
|
cur.execute(
|
|
"SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC LIMIT 1",
|
|
(str(config["configurable"]["thread_id"]),),
|
|
)
|
|
if value := cur.fetchone():
|
|
return CheckpointTuple(
|
|
{
|
|
"configurable": {
|
|
"thread_id": value[0],
|
|
"thread_ts": value[1],
|
|
}
|
|
},
|
|
self.serde.loads(value[3]),
|
|
{
|
|
"configurable": {
|
|
"thread_id": value[0],
|
|
"thread_ts": value[2],
|
|
}
|
|
}
|
|
if value[2]
|
|
else None,
|
|
)
|
|
|
|
def list(self, config: RunnableConfig) -> Iterator[CheckpointTuple]:
|
|
with self.cursor(transaction=False) as cur:
|
|
cur.execute(
|
|
"SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC",
|
|
(str(config["configurable"]["thread_id"]),),
|
|
)
|
|
for thread_id, thread_ts, parent_ts, value in cur:
|
|
yield CheckpointTuple(
|
|
{"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}},
|
|
self.serde.loads(value),
|
|
{
|
|
"configurable": {
|
|
"thread_id": thread_id,
|
|
"thread_ts": parent_ts,
|
|
}
|
|
}
|
|
if parent_ts
|
|
else None,
|
|
)
|
|
|
|
def put(self, config: RunnableConfig, checkpoint: Checkpoint) -> RunnableConfig:
|
|
with self.cursor() as cur:
|
|
cur.execute(
|
|
"INSERT OR REPLACE INTO checkpoints (thread_id, thread_ts, parent_ts, checkpoint) VALUES (?, ?, ?, ?)",
|
|
(
|
|
str(config["configurable"]["thread_id"]),
|
|
checkpoint["ts"],
|
|
config["configurable"].get("thread_ts"),
|
|
self.serde.dumps(checkpoint),
|
|
),
|
|
)
|
|
return {
|
|
"configurable": {
|
|
"thread_id": config["configurable"]["thread_id"],
|
|
"thread_ts": checkpoint["ts"],
|
|
}
|
|
}
|