From a16f86b5bd088822eba40264449c0914f5f872aa Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 8 Aug 2024 12:01:19 -0400 Subject: [PATCH] docs: update postgres persistence example (#1276) --- docs/docs/how-tos/index.md | 2 +- docs/docs/reference/checkpoints.md | 11 + docs/mkdocs.yml | 2 +- examples/persistence_postgres.ipynb | 1586 +++++++++------------------ poetry.lock | 120 +- pyproject.toml | 1 + 6 files changed, 673 insertions(+), 1049 deletions(-) diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index f301ff934..81fb561e9 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -25,7 +25,7 @@ LangGraph makes it easy to persist state across graph runs. The guide below show - [How to manage conversation history](memory/manage-conversation-history.ipynb) - [How to delete messages](memory/delete-messages.ipynb) - [How to add summary conversation memory](memory/add-summary-conversation-history.ipynb) -- [How to create a custom checkpointer using Postgres](persistence_postgres.ipynb) +- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb) - [How to create a custom checkpointer using MongoDB](persistence_mongodb.ipynb) - [How to create a custom checkpointer using Redis](persistence_redis.ipynb) diff --git a/docs/docs/reference/checkpoints.md b/docs/docs/reference/checkpoints.md index 3108091f4..a7031613d 100644 --- a/docs/docs/reference/checkpoints.md +++ b/docs/docs/reference/checkpoints.md @@ -40,4 +40,15 @@ LangGraph also natively provides the following checkpoint implementations. ### SqliteSaver ::: langgraph.checkpoint.sqlite.SqliteSaver + +### AsyncPostgresSaver + +::: langgraph.checkpoint.postgres.aio.AsyncPostgresSaver + +### PostgresSaver + +::: langgraph.checkpoint.postgres.PostgresSaver +handler: python + + handler: python diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ac3601ce1..70df48bb1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -134,7 +134,7 @@ nav: - Manage conversation history: how-tos/memory/manage-conversation-history.ipynb - Delete messages: how-tos/memory/delete-messages.ipynb - Add summary of the conversation history: how-tos/memory/add-summary-conversation-history.ipynb - - Create custom checkpointer using Postgres: how-tos/persistence_postgres.ipynb + - Use Postgres checkpointer for persistence: how-tos/persistence_postgres.ipynb - Create custom checkpointer using MongoDB: how-tos/persistence_mongodb.ipynb - Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb - Human-in-the-loop: diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index d67e674b6..7b62defef 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -1,1043 +1,549 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to create a custom checkpointer using Postgres\n", - "\n", - "When creating LangGraph agents, you can also set them up so that they persist their state. This allows you to do things like interact with an agent multiple times and have it remember previous interactions.\n", - "\n", - "This example shows how to use `Postgres` as the backend for persisting checkpoint state.\n", - "\n", - "NOTE: this is just an example implementation. You can implement your own checkpointer using a different database or modify this one as long as it conforms to the `BaseCheckpointSaver` interface." - ] - }, - { - "cell_type": "markdown", - "id": "ddc5cc5f-89fa-4d7a-9f2b-5b099feb6ecc", - "metadata": {}, - "source": [ - "## Checkpointer implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "faadfb1b-cebe-4dcf-82fd-34044c380bc4", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U psycopg psycopg-pool langgraph" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a35dba8e-5562-4803-ad80-160f53592dd7", - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"Implementation of a langgraph checkpoint saver using Postgres.\"\"\"\n", - "from contextlib import asynccontextmanager, contextmanager\n", - "from typing import (\n", - " Any,\n", - " AsyncGenerator,\n", - " AsyncIterator,\n", - " Generator,\n", - " Optional,\n", - " Union,\n", - " Tuple,\n", - " List,\n", - " Sequence,\n", - ")\n", - "\n", - "import psycopg\n", - "from langchain_core.runnables import RunnableConfig\n", - "from langgraph.checkpoint.base import BaseCheckpointSaver\n", - "from langgraph.serde.jsonplus import JsonPlusSerializer\n", - "from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata, CheckpointTuple\n", - "from psycopg_pool import AsyncConnectionPool, ConnectionPool\n", - "\n", - "\n", - "class JsonAndBinarySerializer(JsonPlusSerializer):\n", - " def _default(self, obj):\n", - " if isinstance(obj, (bytes, bytearray)):\n", - " return self._encode_constructor_args(\n", - " obj.__class__, method=\"fromhex\", args=[obj.hex()]\n", - " )\n", - " return super()._default(obj)\n", - "\n", - " def dumps(self, obj: Any) -> tuple[str, bytes]:\n", - " if isinstance(obj, bytes):\n", - " return \"bytes\", obj\n", - " elif isinstance(obj, bytearray):\n", - " return \"bytearray\", obj\n", - "\n", - " return \"json\", super().dumps(obj)\n", - "\n", - " def loads(self, s: tuple[str, bytes]) -> Any:\n", - " if s[0] == \"bytes\":\n", - " return s[1]\n", - " elif s[0] == \"bytearray\":\n", - " return bytearray(s[1])\n", - " elif s[0] == \"json\":\n", - " return super().loads(s[1])\n", - " else:\n", - " raise NotImplementedError(f\"Unknown serialization type: {s[0]}\")\n", - "\n", - "\n", - "@contextmanager\n", - "def _get_sync_connection(\n", - " connection: Union[psycopg.Connection, ConnectionPool, None],\n", - ") -> Generator[psycopg.Connection, None, None]:\n", - " \"\"\"Get the connection to the Postgres database.\"\"\"\n", - " if isinstance(connection, psycopg.Connection):\n", - " yield connection\n", - " elif isinstance(connection, ConnectionPool):\n", - " with connection.connection() as conn:\n", - " yield conn\n", - " else:\n", - " raise ValueError(\n", - " \"Invalid sync connection object. Please initialize the check pointer \"\n", - " f\"with an appropriate sync connection object. \"\n", - " f\"Got {type(connection)}.\"\n", - " )\n", - "\n", - "\n", - "@asynccontextmanager\n", - "async def _get_async_connection(\n", - " connection: Union[psycopg.AsyncConnection, AsyncConnectionPool, None],\n", - ") -> AsyncGenerator[psycopg.AsyncConnection, None]:\n", - " \"\"\"Get the connection to the Postgres database.\"\"\"\n", - " if isinstance(connection, psycopg.AsyncConnection):\n", - " yield connection\n", - " elif isinstance(connection, AsyncConnectionPool):\n", - " async with connection.connection() as conn:\n", - " yield conn\n", - " else:\n", - " raise ValueError(\n", - " \"Invalid async connection object. Please initialize the check pointer \"\n", - " f\"with an appropriate async connection object. \"\n", - " f\"Got {type(connection)}.\"\n", - " )\n", - "\n", - "\n", - "class PostgresSaver(BaseCheckpointSaver):\n", - " sync_connection: Optional[Union[psycopg.Connection, ConnectionPool]] = None\n", - " \"\"\"The synchronous connection or pool to the Postgres database.\n", - " \n", - " If providing a connection object, please ensure that the connection is open\n", - " and remember to close the connection when done.\n", - " \"\"\"\n", - " async_connection: Optional[\n", - " Union[psycopg.AsyncConnection, AsyncConnectionPool]\n", - " ] = None\n", - " \"\"\"The asynchronous connection or pool to the Postgres database.\n", - " \n", - " If providing a connection object, please ensure that the connection is open\n", - " and remember to close the connection when done.\n", - " \"\"\"\n", - "\n", - " def __init__(\n", - " self,\n", - " sync_connection: Optional[Union[psycopg.Connection, ConnectionPool]] = None,\n", - " async_connection: Optional[\n", - " Union[psycopg.AsyncConnection, AsyncConnectionPool]\n", - " ] = None,\n", - " ):\n", - " super().__init__(serde=JsonPlusSerializer())\n", - " self.sync_connection = sync_connection\n", - " self.async_connection = async_connection\n", - "\n", - " @contextmanager\n", - " def _get_sync_connection(self) -> Generator[psycopg.Connection, None, None]:\n", - " \"\"\"Get the connection to the Postgres database.\"\"\"\n", - " with _get_sync_connection(self.sync_connection) as connection:\n", - " yield connection\n", - "\n", - " @asynccontextmanager\n", - " async def _get_async_connection(\n", - " self,\n", - " ) -> AsyncGenerator[psycopg.AsyncConnection, None]:\n", - " \"\"\"Get the connection to the Postgres database.\"\"\"\n", - " async with _get_async_connection(self.async_connection) as connection:\n", - " yield connection\n", - "\n", - " CREATE_TABLES_QUERY = \"\"\"\n", - " CREATE TABLE IF NOT EXISTS checkpoints (\n", - " thread_id TEXT NOT NULL,\n", - " thread_ts TEXT NOT NULL,\n", - " parent_ts TEXT,\n", - " checkpoint BYTEA NOT NULL,\n", - " metadata BYTEA NOT NULL,\n", - " PRIMARY KEY (thread_id, thread_ts)\n", - " );\n", - " CREATE TABLE IF NOT EXISTS writes (\n", - " thread_id TEXT NOT NULL,\n", - " thread_ts TEXT NOT NULL,\n", - " task_id TEXT NOT NULL,\n", - " idx INTEGER NOT NULL,\n", - " channel TEXT NOT NULL,\n", - " value BYTEA,\n", - " PRIMARY KEY (thread_id, thread_ts, task_id, idx)\n", - " );\n", - " \"\"\"\n", - "\n", - " @staticmethod\n", - " def create_tables(connection: Union[psycopg.Connection, ConnectionPool], /) -> None:\n", - " \"\"\"Create the schema for the checkpoint saver.\"\"\"\n", - " with _get_sync_connection(connection) as conn:\n", - " with conn.cursor() as cur:\n", - " cur.execute(PostgresSaver.CREATE_TABLES_QUERY)\n", - "\n", - " @staticmethod\n", - " async def acreate_tables(\n", - " connection: Union[psycopg.AsyncConnection, AsyncConnectionPool], /\n", - " ) -> None:\n", - " \"\"\"Create the schema for the checkpoint saver.\"\"\"\n", - " async with _get_async_connection(connection) as conn:\n", - " async with conn.cursor() as cur:\n", - " await cur.execute(PostgresSaver.CREATE_TABLES_QUERY)\n", - "\n", - " @staticmethod\n", - " def drop_tables(connection: psycopg.Connection, /) -> None:\n", - " \"\"\"Drop the table for the checkpoint saver.\"\"\"\n", - " with connection.cursor() as cur:\n", - " cur.execute(\"DROP TABLE IF EXISTS checkpoints, writes;\")\n", - "\n", - " @staticmethod\n", - " async def adrop_tables(connection: psycopg.AsyncConnection, /) -> None:\n", - " \"\"\"Drop the table for the checkpoint saver.\"\"\"\n", - " async with connection.cursor() as cur:\n", - " await cur.execute(\"DROP TABLE IF EXISTS checkpoints, writes;\")\n", - "\n", - " UPSERT_CHECKPOINT_QUERY = \"\"\"\n", - " INSERT INTO checkpoints \n", - " (thread_id, thread_ts, parent_ts, checkpoint, metadata)\n", - " VALUES \n", - " (%s, %s, %s, %s, %s)\n", - " ON CONFLICT (thread_id, thread_ts)\n", - " DO UPDATE SET checkpoint = EXCLUDED.checkpoint,\n", - " metadata = EXCLUDED.metadata;\n", - " \"\"\"\n", - "\n", - " def put(\n", - " self,\n", - " config: RunnableConfig,\n", - " checkpoint: Checkpoint,\n", - " metadata: CheckpointMetadata,\n", - " ) -> RunnableConfig:\n", - " \"\"\"Put the checkpoint for the given configuration.\n", - " Args:\n", - " config: The configuration for the checkpoint.\n", - " A dict with a `configurable` key which is a dict with\n", - " a `thread_id` key and an optional `thread_ts` key.\n", - " For example, { 'configurable': { 'thread_id': 'test_thread' } }\n", - " checkpoint: The checkpoint to persist.\n", - " Returns:\n", - " The RunnableConfig that describes the checkpoint that was just created.\n", - " It'll contain the `thread_id` and `thread_ts` of the checkpoint.\n", - " \"\"\"\n", - " thread_id = config[\"configurable\"][\"thread_id\"]\n", - " parent_ts = config[\"configurable\"].get(\"thread_ts\")\n", - " with self._get_sync_connection() as conn:\n", - " with conn.cursor() as cur:\n", - " cur.execute(\n", - " self.UPSERT_CHECKPOINT_QUERY,\n", - " (\n", - " thread_id,\n", - " checkpoint[\"id\"],\n", - " parent_ts if parent_ts else None,\n", - " self.serde.dumps(checkpoint),\n", - " self.serde.dumps(metadata),\n", - " ),\n", - " )\n", - "\n", - " return {\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": checkpoint[\"id\"],\n", - " },\n", - " }\n", - "\n", - " async def aput(\n", - " self,\n", - " config: RunnableConfig,\n", - " checkpoint: Checkpoint,\n", - " metadata: CheckpointMetadata,\n", - " ) -> RunnableConfig:\n", - " \"\"\"Put the checkpoint for the given configuration.\n", - " Args:\n", - " config: The configuration for the checkpoint.\n", - " A dict with a `configurable` key which is a dict with\n", - " a `thread_id` key and an optional `thread_ts` key.\n", - " For example, { 'configurable': { 'thread_id': 'test_thread' } }\n", - " checkpoint: The checkpoint to persist.\n", - " Returns:\n", - " The RunnableConfig that describes the checkpoint that was just created.\n", - " It'll contain the `thread_id` and `thread_ts` of the checkpoint.\n", - " \"\"\"\n", - " thread_id = config[\"configurable\"][\"thread_id\"]\n", - " parent_ts = config[\"configurable\"].get(\"thread_ts\")\n", - " async with self._get_async_connection() as conn:\n", - " async with conn.cursor() as cur:\n", - " await cur.execute(\n", - " self.UPSERT_CHECKPOINT_QUERY,\n", - " (\n", - " thread_id,\n", - " checkpoint[\"id\"],\n", - " parent_ts if parent_ts else None,\n", - " self.serde.dumps(checkpoint),\n", - " self.serde.dumps(metadata),\n", - " ),\n", - " )\n", - "\n", - " return {\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": checkpoint[\"id\"],\n", - " },\n", - " }\n", - "\n", - " UPSERT_WRITES_QUERY = \"\"\"\n", - " INSERT INTO writes\n", - " (thread_id, thread_ts, task_id, idx, channel, value)\n", - " VALUES\n", - " (%s, %s, %s, %s, %s, %s)\n", - " ON CONFLICT (thread_id, thread_ts, task_id, idx)\n", - " DO UPDATE SET value = EXCLUDED.value;\n", - " \"\"\"\n", - "\n", - " def put_writes(\n", - " self,\n", - " config: RunnableConfig,\n", - " writes: Sequence[Tuple[str, Any]],\n", - " task_id: str,\n", - " ) -> None:\n", - " with self._get_sync_connection() as conn:\n", - " with conn.cursor() as cur:\n", - " cur.executemany(\n", - " self.UPSERT_WRITES_QUERY,\n", - " [\n", - " (\n", - " str(config[\"configurable\"][\"thread_id\"]),\n", - " str(config[\"configurable\"][\"thread_ts\"]),\n", - " task_id,\n", - " idx,\n", - " channel,\n", - " self.serde.dumps(value),\n", - " )\n", - " for idx, (channel, value) in enumerate(writes)\n", - " ],\n", - " )\n", - " conn.commit()\n", - "\n", - " async def aput_writes(\n", - " self,\n", - " config: RunnableConfig,\n", - " writes: Sequence[Tuple[str, Any]],\n", - " task_id: str,\n", - " ) -> None:\n", - " async with self._get_async_connection() as conn:\n", - " async with conn.cursor() as cur:\n", - " await cur.executemany(\n", - " self.UPSERT_WRITES_QUERY,\n", - " [\n", - " (\n", - " str(config[\"configurable\"][\"thread_id\"]),\n", - " str(config[\"configurable\"][\"thread_ts\"]),\n", - " task_id,\n", - " idx,\n", - " channel,\n", - " self.serde.dumps(value),\n", - " )\n", - " for idx, (channel, value) in enumerate(writes)\n", - " ],\n", - " )\n", - " await conn.commit()\n", - "\n", - " LIST_CHECKPOINTS_QUERY_STR = \"\"\"\n", - " SELECT checkpoint, metadata, thread_ts, parent_ts\n", - " FROM checkpoints\n", - " {where}\n", - " ORDER BY thread_ts DESC\n", - " \"\"\"\n", - "\n", - " def list(\n", - " self,\n", - " config: Optional[RunnableConfig],\n", - " *,\n", - " filter: Optional[dict[str, Any]] = None,\n", - " before: Optional[RunnableConfig] = None,\n", - " limit: Optional[int] = None,\n", - " ) -> Generator[CheckpointTuple, None, None]:\n", - " \"\"\"Get all the checkpoints for the given configuration.\"\"\"\n", - " where, args = self._search_where(config, filter, before)\n", - " query = self.LIST_CHECKPOINTS_QUERY_STR.format(where=where)\n", - " if limit:\n", - " query += f\" LIMIT {limit}\"\n", - " with self._get_sync_connection() as conn:\n", - " with conn.cursor() as cur:\n", - " thread_id = config[\"configurable\"][\"thread_id\"]\n", - " cur.execute(query, tuple(args))\n", - " for value in cur:\n", - " checkpoint, metadata, thread_ts, parent_ts = value\n", - " yield CheckpointTuple(\n", - " config={\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " }\n", - " },\n", - " checkpoint=self.serde.loads(checkpoint),\n", - " metadata=self.serde.loads(metadata),\n", - " parent_config={\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " }\n", - " }\n", - " if parent_ts\n", - " else None,\n", - " )\n", - "\n", - " async def alist(\n", - " self,\n", - " config: Optional[RunnableConfig],\n", - " *,\n", - " filter: Optional[dict[str, Any]] = None,\n", - " before: Optional[RunnableConfig] = None,\n", - " limit: Optional[int] = None,\n", - " ) -> AsyncIterator[CheckpointTuple]:\n", - " \"\"\"Get all the checkpoints for the given configuration.\"\"\"\n", - " where, args = self._search_where(config, filter, before)\n", - " query = self.LIST_CHECKPOINTS_QUERY_STR.format(where=where)\n", - " if limit:\n", - " query += f\" LIMIT {limit}\"\n", - " async with self._get_async_connection() as conn:\n", - " async with conn.cursor() as cur:\n", - " thread_id = config[\"configurable\"][\"thread_id\"]\n", - " await cur.execute(query, tuple(args))\n", - " async for value in cur:\n", - " checkpoint, metadata, thread_ts, parent_ts = value\n", - " yield CheckpointTuple(\n", - " config={\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " }\n", - " },\n", - " checkpoint=self.serde.loads(checkpoint),\n", - " metadata=self.serde.loads(metadata),\n", - " parent_config={\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " }\n", - " }\n", - " if parent_ts\n", - " else None,\n", - " )\n", - "\n", - " GET_CHECKPOINT_BY_TS_QUERY = \"\"\"\n", - " SELECT checkpoint, metadata, thread_ts, parent_ts\n", - " FROM checkpoints\n", - " WHERE thread_id = %(thread_id)s AND thread_ts = %(thread_ts)s\n", - " \"\"\"\n", - "\n", - " GET_CHECKPOINT_QUERY = \"\"\"\n", - " SELECT checkpoint, metadata, thread_ts, parent_ts\n", - " FROM checkpoints\n", - " WHERE thread_id = %(thread_id)s\n", - " ORDER BY thread_ts DESC LIMIT 1\n", - " \"\"\"\n", - "\n", - " def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n", - " \"\"\"Get the checkpoint tuple for the given configuration.\n", - " Args:\n", - " config: The configuration for the checkpoint.\n", - " A dict with a `configurable` key which is a dict with\n", - " a `thread_id` key and an optional `thread_ts` key.\n", - " For example, { 'configurable': { 'thread_id': 'test_thread' } }\n", - " Returns:\n", - " The checkpoint tuple for the given configuration if it exists,\n", - " otherwise None.\n", - " If thread_ts is None, the latest checkpoint is returned if it exists.\n", - " \"\"\"\n", - " thread_id = config[\"configurable\"][\"thread_id\"]\n", - " thread_ts = config[\"configurable\"].get(\"thread_ts\")\n", - " with self._get_sync_connection() as conn:\n", - " with conn.cursor() as cur:\n", - " # find the latest checkpoint for the thread_id\n", - " if thread_ts:\n", - " cur.execute(\n", - " self.GET_CHECKPOINT_BY_TS_QUERY,\n", - " {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " },\n", - " )\n", - " else:\n", - " cur.execute(\n", - " self.GET_CHECKPOINT_QUERY,\n", - " {\n", - " \"thread_id\": thread_id,\n", - " },\n", - " )\n", - "\n", - " # if a checkpoint is found, return it\n", - " if value := cur.fetchone():\n", - " checkpoint, metadata, thread_ts, parent_ts = value\n", - " if not config[\"configurable\"].get(\"thread_ts\"):\n", - " config = {\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " }\n", - " }\n", - "\n", - " # find any pending writes\n", - " cur.execute(\n", - " \"SELECT task_id, channel, value FROM writes WHERE thread_id = %(thread_id)s AND thread_ts = %(thread_ts)s\",\n", - " {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " },\n", - " )\n", - " # deserialize the checkpoint and metadata\n", - " return CheckpointTuple(\n", - " config=config,\n", - " checkpoint=self.serde.loads(checkpoint),\n", - " metadata=self.serde.loads(metadata),\n", - " parent_config={\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": parent_ts,\n", - " }\n", - " }\n", - " if parent_ts\n", - " else None,\n", - " pending_writes=[\n", - " (task_id, channel, self.serde.loads(value))\n", - " for task_id, channel, value in cur\n", - " ],\n", - " )\n", - "\n", - " async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n", - " \"\"\"Get the checkpoint tuple for the given configuration.\n", - " Args:\n", - " config: The configuration for the checkpoint.\n", - " A dict with a `configurable` key which is a dict with\n", - " a `thread_id` key and an optional `thread_ts` key.\n", - " For example, { 'configurable': { 'thread_id': 'test_thread' } }\n", - " Returns:\n", - " The checkpoint tuple for the given configuration if it exists,\n", - " otherwise None.\n", - " If thread_ts is None, the latest checkpoint is returned if it exists.\n", - " \"\"\"\n", - " thread_id = config[\"configurable\"][\"thread_id\"]\n", - " thread_ts = config[\"configurable\"].get(\"thread_ts\")\n", - " async with self._get_async_connection() as conn:\n", - " async with conn.cursor() as cur:\n", - " # find the latest checkpoint for the thread_id\n", - " if thread_ts:\n", - " await cur.execute(\n", - " self.GET_CHECKPOINT_BY_TS_QUERY,\n", - " {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " },\n", - " )\n", - " else:\n", - " await cur.execute(\n", - " self.GET_CHECKPOINT_QUERY,\n", - " {\n", - " \"thread_id\": thread_id,\n", - " },\n", - " )\n", - " # if a checkpoint is found, return it\n", - " if value := await cur.fetchone():\n", - " checkpoint, metadata, thread_ts, parent_ts = value\n", - " if not config[\"configurable\"].get(\"thread_ts\"):\n", - " config = {\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " }\n", - " }\n", - "\n", - " # find any pending writes\n", - " await cur.execute(\n", - " \"SELECT task_id, channel, value FROM writes WHERE thread_id = %(thread_id)s AND thread_ts = %(thread_ts)s\",\n", - " {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": thread_ts,\n", - " },\n", - " )\n", - " # deserialize the checkpoint and metadata\n", - " return CheckpointTuple(\n", - " config=config,\n", - " checkpoint=self.serde.loads(checkpoint),\n", - " metadata=self.serde.loads(metadata),\n", - " parent_config={\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"thread_ts\": parent_ts,\n", - " }\n", - " }\n", - " if parent_ts\n", - " else None,\n", - " pending_writes=[\n", - " (task_id, channel, self.serde.loads(value))\n", - " async for task_id, channel, value in cur\n", - " ],\n", - " )\n", - "\n", - " def _search_where(\n", - " self,\n", - " config: Optional[RunnableConfig],\n", - " filter: Optional[dict[str, Any]] = None,\n", - " before: Optional[RunnableConfig] = None,\n", - " ) -> Tuple[str, List[Any]]:\n", - " \"\"\"Return WHERE clause predicates for given config, filter, and before parameters.\n", - " Args:\n", - " config (Optional[RunnableConfig]): The config to use for filtering.\n", - " filter (Optional[Dict[str, Any]]): Additional filtering criteria.\n", - " before (Optional[RunnableConfig]): A config to limit results before a certain timestamp.\n", - " Returns:\n", - " Tuple[str, Sequence[Any]]: A tuple containing the WHERE clause and parameter values.\n", - " \"\"\"\n", - " wheres = []\n", - " param_values = []\n", - "\n", - " # Add predicate for config\n", - " if config is not None:\n", - " wheres.append(\"thread_id = %s \")\n", - " param_values.append(config[\"configurable\"][\"thread_id\"])\n", - "\n", - " if filter:\n", - " raise NotImplementedError()\n", - "\n", - " # Add predicate for limiting results before a certain timestamp\n", - " if before is not None:\n", - " wheres.append(\"thread_ts < %s\")\n", - " param_values.append(before[\"configurable\"][\"thread_ts\"])\n", - "\n", - " where_clause = \"WHERE \" + \" AND \".join(wheres) if wheres else \"\"\n", - " return where_clause, param_values" - ] - }, - { - "cell_type": "markdown", - "id": "456fa19c-93a5-4750-a410-f2d810b964ad", - "metadata": {}, - "source": [ - "## Setup environment" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "eca9aafb-a155-407a-8036-682a2f1297d7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "OPENAI_API_KEY: ········\n" - ] - } - ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "id": "e26b3204-cca2-414c-800e-7e09032445ae", - "metadata": {}, - "source": [ - "## Setup model and tools for the graph" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "e5213193-5a7d-43e7-aeba-fe732bb1cd7a", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Literal\n", - "from langchain_core.runnables import ConfigurableField\n", - "from langchain_core.tools import tool\n", - "from langchain_openai import ChatOpenAI\n", - "from langgraph.prebuilt import create_react_agent\n", - "\n", - "\n", - "@tool\n", - "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", - " \"\"\"Use this to get weather information.\"\"\"\n", - " if city == \"nyc\":\n", - " return \"It might be cloudy in nyc\"\n", - " elif city == \"sf\":\n", - " return \"It's always sunny in sf\"\n", - " else:\n", - " raise AssertionError(\"Unknown city\")\n", - "\n", - "\n", - "tools = [get_weather]\n", - "model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)" - ] - }, - { - "cell_type": "markdown", - "id": "e9342c62-dbb4-40f6-9271-7393f1ca48c4", - "metadata": {}, - "source": [ - "## Use sync connection" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "2b9d13b1-9d72-48a0-b63a-adc062c06c29", - "metadata": {}, - "outputs": [], - "source": [ - "DB_URI = \"postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable\"" - ] - }, - { - "cell_type": "markdown", - "id": "e39fc712-9e1c-4831-9077-dd07b0c13594", - "metadata": {}, - "source": [ - "### With a connection pool" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "2657c1c4-d8a5-4fe3-8f77-95415a98ed6c", - "metadata": {}, - "outputs": [], - "source": [ - "from psycopg_pool import ConnectionPool\n", - "\n", - "pool = ConnectionPool(\n", - " # Example configuration\n", - " conninfo=DB_URI,\n", - " max_size=20,\n", - ")\n", - "\n", - "checkpointer = PostgresSaver(sync_connection=pool)\n", - "checkpointer.create_tables(pool)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "6d388241-de57-4b4e-af7b-eb1081fb8f36", - "metadata": {}, - "outputs": [], - "source": [ - "graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a7e0e7ec-a675-470b-9270-e4bdc59d4a4d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='bc87fac7-1da1-4818-a43b-6ba7c9b9b3e4'),\n", - " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b9de0cab-f310-4f74-897e-97014072c001-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n", - " ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='8d8f9596-a683-4644-a898-1e303b5a01ea', tool_call_id='call_MjkmibJlXeuNchL6B8qpIjOW'),\n", - " AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-4b5282a3-e7a6-42ee-ad0f-e6013a745a88-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "res" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "96efd8b2-97c9-4207-83b2-00131723a75a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'v': 1,\n", - " 'ts': '2024-07-12T15:21:51.891852+00:00',\n", - " 'id': '1ef40627-6fb2-6962-8003-b74d816658c5',\n", - " 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='bc87fac7-1da1-4818-a43b-6ba7c9b9b3e4'),\n", - " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b9de0cab-f310-4f74-897e-97014072c001-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n", - " ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='8d8f9596-a683-4644-a898-1e303b5a01ea', tool_call_id='call_MjkmibJlXeuNchL6B8qpIjOW'),\n", - " AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-4b5282a3-e7a6-42ee-ad0f-e6013a745a88-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})],\n", - " 'agent': 'agent'},\n", - " 'channel_versions': {'__start__': 2,\n", - " 'messages': 5,\n", - " 'start:agent': 3,\n", - " 'agent': 5,\n", - " 'branch:agent:should_continue:tools': 4,\n", - " 'tools': 5},\n", - " 'versions_seen': {'__start__': {'__start__': 1},\n", - " 'agent': {'start:agent': 3, 'tools': 4},\n", - " 'tools': {'branch:agent:should_continue:tools': 3}},\n", - " 'pending_sends': []}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "checkpointer.get(config)" - ] - }, - { - "cell_type": "markdown", - "id": "967c95c7-e392-4819-bd71-f29e91c68df3", - "metadata": {}, - "source": [ - "### With a connection" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "180d6daf-8fa7-4608-bd2e-bfbf44ed5836", - "metadata": {}, - "outputs": [], - "source": [ - "from psycopg import Connection\n", - "\n", - "with Connection.connect(DB_URI) as conn:\n", - " checkpointer = PostgresSaver(sync_connection=conn)\n", - "\n", - " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", - " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", - "\n", - " checkpoint_tuple = checkpointer.get_tuple(config)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "613d0bbc-0e38-45c4-aace-1f6f7ae27c7b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "CheckpointTuple(config={'configurable': {'thread_id': '2', 'thread_ts': '1ef40627-7d58-6422-8003-de6e83a8c293'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:53.322868+00:00', 'id': '1ef40627-7d58-6422-8003-de6e83a8c293', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='8d0209ed-a8c2-42ae-8e77-cc71a9cca29d'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_BO4zHHp0JkEWtrtaEqFHkDjK', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-1f76b350-6a33-4de7-9276-59725b1ac101-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_BO4zHHp0JkEWtrtaEqFHkDjK', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='c1bb1a24-62a8-4b43-b90e-b00899c112a8', tool_call_id='call_BO4zHHp0JkEWtrtaEqFHkDjK'), AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-7576d437-4938-48b9-b2cf-e4809d92742d-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-7576d437-4938-48b9-b2cf-e4809d92742d-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}}, parent_config={'configurable': {'thread_id': '2', 'thread_ts': '1ef40627-775a-6746-8002-a3967bf0eae6'}}, pending_writes=[])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "checkpoint_tuple" - ] - }, - { - "cell_type": "markdown", - "id": "c0a47d3e-e588-48fc-a5d4-2145dff17e77", - "metadata": {}, - "source": [ - "## Use async connection" - ] - }, - { - "cell_type": "markdown", - "id": "ee6b6cf7-d8f7-4777-a48d-93b5855fe681", - "metadata": {}, - "source": [ - "### With a connection pool" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "20cea8b7-8f13-4dc7-a3c9-825040eb4c57", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/vadymbarda/.virtualenvs/langgraph-postgres/lib/python3.11/site-packages/psycopg_pool/pool_async.py:138: RuntimeWarning: opening the async pool AsyncConnectionPool in the constructor is deprecated and will not be supported anymore in a future release. Please use `await pool.open()`, or use the pool as context manager using: `async with AsyncConnectionPool(...) as pool: `...\n", - " warnings.warn(\n" - ] - } - ], - "source": [ - "from psycopg_pool import AsyncConnectionPool\n", - "\n", - "pool = AsyncConnectionPool(\n", - " # Example configuration\n", - " conninfo=DB_URI,\n", - " max_size=20,\n", - ")\n", - "\n", - "checkpointer = PostgresSaver(async_connection=pool)\n", - "await checkpointer.acreate_tables(pool)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "f889dce6-7ec1-4277-b8af-ace7811733fa", - "metadata": {}, - "outputs": [], - "source": [ - "graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", - "config = {\"configurable\": {\"thread_id\": \"3\"}}\n", - "res = await graph.ainvoke(\n", - " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "ed58c722-1662-4ae2-9bb7-4872158a5b29", - "metadata": {}, - "outputs": [], - "source": [ - "checkpoint_tuple = await checkpointer.aget_tuple(config)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "e0c42044-4de6-4742-8e00-fe295d50c95a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "CheckpointTuple(config={'configurable': {'thread_id': '3', 'thread_ts': '1ef40627-8b0e-6b02-8003-68a7a04ea6a5'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:54.760751+00:00', 'id': '1ef40627-8b0e-6b02-8003-68a7a04ea6a5', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='108ac72d-f658-4ae0-af57-af481adc8aa5'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_XY5TyZEwF5nbdNTWjjiqGtdS', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-454e2142-6f18-4676-ac4b-91f89ea7a6d4-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_XY5TyZEwF5nbdNTWjjiqGtdS', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='2d86514f-b8f0-439b-ab94-68c731309c63', tool_call_id='call_XY5TyZEwF5nbdNTWjjiqGtdS'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-90ae3622-b480-4964-b689-9c1a572112f1-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-90ae3622-b480-4964-b689-9c1a572112f1-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '3', 'thread_ts': '1ef40627-860c-63d4-8002-49a92ae87052'}}, pending_writes=[])" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "checkpoint_tuple" - ] - }, - { - "cell_type": "markdown", - "id": "56552584-9eb8-40df-a6a0-44151018b509", - "metadata": {}, - "source": [ - "### Use connection" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "386b78bc-2f73-49ba-a2a4-47bce6fc49b7", - "metadata": {}, - "outputs": [], - "source": [ - "from psycopg import AsyncConnection\n", - "\n", - "async with await AsyncConnection.connect(DB_URI) as conn:\n", - " checkpointer = PostgresSaver(async_connection=conn)\n", - " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", - " config = {\"configurable\": {\"thread_id\": \"4\"}}\n", - " res = await graph.ainvoke(\n", - " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", - " )\n", - " checkpoint_tuples = [c async for c in checkpointer.alist(config)]" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "d1ed1344-c923-4a46-b04e-cc3646737d48", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-96b9-6682-8003-134aebfec1e9'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:55.984109+00:00', 'id': '1ef40627-96b9-6682-8003-134aebfec1e9', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='50e612d7-c770-44dd-b128-4bfdbd7d5b7d', tool_call_id='call_pS4ybOXkIDOmS93jZ8wOYGfU'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-924e10c9-7005-4cbf-a92e-3ce63b54092f-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-924e10c9-7005-4cbf-a92e-3ce63b54092f-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-96b9-6682-8003-134aebfec1e9'}}, pending_writes=None),\n", - " CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-91a0-6100-8002-e404dda477d4'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:55.449447+00:00', 'id': '1ef40627-91a0-6100-8002-e404dda477d4', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='50e612d7-c770-44dd-b128-4bfdbd7d5b7d', tool_call_id='call_pS4ybOXkIDOmS93jZ8wOYGfU')], 'tools': 'tools'}, 'channel_versions': {'__start__': 2, 'messages': 4, 'start:agent': 3, 'agent': 4, 'branch:agent:should_continue:tools': 4, 'tools': 4}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 2}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 2, 'writes': {'tools': {'messages': [ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='50e612d7-c770-44dd-b128-4bfdbd7d5b7d', tool_call_id='call_pS4ybOXkIDOmS93jZ8wOYGfU')]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-91a0-6100-8002-e404dda477d4'}}, pending_writes=None),\n", - " CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-9194-66de-8001-86c8d77c2d7c'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:55.444687+00:00', 'id': '1ef40627-9194-66de-8001-86c8d77c2d7c', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})], 'agent': 'agent', 'branch:agent:should_continue:tools': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 3, 'start:agent': 3, 'agent': 3, 'branch:agent:should_continue:tools': 3}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 2}, 'tools': {}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 1, 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-9194-66de-8001-86c8d77c2d7c'}}, pending_writes=None),\n", - " CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-8b8a-6b1c-8000-55b423aa733b'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:54.811566+00:00', 'id': '1ef40627-8b8a-6b1c-8000-55b423aa733b', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579')], 'start:agent': '__start__'}, 'channel_versions': {'__start__': 2, 'messages': 2, 'start:agent': 2}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {}, 'tools': {}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 0, 'writes': None}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-8b8a-6b1c-8000-55b423aa733b'}}, pending_writes=None),\n", - " CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-8b88-62b8-bfff-9922bbf9342b'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:54.810527+00:00', 'id': '1ef40627-8b88-62b8-bfff-9922bbf9342b', 'channel_values': {'messages': [], '__start__': {'messages': [['human', \"what's the weather in nyc\"]]}}, 'channel_versions': {'__start__': 1}, 'versions_seen': {}, 'pending_sends': []}, metadata={'source': 'input', 'step': -1, 'writes': {'messages': [['human', \"what's the weather in nyc\"]]}}, parent_config=None, pending_writes=None)]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "checkpoint_tuples" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6a39d1ff-ca37-4457-8b52-07d33b59c36e", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "langgraph-postgres", - "language": "python", - "name": "langgraph-postgres" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to use Postgres checkpointer for persistence\n", + "\n", + "When creating LangGraph agents, you can also set them up so that they persist their state. This allows you to do things like interact with an agent multiple times and have it remember previous interactions.\n", + "\n", + "This example shows how to use `Postgres` as the backend for persisting checkpoint state using [`langgraph-checkpoint-postgres`](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres) library." + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "markdown", + "id": "456fa19c-93a5-4750-a410-f2d810b964ad", + "metadata": {}, + "source": [ + "## Setup environment" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "faadfb1b-cebe-4dcf-82fd-34044c380bc4", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U psycopg psycopg-pool langgraph langgraph-checkpoint-postgres" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eca9aafb-a155-407a-8036-682a2f1297d7", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "OPENAI_API_KEY: ········\n" + ] + } + ], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "e26b3204-cca2-414c-800e-7e09032445ae", + "metadata": {}, + "source": [ + "## Setup model and tools for the graph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e5213193-5a7d-43e7-aeba-fe732bb1cd7a", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Literal\n", + "from langchain_core.runnables import ConfigurableField\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "from langgraph.prebuilt import create_react_agent\n", + "from langgraph.checkpoint.postgres import PostgresSaver\n", + "from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver\n", + "\n", + "\n", + "@tool\n", + "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", + " \"\"\"Use this to get weather information.\"\"\"\n", + " if city == \"nyc\":\n", + " return \"It might be cloudy in nyc\"\n", + " elif city == \"sf\":\n", + " return \"It's always sunny in sf\"\n", + " else:\n", + " raise AssertionError(\"Unknown city\")\n", + "\n", + "\n", + "tools = [get_weather]\n", + "model = ChatOpenAI(model_name=\"gpt-4o-mini\", temperature=0)" + ] + }, + { + "cell_type": "markdown", + "id": "e9342c62-dbb4-40f6-9271-7393f1ca48c4", + "metadata": {}, + "source": [ + "## Use sync connection" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2b9d13b1-9d72-48a0-b63a-adc062c06c29", + "metadata": {}, + "outputs": [], + "source": [ + "DB_URI = \"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3fe36f67-073a-4fd7-a8f8-da196dd46a0d", + "metadata": {}, + "outputs": [], + "source": [ + "from psycopg.rows import dict_row\n", + "\n", + "connection_kwargs ={\n", + " \"autocommit\": True,\n", + " \"prepare_threshold\": 0,\n", + " \"row_factory\": dict_row,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "e39fc712-9e1c-4831-9077-dd07b0c13594", + "metadata": {}, + "source": [ + "### With a connection pool" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "bd235fc7-1e5c-4db6-a90b-ea75462ccf7d", + "metadata": {}, + "outputs": [], + "source": [ + "from psycopg_pool import ConnectionPool\n", + "\n", + "pool = ConnectionPool(\n", + " # Example configuration\n", + " conninfo=DB_URI,\n", + " max_size=20,\n", + " kwargs=connection_kwargs\n", + ")\n", + "\n", + "with pool.connection() as conn:\n", + " checkpointer = PostgresSaver(conn)\n", + "\n", + " # NOTE: you need to call .setup() the first time you're using your checkpointer\n", + " checkpointer.setup()\n", + "\n", + " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", + " config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", + " checkpoint = checkpointer.get(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a7e0e7ec-a675-470b-9270-e4bdc59d4a4d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='735b7deb-b0fe-4ad5-8920-2a3c69bbe9f7'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_lJHMDYgfgRdiEAGfFsEhqqKV', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-c56b3e04-08a9-4a59-b3f5-ee52d0ef0656-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_lJHMDYgfgRdiEAGfFsEhqqKV', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n", + " ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='0644bf7b-4d1b-4ebe-afa1-d2169ccce582', tool_call_id='call_lJHMDYgfgRdiEAGfFsEhqqKV'),\n", + " AIMessage(content='The weather in San Francisco is always sunny!', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-1ed9b8d0-9b50-4b87-b3a2-9860f51e9fd1-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "96efd8b2-97c9-4207-83b2-00131723a75a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'v': 1,\n", + " 'id': '1ef559b7-3b19-6ce8-8003-18d0f60634be',\n", + " 'ts': '2024-08-08T15:32:42.108605+00:00',\n", + " 'current_tasks': {},\n", + " 'pending_sends': [],\n", + " 'versions_seen': {'agent': {'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8',\n", + " 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'},\n", + " 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'},\n", + " '__input__': {},\n", + " '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}},\n", + " 'channel_versions': {'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af',\n", + " 'tools': '00000000000000000000000000000005.',\n", + " 'messages': '00000000000000000000000000000005.b9adc75836c78af94af1d6811340dd13',\n", + " '__start__': '00000000000000000000000000000002.',\n", + " 'start:agent': '00000000000000000000000000000003.',\n", + " 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'},\n", + " 'channel_values': {'agent': 'agent',\n", + " 'messages': [HumanMessage(content=\"what's the weather in sf\", id='735b7deb-b0fe-4ad5-8920-2a3c69bbe9f7'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_lJHMDYgfgRdiEAGfFsEhqqKV', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-c56b3e04-08a9-4a59-b3f5-ee52d0ef0656-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_lJHMDYgfgRdiEAGfFsEhqqKV', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n", + " ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='0644bf7b-4d1b-4ebe-afa1-d2169ccce582', tool_call_id='call_lJHMDYgfgRdiEAGfFsEhqqKV'),\n", + " AIMessage(content='The weather in San Francisco is always sunny!', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-1ed9b8d0-9b50-4b87-b3a2-9860f51e9fd1-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint" + ] + }, + { + "cell_type": "markdown", + "id": "967c95c7-e392-4819-bd71-f29e91c68df3", + "metadata": {}, + "source": [ + "### With a connection" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "180d6daf-8fa7-4608-bd2e-bfbf44ed5836", + "metadata": {}, + "outputs": [], + "source": [ + "from psycopg import Connection\n", + "\n", + "\n", + "with Connection.connect(DB_URI, **connection_kwargs) as conn:\n", + " checkpointer = PostgresSaver(conn)\n", + " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", + " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", + "\n", + " checkpoint_tuple = checkpointer.get_tuple(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "613d0bbc-0e38-45c4-aace-1f6f7ae27c7b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-4650-6bfc-8003-1c5488f19318'}}, checkpoint={'v': 1, 'id': '1ef559b7-4650-6bfc-8003-1c5488f19318', 'ts': '2024-08-08T15:32:43.284551+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}}, 'channel_versions': {'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'tools': '00000000000000000000000000000005.', 'messages': '00000000000000000000000000000005.af9f229d2c4e14f4866eb37f72ec39f6', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'}, 'channel_values': {'agent': 'agent', 'messages': [HumanMessage(content=\"what's the weather in sf\", id='7a14f96c-2d88-454f-9520-0e0287a4abbb'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_NcL4dBTYu4kSPGMKdxztdpjN', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-39adbf2c-36ef-40f6-9cad-8e1f8167fc19-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_NcL4dBTYu4kSPGMKdxztdpjN', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='c9f82354-3225-40a8-bf54-81f3e199043b', tool_call_id='call_NcL4dBTYu4kSPGMKdxztdpjN'), AIMessage(content='The weather in San Francisco is always sunny!', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-83888be3-d681-42ca-ad67-e2f5ee8550de-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}, metadata={'step': 3, 'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='The weather in San Francisco is always sunny!', response_metadata={'logprobs': None, 'model_name': 'gpt-4o-mini-2024-07-18', 'token_usage': {'total_tokens': 94, 'prompt_tokens': 84, 'completion_tokens': 10}, 'finish_reason': 'stop', 'system_fingerprint': 'fp_48196bc67a'}, id='run-83888be3-d681-42ca-ad67-e2f5ee8550de-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}}, parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-4087-681a-8002-88a5738f76f1'}}, pending_writes=[])" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint_tuple" + ] + }, + { + "cell_type": "markdown", + "id": "49fb52fd-af31-4603-889d-66d783244bce", + "metadata": {}, + "source": [ + "### With a connection string" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5fe54e79-9eaf-44e2-b2d9-1e0284b984d0", + "metadata": {}, + "outputs": [], + "source": [ + "from psycopg import Connection\n", + "\n", + "with PostgresSaver.from_conn_string(DB_URI) as checkpointer:\n", + " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", + " config = {\"configurable\": {\"thread_id\": \"3\"}}\n", + " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", + "\n", + " checkpoint_tuples = list(checkpointer.list(config))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b2ce743b-5896-443b-9ec0-a655b065895c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[CheckpointTuple(config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-5024-6476-8003-cf0a750e6b37'}}, checkpoint={'v': 1, 'id': '1ef559b7-5024-6476-8003-cf0a750e6b37', 'ts': '2024-08-08T15:32:44.314900+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}}, 'channel_versions': {'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'tools': '00000000000000000000000000000005.', 'messages': '00000000000000000000000000000005.3f8b8d9923575b911e17157008ab75ac', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'}, 'channel_values': {'agent': 'agent', 'messages': [HumanMessage(content=\"what's the weather in sf\", id='5bf79d15-6332-4bf5-89bd-ee192b31ed84'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_507c9469a1', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2958adc7-f6a4-415d-ade1-5ee77e0b9276-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='cac4f90a-dc3e-4bfa-940f-1c630289a583', tool_call_id='call_9y3q1BiwW7zGh2gk2faInTRk'), AIMessage(content='The weather in San Francisco is always sunny!', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-97d3fb7a-3d2e-4090-84f4-dafdfe44553f-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}, metadata={'step': 3, 'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='The weather in San Francisco is always sunny!', response_metadata={'logprobs': None, 'model_name': 'gpt-4o-mini-2024-07-18', 'token_usage': {'total_tokens': 94, 'prompt_tokens': 84, 'completion_tokens': 10}, 'finish_reason': 'stop', 'system_fingerprint': 'fp_48196bc67a'}, id='run-97d3fb7a-3d2e-4090-84f4-dafdfe44553f-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}}, parent_config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-4b3d-6430-8002-b5c99d2eb4db'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-4b3d-6430-8002-b5c99d2eb4db'}}, checkpoint={'v': 1, 'id': '1ef559b7-4b3d-6430-8002-b5c99d2eb4db', 'ts': '2024-08-08T15:32:43.800857+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}}, 'channel_versions': {'agent': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8', 'messages': '00000000000000000000000000000004.1195f50946feaedb0bae1fdbfadc806b', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'}, 'channel_values': {'tools': 'tools', 'messages': [HumanMessage(content=\"what's the weather in sf\", id='5bf79d15-6332-4bf5-89bd-ee192b31ed84'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_507c9469a1', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2958adc7-f6a4-415d-ade1-5ee77e0b9276-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='cac4f90a-dc3e-4bfa-940f-1c630289a583', tool_call_id='call_9y3q1BiwW7zGh2gk2faInTRk')]}}, metadata={'step': 2, 'source': 'loop', 'writes': {'tools': {'messages': [ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='cac4f90a-dc3e-4bfa-940f-1c630289a583', tool_call_id='call_9y3q1BiwW7zGh2gk2faInTRk')]}}}, parent_config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-4b30-6078-8001-eaf8c9bd8844'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-4b30-6078-8001-eaf8c9bd8844'}}, checkpoint={'v': 1, 'id': '1ef559b7-4b30-6078-8001-eaf8c9bd8844', 'ts': '2024-08-08T15:32:43.795440+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}}, 'channel_versions': {'agent': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af', 'messages': '00000000000000000000000000000003.bab5fb3a70876f600f5f2fd46945ce5f', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, 'channel_values': {'agent': 'agent', 'messages': [HumanMessage(content=\"what's the weather in sf\", id='5bf79d15-6332-4bf5-89bd-ee192b31ed84'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_507c9469a1', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2958adc7-f6a4-415d-ade1-5ee77e0b9276-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})], 'branch:agent:should_continue:tools': 'agent'}}, metadata={'step': 1, 'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'type': 'function', 'function': {'name': 'get_weather', 'arguments': '{\"city\":\"sf\"}'}}]}, response_metadata={'logprobs': None, 'model_name': 'gpt-4o-mini-2024-07-18', 'token_usage': {'total_tokens': 71, 'prompt_tokens': 57, 'completion_tokens': 14}, 'finish_reason': 'tool_calls', 'system_fingerprint': 'fp_507c9469a1'}, id='run-2958adc7-f6a4-415d-ade1-5ee77e0b9276-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_9y3q1BiwW7zGh2gk2faInTRk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})]}}}, parent_config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-46d7-6116-8000-8976b7c89a2f'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-46d7-6116-8000-8976b7c89a2f'}}, checkpoint={'v': 1, 'id': '1ef559b7-46d7-6116-8000-8976b7c89a2f', 'ts': '2024-08-08T15:32:43.339573+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}}, 'channel_versions': {'messages': '00000000000000000000000000000002.ba0c90d32863686481f7fe5eab9ecdf0', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='5bf79d15-6332-4bf5-89bd-ee192b31ed84')], 'start:agent': '__start__'}}, metadata={'step': 0, 'source': 'loop', 'writes': None}, parent_config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-46ce-6c64-bfff-ef7fe2663573'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '3', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-46ce-6c64-bfff-ef7fe2663573'}}, checkpoint={'v': 1, 'id': '1ef559b7-46ce-6c64-bfff-ef7fe2663573', 'ts': '2024-08-08T15:32:43.336188+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'__input__': {}}, 'channel_versions': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}, 'channel_values': {'__start__': {'messages': [['human', \"what's the weather in sf\"]]}}}, metadata={'step': -1, 'source': 'input', 'writes': {'messages': [['human', \"what's the weather in sf\"]]}}, parent_config=None, pending_writes=None)]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint_tuples" + ] + }, + { + "cell_type": "markdown", + "id": "c0a47d3e-e588-48fc-a5d4-2145dff17e77", + "metadata": {}, + "source": [ + "## Use async connection" + ] + }, + { + "cell_type": "markdown", + "id": "ee6b6cf7-d8f7-4777-a48d-93b5855fe681", + "metadata": {}, + "source": [ + "### With a connection pool" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4faf6087-73cc-4957-9a4f-f3509a32a740", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/vadymbarda/.virtualenvs/langgraph-postgres/lib/python3.11/site-packages/psycopg_pool/pool_async.py:138: RuntimeWarning: opening the async pool AsyncConnectionPool in the constructor is deprecated and will not be supported anymore in a future release. Please use `await pool.open()`, or use the pool as context manager using: `async with AsyncConnectionPool(...) as pool: `...\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "from psycopg_pool import AsyncConnectionPool\n", + "\n", + "pool = AsyncConnectionPool(\n", + " # Example configuration\n", + " conninfo=DB_URI,\n", + " max_size=20,\n", + " kwargs=connection_kwargs\n", + ")\n", + "\n", + "async with pool.connection() as conn:\n", + " checkpointer = AsyncPostgresSaver(conn)\n", + "\n", + " # NOTE: you need to call .setup() the first time you're using your checkpointer\n", + " # await checkpointer.setup()\n", + "\n", + " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", + " config = {\"configurable\": {\"thread_id\": \"4\"}}\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", + "\n", + " checkpoint = await checkpointer.aget(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "e0c42044-4de6-4742-8e00-fe295d50c95a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'v': 1,\n", + " 'id': '1ef559b7-5cc9-6460-8003-8655824c0944',\n", + " 'ts': '2024-08-08T15:32:45.640793+00:00',\n", + " 'current_tasks': {},\n", + " 'pending_sends': [],\n", + " 'versions_seen': {'agent': {'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8',\n", + " 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'},\n", + " 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'},\n", + " '__input__': {},\n", + " '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}},\n", + " 'channel_versions': {'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af',\n", + " 'tools': '00000000000000000000000000000005.',\n", + " 'messages': '00000000000000000000000000000005.d869fc7231619df0db74feed624efe41',\n", + " '__start__': '00000000000000000000000000000002.',\n", + " 'start:agent': '00000000000000000000000000000003.',\n", + " 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'},\n", + " 'channel_values': {'agent': 'agent',\n", + " 'messages': [HumanMessage(content=\"what's the weather in nyc\", id='d883b8a0-99de-486d-91a2-bcfa7f25dc05'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_H6TAYfyd6AnaCrkQGs6Q2fVp', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-6f542f84-ad73-444c-8ef7-b5ea75a2e09b-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_H6TAYfyd6AnaCrkQGs6Q2fVp', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}),\n", + " ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='c0e52254-77a4-4ea9-a2b7-61dd2d65ec68', tool_call_id='call_H6TAYfyd6AnaCrkQGs6Q2fVp'),\n", + " AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-977140d4-7582-40c3-b2b6-31b542c430a3-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint" + ] + }, + { + "cell_type": "markdown", + "id": "56552584-9eb8-40df-a6a0-44151018b509", + "metadata": {}, + "source": [ + "### With a connection" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "386b78bc-2f73-49ba-a2a4-47bce6fc49b7", + "metadata": {}, + "outputs": [], + "source": [ + "from psycopg import AsyncConnection\n", + "\n", + "async with await AsyncConnection.connect(DB_URI, **connection_kwargs) as conn:\n", + " checkpointer = AsyncPostgresSaver(conn)\n", + " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", + " config = {\"configurable\": {\"thread_id\": \"5\"}}\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", + " checkpoint_tuple = await checkpointer.aget_tuple(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d1ed1344-c923-4a46-b04e-cc3646737d48", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "CheckpointTuple(config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-65b4-60ca-8003-1ef4b620559a'}}, checkpoint={'v': 1, 'id': '1ef559b7-65b4-60ca-8003-1ef4b620559a', 'ts': '2024-08-08T15:32:46.575814+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}}, 'channel_versions': {'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'tools': '00000000000000000000000000000005.', 'messages': '00000000000000000000000000000005.1557a6006d58f736d5cb2dd5c5f10111', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'}, 'channel_values': {'agent': 'agent', 'messages': [HumanMessage(content=\"what's the weather in nyc\", id='935e7732-b288-49bd-9ec2-1f7610cc38cb'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_94KtjtPmsiaj7T8yXvL7Ef31', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-790c929a-7982-49e7-af67-2cbe4a86373b-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_94KtjtPmsiaj7T8yXvL7Ef31', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='b2dc1073-abc4-4492-8982-434a7e32e445', tool_call_id='call_94KtjtPmsiaj7T8yXvL7Ef31'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-7e8a7f16-d8e1-457a-89f3-192102396449-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}, metadata={'step': 3, 'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'logprobs': None, 'model_name': 'gpt-4o-mini-2024-07-18', 'token_usage': {'total_tokens': 97, 'prompt_tokens': 88, 'completion_tokens': 9}, 'finish_reason': 'stop', 'system_fingerprint': 'fp_48196bc67a'}, id='run-7e8a7f16-d8e1-457a-89f3-192102396449-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-62ae-6128-8002-c04af82bcd41'}}, pending_writes=[])" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint_tuple" + ] + }, + { + "cell_type": "markdown", + "id": "2f7e486a-3e63-41d7-b84b-6743f0a5764c", + "metadata": {}, + "source": [ + "### With a connection string" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "6a39d1ff-ca37-4457-8b52-07d33b59c36e", + "metadata": {}, + "outputs": [], + "source": [ + "async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:\n", + " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", + " config = {\"configurable\": {\"thread_id\": \"6\"}}\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", + " checkpoint_tuples = [c async for c in checkpointer.alist(config)]" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "2b6d73ca-519e-45f7-90c2-1b8596624505", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[CheckpointTuple(config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-723c-67de-8003-63bd4eab35af'}}, checkpoint={'v': 1, 'id': '1ef559b7-723c-67de-8003-63bd4eab35af', 'ts': '2024-08-08T15:32:47.890003+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}}, 'channel_versions': {'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'tools': '00000000000000000000000000000005.', 'messages': '00000000000000000000000000000005.b6fe2a26011590cfe8fd6a39151a9e92', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'}, 'channel_values': {'agent': 'agent', 'messages': [HumanMessage(content=\"what's the weather in nyc\", id='977ddb90-9991-44cb-9f73-361c6dd21396'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-47b10c48-4db3-46d8-b4fa-e021818e01c5-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='798c520f-4f9a-4f6d-a389-da721eb4d4ce', tool_call_id='call_QIFCuh4zfP9owpjToycJiZf7'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-4a34e05d-8bcf-41ad-adc3-715919fde64c-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}, metadata={'step': 3, 'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'logprobs': None, 'model_name': 'gpt-4o-mini-2024-07-18', 'token_usage': {'total_tokens': 97, 'prompt_tokens': 88, 'completion_tokens': 9}, 'finish_reason': 'stop', 'system_fingerprint': 'fp_48196bc67a'}, id='run-4a34e05d-8bcf-41ad-adc3-715919fde64c-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-6bf5-63c6-8002-ed990dbbc96e'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-6bf5-63c6-8002-ed990dbbc96e'}}, checkpoint={'v': 1, 'id': '1ef559b7-6bf5-63c6-8002-ed990dbbc96e', 'ts': '2024-08-08T15:32:47.231667+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}}, 'channel_versions': {'agent': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8', 'messages': '00000000000000000000000000000004.c9074f2a41f05486b5efb86353dc75c0', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.'}, 'channel_values': {'tools': 'tools', 'messages': [HumanMessage(content=\"what's the weather in nyc\", id='977ddb90-9991-44cb-9f73-361c6dd21396'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-47b10c48-4db3-46d8-b4fa-e021818e01c5-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='798c520f-4f9a-4f6d-a389-da721eb4d4ce', tool_call_id='call_QIFCuh4zfP9owpjToycJiZf7')]}}, metadata={'step': 2, 'source': 'loop', 'writes': {'tools': {'messages': [ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='798c520f-4f9a-4f6d-a389-da721eb4d4ce', tool_call_id='call_QIFCuh4zfP9owpjToycJiZf7')]}}}, parent_config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-6be0-6926-8001-1a8ce73baf9e'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-6be0-6926-8001-1a8ce73baf9e'}}, checkpoint={'v': 1, 'id': '1ef559b7-6be0-6926-8001-1a8ce73baf9e', 'ts': '2024-08-08T15:32:47.223198+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, '__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}}, 'channel_versions': {'agent': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af', 'messages': '00000000000000000000000000000003.097b5407d709b297591f1ef5d50c8368', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000003.', 'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, 'channel_values': {'agent': 'agent', 'messages': [HumanMessage(content=\"what's the weather in nyc\", id='977ddb90-9991-44cb-9f73-361c6dd21396'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-47b10c48-4db3-46d8-b4fa-e021818e01c5-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})], 'branch:agent:should_continue:tools': 'agent'}}, metadata={'step': 1, 'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'type': 'function', 'function': {'name': 'get_weather', 'arguments': '{\"city\":\"nyc\"}'}}]}, response_metadata={'logprobs': None, 'model_name': 'gpt-4o-mini-2024-07-18', 'token_usage': {'total_tokens': 73, 'prompt_tokens': 58, 'completion_tokens': 15}, 'finish_reason': 'tool_calls', 'system_fingerprint': 'fp_48196bc67a'}, id='run-47b10c48-4db3-46d8-b4fa-e021818e01c5-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_QIFCuh4zfP9owpjToycJiZf7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})]}}}, parent_config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-663d-60b4-8000-10a8922bffbf'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-663d-60b4-8000-10a8922bffbf'}}, checkpoint={'v': 1, 'id': '1ef559b7-663d-60b4-8000-10a8922bffbf', 'ts': '2024-08-08T15:32:46.631935+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}}, 'channel_versions': {'messages': '00000000000000000000000000000002.2a79db8da664e437bdb25ea804457ca7', '__start__': '00000000000000000000000000000002.', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='977ddb90-9991-44cb-9f73-361c6dd21396')], 'start:agent': '__start__'}}, metadata={'step': 0, 'source': 'loop', 'writes': None}, parent_config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-6637-6d4e-bfff-6cecf690c3cb'}}, pending_writes=None),\n", + " CheckpointTuple(config={'configurable': {'thread_id': '6', 'checkpoint_ns': '', 'checkpoint_id': '1ef559b7-6637-6d4e-bfff-6cecf690c3cb'}}, checkpoint={'v': 1, 'id': '1ef559b7-6637-6d4e-bfff-6cecf690c3cb', 'ts': '2024-08-08T15:32:46.629806+00:00', 'current_tasks': {}, 'pending_sends': [], 'versions_seen': {'__input__': {}}, 'channel_versions': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}, 'channel_values': {'__start__': {'messages': [['human', \"what's the weather in nyc\"]]}}}, metadata={'step': -1, 'source': 'input', 'writes': {'messages': [['human', \"what's the weather in nyc\"]]}}, parent_config=None, pending_writes=None)]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint_tuples" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph-postgres", + "language": "python", + "name": "langgraph-postgres" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/poetry.lock b/poetry.lock index f69d09c04..7e00b52aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -968,7 +968,7 @@ typing-extensions = ">=4.7" [[package]] name = "langgraph" -version = "0.1.17" +version = "0.2.2" description = "Building stateful, multi-actor applications with LLMs" optional = false python-versions = ">=3.9.0,<4.0" @@ -977,6 +977,7 @@ develop = true [package.dependencies] langchain-core = ">=0.2.27,<0.3" +langgraph-checkpoint = "^1.0.2" [package.source] type = "directory" @@ -984,7 +985,7 @@ url = "libs/langgraph" [[package]] name = "langgraph-checkpoint" -version = "1.0.0" +version = "1.0.2" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -999,16 +1000,35 @@ type = "directory" url = "libs/checkpoint" [[package]] -name = "langgraph-checkpoint-sqlite" -version = "1.0.0" -description = "Library with a SQLite implementation of LangGraph checkpoint saver." +name = "langgraph-checkpoint-postgres" +version = "1.0.1" +description = "Library with a Postgres implementation of LangGraph checkpoint saver." optional = false python-versions = "^3.9.0,<4.0" files = [] develop = true +[package.dependencies] +langgraph-checkpoint = "^1.0.1" +orjson = ">=3.10.1" +psycopg = {version = ">=3.1.19", extras = ["binary"]} + +[package.source] +type = "directory" +url = "libs/checkpoint-postgres" + +[[package]] +name = "langgraph-checkpoint-sqlite" +version = "1.0.0" +description = "Library with a SQLite implementation of LangGraph checkpoint saver." +optional = false +python-versions = "^3.9.0" +files = [] +develop = true + [package.dependencies] aiosqlite = "^0.20.0" +langgraph-checkpoint = "^1.0.1" [package.source] type = "directory" @@ -1016,7 +1036,7 @@ url = "libs/checkpoint-sqlite" [[package]] name = "langgraph-sdk" -version = "0.1.26" +version = "0.1.27" description = "SDK for interacting with LangGraph API" optional = false python-versions = "^3.9.0,<4.0" @@ -1852,6 +1872,92 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "psycopg" +version = "3.2.1" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +typing-extensions = ">=4.4" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.1)"] +c = ["psycopg-c (==3.2.1)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.6)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.6)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.1" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:1d6833f607f3fc7b22226a9e121235d3b84c0eda1d3caab174673ef698f63788"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d353e028b8f848b9784450fc2abf149d53a738d451eab3ee4c85703438128b9"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34e369891f77d0738e5d25727c307d06d5344948771e5379ea29c76c6d84555"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ab58213cc976a1666f66bc1cb2e602315cd753b7981a8e17237ac2a185bd4a1"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0104a72a17aa84b3b7dcab6c84826c595355bf54bb6ea6d284dcb06d99c6801"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:059cbd4e6da2337e17707178fe49464ed01de867dc86c677b30751755ec1dc51"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:73f9c9b984be9c322b5ec1515b12df1ee5896029f5e72d46160eb6517438659c"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0469c00f24c4bec18c3d2ede124bf62688d88d1b8a5f3c3edc2f61046fe0d7"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:463d55345f73ff391df8177a185ad57b552915ad33f5cc2b31b930500c068b22"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:302b86f92c0d76e99fe1b5c22c492ae519ce8b98b88d37ef74fda4c9e24c6b46"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0879b5d76b7d48678d31278242aaf951bc2d69ca4e4d7cef117e4bbf7bfefda9"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f99e59f8a5f4dcd9cbdec445f3d8ac950a492fc0e211032384d6992ed3c17eb7"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84837e99353d16c6980603b362d0f03302d4b06c71672a6651f38df8a482923d"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ce965caf618061817f66c0906f0452aef966c293ae0933d4fa5a16ea6eaf5bb"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c2007caf3c90f08685c5378e3ceb142bafd5636be7495f7d86ec8a977eaeef"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7a84b5eb194a258116154b2a4ff2962ea60ea52de089508db23a51d3d6b1c7d1"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a42b8f9ab39affcd5249b45cac763ac3cf12df962b67e23fd15a2ee2932afe5"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:788ffc43d7517c13e624c83e0e553b7b8823c9655e18296566d36a829bfb373f"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:21927f41c4d722ae8eb30d62a6ce732c398eac230509af5ba1749a337f8a63e2"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -2723,4 +2829,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e55f683b5c1fc8f540cf53e9f80e180996e199250fbae54954dd7981c389fbec" +content-hash = "8bbefefef5e786ed86783a98ff0809cfaaad26d59b2d83623c269587a3bf5f7a" diff --git a/pyproject.toml b/pyproject.toml index c12faf6b5..e6477dce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ python = "^3.10" langgraph = { path = "libs/langgraph/", develop = true } langgraph-checkpoint = { path = "libs/checkpoint/", develop = true } langgraph-checkpoint-sqlite = { path = "libs/checkpoint-sqlite", develop = true } +langgraph-checkpoint-postgres = { path = "libs/checkpoint-postgres", develop = true } langgraph-sdk = {path = "libs/sdk-py", develop = true} mkdocs = "^1.6.0" mkdocstrings = "^0.25.1"