{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
   "metadata": {},
   "source": [
    "# How to create a custom checkpointer using Redis\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 reference implementation shows how to use Redis as the backend for persisting checkpoint state. Make sure that you have Redis running on port `6379` for going through this guide.\n",
    "\n",
    "NOTE: this is just an reference 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": "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 redis langgraph"
   ]
  },
  {
   "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": "ecb23436-f238-4f8c-a2b7-67c7956121e2",
   "metadata": {},
   "source": [
    "## Checkpointer implementation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "752d570c-a9ad-48eb-a317-adf9fc700803",
   "metadata": {},
   "source": [
    "### Define imports and helper functions"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdea5bf7-4865-46f3-9bec-00147dd79895",
   "metadata": {},
   "source": [
    "First, let's define some imports and shared utilities for both `RedisSaver` and `AsyncRedisSaver`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "61e63348-7d56-4177-90bf-aad7645a707a",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"Implementation of a langgraph checkpoint saver using Redis.\"\"\"\n",
    "from contextlib import asynccontextmanager, contextmanager\n",
    "from typing import (\n",
    "    Any,\n",
    "    AsyncGenerator,\n",
    "    AsyncIterator,\n",
    "    Iterator,\n",
    "    List,\n",
    "    Optional,\n",
    "    Tuple,\n",
    ")\n",
    "\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "from langgraph.checkpoint.base import (\n",
    "    BaseCheckpointSaver,\n",
    "    ChannelVersions,\n",
    "    Checkpoint,\n",
    "    CheckpointMetadata,\n",
    "    CheckpointTuple,\n",
    "    PendingWrite,\n",
    "    get_checkpoint_id,\n",
    ")\n",
    "from langgraph.checkpoint.serde.base import SerializerProtocol\n",
    "from redis import Redis\n",
    "from redis.asyncio import Redis as AsyncRedis\n",
    "\n",
    "REDIS_KEY_SEPARATOR = \":\"\n",
    "\n",
    "\n",
    "# Utilities shared by both RedisSaver and AsyncRedisSaver\n",
    "\n",
    "\n",
    "def _make_redis_checkpoint_key(\n",
    "    thread_id: str, checkpoint_ns: str, checkpoint_id: str\n",
    ") -> str:\n",
    "    return REDIS_KEY_SEPARATOR.join(\n",
    "        [\"checkpoint\", thread_id, checkpoint_ns, checkpoint_id]\n",
    "    )\n",
    "\n",
    "\n",
    "def _make_redis_checkpoint_writes_key(\n",
    "    thread_id: str,\n",
    "    checkpoint_ns: str,\n",
    "    checkpoint_id: str,\n",
    "    task_id: str,\n",
    "    idx: Optional[int],\n",
    ") -> str:\n",
    "    if idx is None:\n",
    "        return REDIS_KEY_SEPARATOR.join(\n",
    "            [\"writes\", thread_id, checkpoint_ns, checkpoint_id, task_id]\n",
    "        )\n",
    "\n",
    "    return REDIS_KEY_SEPARATOR.join(\n",
    "        [\"writes\", thread_id, checkpoint_ns, checkpoint_id, task_id, str(idx)]\n",
    "    )\n",
    "\n",
    "\n",
    "def _parse_redis_checkpoint_key(redis_key: str) -> dict:\n",
    "    namespace, thread_id, checkpoint_ns, checkpoint_id = redis_key.split(\n",
    "        REDIS_KEY_SEPARATOR\n",
    "    )\n",
    "    if namespace != \"checkpoint\":\n",
    "        raise ValueError(\"Expected checkpoint key to start with 'checkpoint'\")\n",
    "\n",
    "    return {\n",
    "        \"thread_id\": thread_id,\n",
    "        \"checkpoint_ns\": checkpoint_ns,\n",
    "        \"checkpoint_id\": checkpoint_id,\n",
    "    }\n",
    "\n",
    "\n",
    "def _parse_redis_checkpoint_writes_key(redis_key: str) -> dict:\n",
    "    namespace, thread_id, checkpoint_ns, checkpoint_id, task_id, idx = redis_key.split(\n",
    "        REDIS_KEY_SEPARATOR\n",
    "    )\n",
    "    if namespace != \"writes\":\n",
    "        raise ValueError(\"Expected checkpoint key to start with 'checkpoint'\")\n",
    "\n",
    "    return {\n",
    "        \"thread_id\": thread_id,\n",
    "        \"checkpoint_ns\": checkpoint_ns,\n",
    "        \"checkpoint_id\": checkpoint_id,\n",
    "        \"task_id\": task_id,\n",
    "        \"idx\": idx,\n",
    "    }\n",
    "\n",
    "\n",
    "def _filter_keys(\n",
    "    keys: List[str], before: Optional[RunnableConfig], limit: Optional[int]\n",
    ") -> list:\n",
    "    \"\"\"Filter and sort Redis keys based on optional criteria.\"\"\"\n",
    "    if before:\n",
    "        keys = [\n",
    "            k\n",
    "            for k in keys\n",
    "            if _parse_redis_checkpoint_key(k.decode())[\"checkpoint_id\"]\n",
    "            < before[\"configurable\"][\"checkpoint_id\"]\n",
    "        ]\n",
    "\n",
    "    keys = sorted(\n",
    "        keys,\n",
    "        key=lambda k: _parse_redis_checkpoint_key(k.decode())[\"checkpoint_id\"],\n",
    "        reverse=True,\n",
    "    )\n",
    "    if limit:\n",
    "        keys = keys[:limit]\n",
    "    return keys\n",
    "\n",
    "\n",
    "def _dump_writes(serde: SerializerProtocol, writes: tuple[str, Any]) -> list[dict]:\n",
    "    \"\"\"Serialize pending writes.\"\"\"\n",
    "    serialized_writes = []\n",
    "    for channel, value in writes:\n",
    "        type_, serialized_value = serde.dumps_typed(value)\n",
    "        serialized_writes.append(\n",
    "            {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
    "        )\n",
    "    return serialized_writes\n",
    "\n",
    "\n",
    "def _load_writes(\n",
    "    serde: SerializerProtocol, task_id_to_data: dict[tuple[str, str], dict]\n",
    ") -> list[PendingWrite]:\n",
    "    \"\"\"Deserialize pending writes.\"\"\"\n",
    "    writes = [\n",
    "        (\n",
    "            task_id,\n",
    "            data[b\"channel\"].decode(),\n",
    "            serde.loads_typed((data[b\"type\"].decode(), data[b\"value\"].decode())),\n",
    "        )\n",
    "        for (task_id, _), data in task_id_to_data.items()\n",
    "    ]\n",
    "    return writes\n",
    "\n",
    "\n",
    "def _parse_redis_checkpoint_data(\n",
    "    serde: SerializerProtocol,\n",
    "    key: str,\n",
    "    data: dict,\n",
    "    pending_writes: Optional[List[PendingWrite]] = None,\n",
    ") -> Optional[CheckpointTuple]:\n",
    "    \"\"\"Parse checkpoint data retrieved from Redis.\"\"\"\n",
    "    if not data:\n",
    "        return None\n",
    "\n",
    "    parsed_key = _parse_redis_checkpoint_key(key)\n",
    "    thread_id = parsed_key[\"thread_id\"]\n",
    "    checkpoint_ns = parsed_key[\"checkpoint_ns\"]\n",
    "    checkpoint_id = parsed_key[\"checkpoint_id\"]\n",
    "    config = {\n",
    "        \"configurable\": {\n",
    "            \"thread_id\": thread_id,\n",
    "            \"checkpoint_ns\": checkpoint_ns,\n",
    "            \"checkpoint_id\": checkpoint_id,\n",
    "        }\n",
    "    }\n",
    "\n",
    "    checkpoint = serde.loads_typed(\n",
    "        (data[b\"type\"].decode(), data[b\"checkpoint\"].decode())\n",
    "    )\n",
    "    metadata = serde.loads(data[b\"metadata\"].decode())\n",
    "    parent_checkpoint_id = data.get(b\"parent_checkpoint_id\", b\"\").decode()\n",
    "    parent_config = (\n",
    "        {\n",
    "            \"configurable\": {\n",
    "                \"thread_id\": thread_id,\n",
    "                \"checkpoint_ns\": checkpoint_ns,\n",
    "                \"checkpoint_id\": parent_checkpoint_id,\n",
    "            }\n",
    "        }\n",
    "        if parent_checkpoint_id\n",
    "        else None\n",
    "    )\n",
    "    return CheckpointTuple(\n",
    "        config=config,\n",
    "        checkpoint=checkpoint,\n",
    "        metadata=metadata,\n",
    "        parent_config=parent_config,\n",
    "        pending_writes=pending_writes,\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "922822a8-f7d2-41ce-bada-206fc125c20c",
   "metadata": {},
   "source": [
    "### RedisSaver"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c216852b-8318-4927-9000-1361d3ca81e8",
   "metadata": {},
   "source": [
    "Below is an implementation of RedisSaver (for synchronous use of graph, i.e. `.invoke()`, `.stream()`). RedisSaver implements four methods that are required for any checkpointer:\n",
    "\n",
    "- `.put` - Store a checkpoint with its configuration and metadata.\n",
    "- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).\n",
    "- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).\n",
    "- `.list` - List checkpoints that match a given configuration and filter criteria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "98c8d65e-eb95-4cbd-8975-d33a52351d03",
   "metadata": {},
   "outputs": [],
   "source": [
    "class RedisSaver(BaseCheckpointSaver):\n",
    "    \"\"\"Redis-based checkpoint saver implementation.\"\"\"\n",
    "\n",
    "    conn: Redis\n",
    "\n",
    "    def __init__(self, conn: Redis):\n",
    "        super().__init__()\n",
    "        self.conn = conn\n",
    "\n",
    "    @classmethod\n",
    "    @contextmanager\n",
    "    def from_conn_info(cls, *, host: str, port: int, db: int) -> Iterator[\"RedisSaver\"]:\n",
    "        conn = None\n",
    "        try:\n",
    "            conn = Redis(host=host, port=port, db=db)\n",
    "            yield RedisSaver(conn)\n",
    "        finally:\n",
    "            if conn:\n",
    "                conn.close()\n",
    "\n",
    "    def put(\n",
    "        self,\n",
    "        config: RunnableConfig,\n",
    "        checkpoint: Checkpoint,\n",
    "        metadata: CheckpointMetadata,\n",
    "        new_versions: ChannelVersions,\n",
    "    ) -> RunnableConfig:\n",
    "        \"\"\"Save a checkpoint to Redis.\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): The config to associate with the checkpoint.\n",
    "            checkpoint (Checkpoint): The checkpoint to save.\n",
    "            metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.\n",
    "            new_versions (ChannelVersions): New channel versions as of this write.\n",
    "\n",
    "        Returns:\n",
    "            RunnableConfig: Updated configuration after storing the checkpoint.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
    "        checkpoint_id = checkpoint[\"id\"]\n",
    "        parent_checkpoint_id = config[\"configurable\"].get(\"checkpoint_id\")\n",
    "        key = _make_redis_checkpoint_key(thread_id, checkpoint_ns, checkpoint_id)\n",
    "\n",
    "        type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)\n",
    "        serialized_metadata = self.serde.dumps(metadata)\n",
    "        data = {\n",
    "            \"checkpoint\": serialized_checkpoint,\n",
    "            \"type\": type_,\n",
    "            \"metadata\": serialized_metadata,\n",
    "            \"parent_checkpoint_id\": parent_checkpoint_id\n",
    "            if parent_checkpoint_id\n",
    "            else \"\",\n",
    "        }\n",
    "        self.conn.hset(key, mapping=data)\n",
    "        return {\n",
    "            \"configurable\": {\n",
    "                \"thread_id\": thread_id,\n",
    "                \"checkpoint_ns\": checkpoint_ns,\n",
    "                \"checkpoint_id\": checkpoint_id,\n",
    "            }\n",
    "        }\n",
    "\n",
    "    def put_writes(\n",
    "        self,\n",
    "        config: RunnableConfig,\n",
    "        writes: List[Tuple[str, Any]],\n",
    "        task_id: str,\n",
    "    ) -> RunnableConfig:\n",
    "        \"\"\"Store intermediate writes linked to a checkpoint.\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): Configuration of the related checkpoint.\n",
    "            writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.\n",
    "            task_id (str): Identifier for the task creating the writes.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
    "        checkpoint_id = config[\"configurable\"][\"checkpoint_id\"]\n",
    "\n",
    "        for idx, data in enumerate(_dump_writes(self.serde, writes)):\n",
    "            key = _make_redis_checkpoint_writes_key(\n",
    "                thread_id, checkpoint_ns, checkpoint_id, task_id, idx\n",
    "            )\n",
    "            self.conn.hset(key, mapping=data)\n",
    "        return config\n",
    "\n",
    "    def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
    "        \"\"\"Get a checkpoint tuple from Redis.\n",
    "\n",
    "        This method retrieves a checkpoint tuple from Redis based on the\n",
    "        provided config. If the config contains a \"checkpoint_id\" key, the checkpoint with\n",
    "        the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint\n",
    "        for the given thread ID is retrieved.\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): The config to use for retrieving the checkpoint.\n",
    "\n",
    "        Returns:\n",
    "            Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_id = get_checkpoint_id(config)\n",
    "        checkpoint_ns = config[\"configurable\"].get(\"checkpoint_ns\", \"\")\n",
    "\n",
    "        checkpoint_key = self._get_checkpoint_key(\n",
    "            self.conn, thread_id, checkpoint_ns, checkpoint_id\n",
    "        )\n",
    "        if not checkpoint_key:\n",
    "            return None\n",
    "\n",
    "        checkpoint_data = self.conn.hgetall(checkpoint_key)\n",
    "\n",
    "        # load pending writes\n",
    "        checkpoint_id = (\n",
    "            checkpoint_id\n",
    "            or _parse_redis_checkpoint_key(checkpoint_key)[\"checkpoint_id\"]\n",
    "        )\n",
    "        writes_key = _make_redis_checkpoint_writes_key(\n",
    "            thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
    "        )\n",
    "        matching_keys = self.conn.keys(pattern=writes_key)\n",
    "        parsed_keys = [\n",
    "            _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
    "        ]\n",
    "        pending_writes = _load_writes(\n",
    "            self.serde,\n",
    "            {\n",
    "                (parsed_key[\"task_id\"], parsed_key[\"idx\"]): self.conn.hgetall(key)\n",
    "                for key, parsed_key in sorted(\n",
    "                    zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
    "                )\n",
    "            },\n",
    "        )\n",
    "        return _parse_redis_checkpoint_data(\n",
    "            self.serde, checkpoint_key, checkpoint_data, pending_writes=pending_writes\n",
    "        )\n",
    "\n",
    "    def list(\n",
    "        self,\n",
    "        config: Optional[RunnableConfig],\n",
    "        *,\n",
    "        # TODO: implement filtering\n",
    "        filter: Optional[dict[str, Any]] = None,\n",
    "        before: Optional[RunnableConfig] = None,\n",
    "        limit: Optional[int] = None,\n",
    "    ) -> Iterator[CheckpointTuple]:\n",
    "        \"\"\"List checkpoints from the database.\n",
    "\n",
    "        This method retrieves a list of checkpoint tuples from Redis based\n",
    "        on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): The config to use for listing the checkpoints.\n",
    "            filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None.\n",
    "            before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.\n",
    "            limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.\n",
    "\n",
    "        Yields:\n",
    "            Iterator[CheckpointTuple]: An iterator of checkpoint tuples.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_ns = config[\"configurable\"].get(\"checkpoint_ns\", \"\")\n",
    "        pattern = _make_redis_checkpoint_key(thread_id, checkpoint_ns, \"*\")\n",
    "\n",
    "        keys = _filter_keys(self.conn.keys(pattern), before, limit)\n",
    "        for key in keys:\n",
    "            data = self.conn.hgetall(key)\n",
    "            if data and b\"checkpoint\" in data and b\"metadata\" in data:\n",
    "                yield _parse_redis_checkpoint_data(self.serde, key.decode(), data)\n",
    "\n",
    "    def _get_checkpoint_key(\n",
    "        self, conn, thread_id: str, checkpoint_ns: str, checkpoint_id: Optional[str]\n",
    "    ) -> Optional[str]:\n",
    "        \"\"\"Determine the Redis key for a checkpoint.\"\"\"\n",
    "        if checkpoint_id:\n",
    "            return _make_redis_checkpoint_key(thread_id, checkpoint_ns, checkpoint_id)\n",
    "\n",
    "        all_keys = conn.keys(_make_redis_checkpoint_key(thread_id, checkpoint_ns, \"*\"))\n",
    "        if not all_keys:\n",
    "            return None\n",
    "\n",
    "        latest_key = max(\n",
    "            all_keys,\n",
    "            key=lambda k: _parse_redis_checkpoint_key(k.decode())[\"checkpoint_id\"],\n",
    "        )\n",
    "        return latest_key.decode()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec21ff00-75a7-4789-b863-93fffcc0b32d",
   "metadata": {},
   "source": [
    "### AsyncRedis"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9e5ad763-12ab-4918-af40-0be85678e35b",
   "metadata": {},
   "source": [
    "Below is a reference implementation of AsyncRedisSaver (for asynchronous use of graph, i.e. `.ainvoke()`, `.astream()`). AsyncRedisSaver implements four methods that are required for any async checkpointer:\n",
    "\n",
    "- `.aput` - Store a checkpoint with its configuration and metadata.\n",
    "- `.aput_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).\n",
    "- `.aget_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).\n",
    "- `.alist` - List checkpoints that match a given configuration and filter criteria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "888302ee-c201-498f-b6e3-69ec5f1a039c",
   "metadata": {},
   "outputs": [],
   "source": [
    "class AsyncRedisSaver(BaseCheckpointSaver):\n",
    "    \"\"\"Async redis-based checkpoint saver implementation.\"\"\"\n",
    "\n",
    "    conn: AsyncRedis\n",
    "\n",
    "    def __init__(self, conn: AsyncRedis):\n",
    "        super().__init__()\n",
    "        self.conn = conn\n",
    "\n",
    "    @classmethod\n",
    "    @asynccontextmanager\n",
    "    async def from_conn_info(cls, *, host: str, port: int, db: int) -> AsyncIterator[\"AsyncRedisSaver\"]:\n",
    "        conn = None\n",
    "        try:\n",
    "            conn = AsyncRedis(host=host, port=port, db=db)\n",
    "            yield AsyncRedisSaver(conn)\n",
    "        finally:\n",
    "            if conn:\n",
    "                await conn.aclose()\n",
    "\n",
    "    async def aput(\n",
    "        self,\n",
    "        config: RunnableConfig,\n",
    "        checkpoint: Checkpoint,\n",
    "        metadata: CheckpointMetadata,\n",
    "        new_versions: ChannelVersions,\n",
    "    ) -> RunnableConfig:\n",
    "        \"\"\"Save a checkpoint to the database asynchronously.\n",
    "\n",
    "        This method saves a checkpoint to Redis. The checkpoint is associated\n",
    "        with the provided config and its parent config (if any).\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): The config to associate with the checkpoint.\n",
    "            checkpoint (Checkpoint): The checkpoint to save.\n",
    "            metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.\n",
    "            new_versions (ChannelVersions): New channel versions as of this write.\n",
    "\n",
    "        Returns:\n",
    "            RunnableConfig: Updated configuration after storing the checkpoint.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
    "        checkpoint_id = checkpoint[\"id\"]\n",
    "        parent_checkpoint_id = config[\"configurable\"].get(\"checkpoint_id\")\n",
    "        key = _make_redis_checkpoint_key(thread_id, checkpoint_ns, checkpoint_id)\n",
    "\n",
    "        type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)\n",
    "        serialized_metadata = self.serde.dumps(metadata)\n",
    "        data = {\n",
    "            \"checkpoint\": serialized_checkpoint,\n",
    "            \"type\": type_,\n",
    "            \"checkpoint_id\": checkpoint_id,\n",
    "            \"metadata\": serialized_metadata,\n",
    "            \"parent_checkpoint_id\": parent_checkpoint_id\n",
    "            if parent_checkpoint_id\n",
    "            else \"\",\n",
    "        }\n",
    "\n",
    "        await self.conn.hset(key, mapping=data)\n",
    "        return {\n",
    "            \"configurable\": {\n",
    "                \"thread_id\": thread_id,\n",
    "                \"checkpoint_ns\": checkpoint_ns,\n",
    "                \"checkpoint_id\": checkpoint_id,\n",
    "            }\n",
    "        }\n",
    "\n",
    "    async def aput_writes(\n",
    "        self,\n",
    "        config: RunnableConfig,\n",
    "        writes: List[Tuple[str, Any]],\n",
    "        task_id: str,\n",
    "    ) -> RunnableConfig:\n",
    "        \"\"\"Store intermediate writes linked to a checkpoint asynchronously.\n",
    "\n",
    "        This method saves intermediate writes associated with a checkpoint to the database.\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): Configuration of the related checkpoint.\n",
    "            writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.\n",
    "            task_id (str): Identifier for the task creating the writes.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
    "        checkpoint_id = config[\"configurable\"][\"checkpoint_id\"]\n",
    "\n",
    "        for idx, data in enumerate(_dump_writes(self.serde, writes)):\n",
    "            key = _make_redis_checkpoint_writes_key(\n",
    "                thread_id, checkpoint_ns, checkpoint_id, task_id, idx\n",
    "            )\n",
    "            await self.conn.hset(key, mapping=data)\n",
    "        return config\n",
    "\n",
    "    async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
    "        \"\"\"Get a checkpoint tuple from Redis asynchronously.\n",
    "\n",
    "        This method retrieves a checkpoint tuple from Redis based on the\n",
    "        provided config. If the config contains a \"checkpoint_id\" key, the checkpoint with\n",
    "        the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint\n",
    "        for the given thread ID is retrieved.\n",
    "\n",
    "        Args:\n",
    "            config (RunnableConfig): The config to use for retrieving the checkpoint.\n",
    "\n",
    "        Returns:\n",
    "            Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_id = get_checkpoint_id(config)\n",
    "        checkpoint_ns = config[\"configurable\"].get(\"checkpoint_ns\", \"\")\n",
    "        if not get_checkpoint_id(config):\n",
    "            config = {\n",
    "                \"configurable\": {\n",
    "                    \"thread_id\": thread_id,\n",
    "                    \"checkpoint_ns\": checkpoint_ns,\n",
    "                    \"checkpoint_id\": checkpoint_id,\n",
    "                }\n",
    "            }\n",
    "\n",
    "        checkpoint_key = await self._aget_checkpoint_key(\n",
    "            self.conn, thread_id, checkpoint_ns, checkpoint_id\n",
    "        )\n",
    "        if not checkpoint_key:\n",
    "            return None\n",
    "        checkpoint_data = await self.conn.hgetall(checkpoint_key)\n",
    "\n",
    "        # load pending writes\n",
    "        checkpoint_id = (\n",
    "            checkpoint_id\n",
    "            or _parse_redis_checkpoint_key(checkpoint_key)[\"checkpoint_id\"]\n",
    "        )\n",
    "        writes_key = _make_redis_checkpoint_writes_key(\n",
    "            thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
    "        )\n",
    "        matching_keys = await self.conn.keys(pattern=writes_key)\n",
    "        parsed_keys = [\n",
    "            _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
    "        ]\n",
    "        pending_writes = _load_writes(\n",
    "            self.serde,\n",
    "            {\n",
    "                (parsed_key[\"task_id\"], parsed_key[\"idx\"]): await self.conn.hgetall(key)\n",
    "                for key, parsed_key in sorted(\n",
    "                    zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
    "                )\n",
    "            },\n",
    "        )\n",
    "        return _parse_redis_checkpoint_data(\n",
    "            self.serde, checkpoint_key, checkpoint_data, pending_writes=pending_writes\n",
    "        )\n",
    "\n",
    "    async def alist(\n",
    "        self,\n",
    "        config: Optional[RunnableConfig],\n",
    "        *,\n",
    "        # TODO: implement filtering\n",
    "        filter: Optional[dict[str, Any]] = None,\n",
    "        before: Optional[RunnableConfig] = None,\n",
    "        limit: Optional[int] = None,\n",
    "    ) -> AsyncGenerator[CheckpointTuple, None]:\n",
    "        \"\"\"List checkpoints from Redis asynchronously.\n",
    "\n",
    "        This method retrieves a list of checkpoint tuples from Redis based\n",
    "        on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).\n",
    "\n",
    "        Args:\n",
    "            config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.\n",
    "            filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.\n",
    "            before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.\n",
    "            limit (Optional[int]): Maximum number of checkpoints to return.\n",
    "\n",
    "        Yields:\n",
    "            AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.\n",
    "        \"\"\"\n",
    "        thread_id = config[\"configurable\"][\"thread_id\"]\n",
    "        checkpoint_ns = config[\"configurable\"].get(\"checkpoint_ns\", \"\")\n",
    "        pattern = _make_redis_checkpoint_key(thread_id, checkpoint_ns, \"*\")\n",
    "        keys = _filter_keys(await self.conn.keys(pattern), before, limit)\n",
    "        for key in keys:\n",
    "            data = await self.conn.hgetall(key)\n",
    "            if data and b\"checkpoint\" in data and b\"metadata\" in data:\n",
    "                yield _parse_redis_checkpoint_data(self.serde, key.decode(), data)\n",
    "\n",
    "    async def _aget_checkpoint_key(\n",
    "        self, conn, thread_id: str, checkpoint_ns: str, checkpoint_id: Optional[str]\n",
    "    ) -> Optional[str]:\n",
    "        \"\"\"Asynchronously determine the Redis key for a checkpoint.\"\"\"\n",
    "        if checkpoint_id:\n",
    "            return _make_redis_checkpoint_key(thread_id, checkpoint_ns, checkpoint_id)\n",
    "\n",
    "        all_keys = await conn.keys(\n",
    "            _make_redis_checkpoint_key(thread_id, checkpoint_ns, \"*\")\n",
    "        )\n",
    "        if not all_keys:\n",
    "            return None\n",
    "\n",
    "        latest_key = max(\n",
    "            all_keys,\n",
    "            key=lambda k: _parse_redis_checkpoint_key(k.decode())[\"checkpoint_id\"],\n",
    "        )\n",
    "        return latest_key.decode()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e26b3204-cca2-414c-800e-7e09032445ae",
   "metadata": {},
   "source": [
    "## Setup model and tools for the graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "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-mini\", temperature=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9342c62-dbb4-40f6-9271-7393f1ca48c4",
   "metadata": {},
   "source": [
    "## Use sync connection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "5fe54e79-9eaf-44e2-b2d9-1e0284b984d0",
   "metadata": {},
   "outputs": [],
   "source": [
    "with RedisSaver.from_conn_info(host=\"localhost\", port=6379, db=0) as checkpointer:\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",
    "\n",
    "    latest_checkpoint = checkpointer.get(config)\n",
    "    latest_checkpoint_tuple = checkpointer.get_tuple(config)\n",
    "    checkpoint_tuples = list(checkpointer.list(config))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "c298e627-115a-4b4c-ae17-520ca9a640cd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'v': 1,\n",
       " 'ts': '2024-08-09T01:56:48.328315+00:00',\n",
       " 'id': '1ef55f2a-3614-69b4-8003-2181cff935cc',\n",
       " 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='f911e000-75a1-41f6-8e38-77bb086c2ecf'),\n",
       "   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4f1531f1-067c-4e16-8b62-7a6b663e93bd-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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='e27bb3a1-1798-494a-b4ad-2deadda8b2bf', tool_call_id='call_l5e5YcTJDJYOdvi4scBy9n2I'),\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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-ad546b5a-70ce-404e-9656-dcc6ecd482d3-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})],\n",
       "  'agent': 'agent'},\n",
       " 'channel_versions': {'__start__': '00000000000000000000000000000002.',\n",
       "  'messages': '00000000000000000000000000000005.16e98d6f7ece7598829eddf1b33a33c4',\n",
       "  'start:agent': '00000000000000000000000000000003.',\n",
       "  'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af',\n",
       "  'branch:agent:should_continue:tools': '00000000000000000000000000000004.',\n",
       "  'tools': '00000000000000000000000000000005.'},\n",
       " 'versions_seen': {'__input__': {},\n",
       "  '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'},\n",
       "  'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc',\n",
       "   'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'},\n",
       "  'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}},\n",
       " 'pending_sends': [],\n",
       " 'current_tasks': {}}"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "latest_checkpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "922f9406-0f68-418a-9cb4-e0e29de4b5f9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "CheckpointTuple(config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3614-69b4-8003-2181cff935cc'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:48.328315+00:00', 'id': '1ef55f2a-3614-69b4-8003-2181cff935cc', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='f911e000-75a1-41f6-8e38-77bb086c2ecf'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4f1531f1-067c-4e16-8b62-7a6b663e93bd-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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='e27bb3a1-1798-494a-b4ad-2deadda8b2bf', tool_call_id='call_l5e5YcTJDJYOdvi4scBy9n2I'), 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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-ad546b5a-70ce-404e-9656-dcc6ecd482d3-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})], 'agent': 'agent'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000005.16e98d6f7ece7598829eddf1b33a33c4', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000005.'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-ad546b5a-70ce-404e-9656-dcc6ecd482d3-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}, 'step': 3}, parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-306f-6252-8002-47c2374ec1f2'}}, pending_writes=[])"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "latest_checkpoint_tuple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "b2ce743b-5896-443b-9ec0-a655b065895c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[CheckpointTuple(config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3614-69b4-8003-2181cff935cc'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:48.328315+00:00', 'id': '1ef55f2a-3614-69b4-8003-2181cff935cc', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='f911e000-75a1-41f6-8e38-77bb086c2ecf'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4f1531f1-067c-4e16-8b62-7a6b663e93bd-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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='e27bb3a1-1798-494a-b4ad-2deadda8b2bf', tool_call_id='call_l5e5YcTJDJYOdvi4scBy9n2I'), 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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-ad546b5a-70ce-404e-9656-dcc6ecd482d3-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})], 'agent': 'agent'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000005.16e98d6f7ece7598829eddf1b33a33c4', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000005.'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-ad546b5a-70ce-404e-9656-dcc6ecd482d3-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}, 'step': 3}, parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-306f-6252-8002-47c2374ec1f2'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-306f-6252-8002-47c2374ec1f2'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:47.736251+00:00', 'id': '1ef55f2a-306f-6252-8002-47c2374ec1f2', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='f911e000-75a1-41f6-8e38-77bb086c2ecf'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4f1531f1-067c-4e16-8b62-7a6b663e93bd-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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='e27bb3a1-1798-494a-b4ad-2deadda8b2bf', tool_call_id='call_l5e5YcTJDJYOdvi4scBy9n2I')], 'tools': 'tools'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000004.b16eb718f179ac1dcde54c5652768cf5', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000004.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': {'tools': {'messages': [ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='e27bb3a1-1798-494a-b4ad-2deadda8b2bf', tool_call_id='call_l5e5YcTJDJYOdvi4scBy9n2I')]}}, 'step': 2}, parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-305f-61cc-8001-efac33022ef7'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-305f-61cc-8001-efac33022ef7'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:47.729689+00:00', 'id': '1ef55f2a-305f-61cc-8001-efac33022ef7', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='f911e000-75a1-41f6-8e38-77bb086c2ecf'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4f1531f1-067c-4e16-8b62-7a6b663e93bd-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})], 'agent': 'agent', 'branch:agent:should_continue:tools': 'agent'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000003.4dd312547dcca1cf91a19adb620a18d6', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af', 'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4f1531f1-067c-4e16-8b62-7a6b663e93bd-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_l5e5YcTJDJYOdvi4scBy9n2I', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})]}}, 'step': 1}, parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-2a52-6a7c-8000-27624d954d15'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-2a52-6a7c-8000-27624d954d15'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:47.095456+00:00', 'id': '1ef55f2a-2a52-6a7c-8000-27624d954d15', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='f911e000-75a1-41f6-8e38-77bb086c2ecf')], 'start:agent': '__start__'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000002.52e8b0c387f50c28345585c088150464', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': None, 'step': 0}, parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-2a50-6812-bfff-34e3be35d6f2'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-2a50-6812-bfff-34e3be35d6f2'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:47.094575+00:00', 'id': '1ef55f2a-2a50-6812-bfff-34e3be35d6f2', 'channel_values': {'messages': [], '__start__': {'messages': [['human', \"what's the weather in sf\"]]}}, 'channel_versions': {'__start__': '00000000000000000000000000000001.ab89befb52cc0e91e106ef7f500ea033'}, 'versions_seen': {'__input__': {}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'input', 'writes': {'messages': [['human', \"what's the weather in sf\"]]}, 'step': -1}, parent_config=None, pending_writes=None)]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "checkpoint_tuples"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0a47d3e-e588-48fc-a5d4-2145dff17e77",
   "metadata": {},
   "source": [
    "## Use async connection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "6a39d1ff-ca37-4457-8b52-07d33b59c36e",
   "metadata": {},
   "outputs": [],
   "source": [
    "async with AsyncRedisSaver.from_conn_info(host=\"localhost\", port=6379, db=0) as checkpointer:\n",
    "    graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
    "    config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
    "    res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n",
    "\n",
    "    latest_checkpoint = await checkpointer.aget(config)\n",
    "    latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n",
    "    checkpoint_tuples = [c async for c in checkpointer.alist(config)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "51125ef1-bdb6-454e-82cc-4ae19a113606",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'v': 1,\n",
       " 'ts': '2024-08-09T01:56:49.503241+00:00',\n",
       " 'id': '1ef55f2a-4149-61ea-8003-dc5506862287',\n",
       " 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='5a106e79-a617-4707-839f-134d4e4b762a'),\n",
       "   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0d6fa3b4-cace-41a8-b025-d01d16f6bbe9-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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='922124bd-d3b0-4929-a996-a75d842b8b44', tool_call_id='call_TvPLLyhuQQN99EcZc8SzL8x9'),\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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-69a10e66-d61f-475e-b7de-a1ecd08a6c3a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})],\n",
       "  'agent': 'agent'},\n",
       " 'channel_versions': {'__start__': '00000000000000000000000000000002.',\n",
       "  'messages': '00000000000000000000000000000005.2cb29d082da6435a7528b4c917fd0c28',\n",
       "  'start:agent': '00000000000000000000000000000003.',\n",
       "  'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af',\n",
       "  'branch:agent:should_continue:tools': '00000000000000000000000000000004.',\n",
       "  'tools': '00000000000000000000000000000005.'},\n",
       " 'versions_seen': {'__input__': {},\n",
       "  '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'},\n",
       "  'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc',\n",
       "   'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'},\n",
       "  'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}},\n",
       " 'pending_sends': [],\n",
       " 'current_tasks': {}}"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "latest_checkpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "97f8a87b-8423-41c6-a76b-9a6b30904e73",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-4149-61ea-8003-dc5506862287'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:49.503241+00:00', 'id': '1ef55f2a-4149-61ea-8003-dc5506862287', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='5a106e79-a617-4707-839f-134d4e4b762a'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0d6fa3b4-cace-41a8-b025-d01d16f6bbe9-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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='922124bd-d3b0-4929-a996-a75d842b8b44', tool_call_id='call_TvPLLyhuQQN99EcZc8SzL8x9'), 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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-69a10e66-d61f-475e-b7de-a1ecd08a6c3a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000005.2cb29d082da6435a7528b4c917fd0c28', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000005.'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', '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-mini', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-69a10e66-d61f-475e-b7de-a1ecd08a6c3a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}, 'step': 3}, parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3d07-647e-8002-b5e4d28c00c9'}}, pending_writes=[])"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "latest_checkpoint_tuple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "2b6d73ca-519e-45f7-90c2-1b8596624505",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-4149-61ea-8003-dc5506862287'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:49.503241+00:00', 'id': '1ef55f2a-4149-61ea-8003-dc5506862287', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='5a106e79-a617-4707-839f-134d4e4b762a'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0d6fa3b4-cace-41a8-b025-d01d16f6bbe9-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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='922124bd-d3b0-4929-a996-a75d842b8b44', tool_call_id='call_TvPLLyhuQQN99EcZc8SzL8x9'), 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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-69a10e66-d61f-475e-b7de-a1ecd08a6c3a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000005.2cb29d082da6435a7528b4c917fd0c28', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000005.065d90dd7f7cd091f0233855210bb2af', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000005.'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', '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-mini', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'stop', 'logprobs': None}, id='run-69a10e66-d61f-475e-b7de-a1ecd08a6c3a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}, 'step': 3}, parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3d07-647e-8002-b5e4d28c00c9'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3d07-647e-8002-b5e4d28c00c9'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:49.056860+00:00', 'id': '1ef55f2a-3d07-647e-8002-b5e4d28c00c9', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='5a106e79-a617-4707-839f-134d4e4b762a'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0d6fa3b4-cace-41a8-b025-d01d16f6bbe9-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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='922124bd-d3b0-4929-a996-a75d842b8b44', tool_call_id='call_TvPLLyhuQQN99EcZc8SzL8x9')], 'tools': 'tools'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000004.07964a3a545f9ff95545db45a9753d11', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000004.', 'branch:agent:should_continue:tools': '00000000000000000000000000000004.', 'tools': '00000000000000000000000000000004.022986cd20ae85c77ea298a383f69ba8'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'tools': {'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': {'tools': {'messages': [ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='922124bd-d3b0-4929-a996-a75d842b8b44', tool_call_id='call_TvPLLyhuQQN99EcZc8SzL8x9')]}}, 'step': 2}, parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3cf9-6996-8001-88dab066840d'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-3cf9-6996-8001-88dab066840d'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:49.051234+00:00', 'id': '1ef55f2a-3cf9-6996-8001-88dab066840d', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='5a106e79-a617-4707-839f-134d4e4b762a'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0d6fa3b4-cace-41a8-b025-d01d16f6bbe9-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000003.cc96d93b1afbd1b69d53851320670b97', 'start:agent': '00000000000000000000000000000003.', 'agent': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af', 'branch:agent:should_continue:tools': '00000000000000000000000000000003.065d90dd7f7cd091f0233855210bb2af'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}, 'agent': {'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', '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', 'system_fingerprint': 'fp_48196bc67a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0d6fa3b4-cace-41a8-b025-d01d16f6bbe9-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TvPLLyhuQQN99EcZc8SzL8x9', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})]}}, 'step': 1}, parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-36a6-6788-8000-9efe1769f8c1'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-36a6-6788-8000-9efe1769f8c1'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:48.388067+00:00', 'id': '1ef55f2a-36a6-6788-8000-9efe1769f8c1', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='5a106e79-a617-4707-839f-134d4e4b762a')], 'start:agent': '__start__'}, 'channel_versions': {'__start__': '00000000000000000000000000000002.', 'messages': '00000000000000000000000000000002.a6994b785a651d88df51020401745af8', 'start:agent': '00000000000000000000000000000002.d6f25946c3108fc12f27abbcf9b4cedc'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'loop', 'writes': None, 'step': 0}, parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-36a3-6614-bfff-05dafa02b4d7'}}, pending_writes=None),\n",
       " CheckpointTuple(config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef55f2a-36a3-6614-bfff-05dafa02b4d7'}}, checkpoint={'v': 1, 'ts': '2024-08-09T01:56:48.386807+00:00', 'id': '1ef55f2a-36a3-6614-bfff-05dafa02b4d7', 'channel_values': {'messages': [], '__start__': {'messages': [['human', \"what's the weather in nyc\"]]}}, 'channel_versions': {'__start__': '00000000000000000000000000000001.0e148ae3debe753278387e84f786e863'}, 'versions_seen': {'__input__': {}}, 'pending_sends': [], 'current_tasks': {}}, metadata={'source': 'input', 'writes': {'messages': [['human', \"what's the weather in nyc\"]]}, 'step': -1}, parent_config=None, pending_writes=None)]"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "checkpoint_tuples"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langgraph",
   "language": "python",
   "name": "langgraph"
  },
  "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
}
