mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 14:15:44 +02:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da6462e608 | ||
|
|
49bf8f6816 | ||
|
|
71ce07d971 | ||
|
|
a16f86b5bd | ||
|
|
4dc27b98f1 | ||
|
|
be8476d981 | ||
|
|
cd92f19858 | ||
|
|
a2f4d57bf2 | ||
|
|
f9fa35ed82 | ||
|
|
3b87a81c70 | ||
|
|
002b260e7a | ||
|
|
659c9881cf | ||
|
|
b535d38393 | ||
|
|
d33807b5ea | ||
|
|
9650f0d41d | ||
|
|
127b6dedee | ||
|
|
db5a6c569a | ||
|
|
b47ab6c30a | ||
|
|
281c97b9e5 | ||
|
|
cb21a3547e | ||
|
|
6028eb02c9 | ||
|
|
712d52ad3b | ||
|
|
6078c408c3 | ||
|
|
9caeeaeeec | ||
|
|
2bd9eb4298 | ||
|
|
4c2542aac6 | ||
|
|
54da68cfe7 | ||
|
|
51b62ca0bd | ||
|
|
ca4b65e1a6 | ||
|
|
1e237bf33a | ||
|
|
fb8390e138 | ||
|
|
5cab47f751 | ||
|
|
172b4af6ed |
@@ -52,7 +52,11 @@ By default, all nodes in the graph will share the same state. This means that th
|
||||
|
||||
### Reducers
|
||||
|
||||
Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. Let's take a look at a few examples to understand them better.
|
||||
Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. There are a few different types of reducers, starting with the default type of reducer:
|
||||
|
||||
#### Default Reducer
|
||||
|
||||
These two examples show how to use the default reducer:
|
||||
|
||||
**Example A:**
|
||||
|
||||
@@ -79,6 +83,10 @@ class State(TypedDict):
|
||||
|
||||
In this example, we've used the `Annotated` type to specify a reducer function (`operator.add`) for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["hi", "bye"]}`. Notice here that the `bar` key is updated by adding the two lists together.
|
||||
|
||||
#### Context Reducer
|
||||
|
||||
You can use `Context` channels to define shared resources (such as database connections) that are managed outside of your graph's nodes and excluded from checkpointing. The context manager provided to the Context channel is entered before the first step of the graph execution and exited after the last step, allowing you to set up and clean up resources for the duration of the graph invocation. Read this [how to](https://langchain-ai.github.io/langgraph/how-tos/state-context-key) to see an example of using the `Context` channel in your graph.
|
||||
|
||||
### Working with Messages in Graph State
|
||||
|
||||
#### Why use messages?
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-community langchain-openai scikit-learn"]
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-community langchain-openai scikit-learn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -48,7 +51,15 @@
|
||||
"id": "3d1ef253-6b0c-4481-868c-e1fe84f2c8ff",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import requests\n\nurl = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\nresponse = requests.get(url)\n\nwith open(\"Chinook.db\", \"wb\") as file:\n file.write(response.content)"]
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n",
|
||||
"response = requests.get(url)\n",
|
||||
"\n",
|
||||
"with open(\"Chinook.db\", \"wb\") as file:\n",
|
||||
" file.write(response.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -77,7 +88,12 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["from langchain_community.utilities import SQLDatabase\n\ndb = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\ndb.get_usable_table_names()"]
|
||||
"source": [
|
||||
"from langchain_community.utilities import SQLDatabase\n",
|
||||
"\n",
|
||||
"db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n",
|
||||
"db.get_usable_table_names()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -96,7 +112,11 @@
|
||||
"id": "d9ea4e80-30e6-4d46-b480-35f0be2fb055",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"]
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -118,7 +138,9 @@
|
||||
"id": "ea958e9f-ab1f-49b5-bd85-16332055297c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.messages import HumanMessage, SystemMessage"]
|
||||
"source": [
|
||||
"from langchain_core.messages import HumanMessage, SystemMessage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -137,7 +159,12 @@
|
||||
"id": "975b039a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["# This tool is given to the agent to look up information about a customer\ndef get_customer_info(customer_id: int):\n \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"]
|
||||
"source": [
|
||||
"# This tool is given to the agent to look up information about a customer\n",
|
||||
"def get_customer_info(customer_id: int):\n",
|
||||
" \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n",
|
||||
" return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -145,7 +172,20 @@
|
||||
"id": "1d5fa446",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["customer_prompt = \"\"\"Your job is to help a user update their profile.\n\nYou only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n\nIf you are unable to help the user, you can \"\"\"\n\n\ndef get_customer_messages(messages):\n return [SystemMessage(content=customer_prompt)] + messages\n\n\ncustomer_chain = get_customer_messages | model.bind_tools([get_customer_info])"]
|
||||
"source": [
|
||||
"customer_prompt = \"\"\"Your job is to help a user update their profile.\n",
|
||||
"\n",
|
||||
"You only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n",
|
||||
"\n",
|
||||
"If you are unable to help the user, you can \"\"\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_customer_messages(messages):\n",
|
||||
" return [SystemMessage(content=customer_prompt)] + messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"customer_chain = get_customer_messages | model.bind_tools([get_customer_info])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -166,7 +206,19 @@
|
||||
"id": "a8604a3b-b484-4b2b-a914-4236cb98c524",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_openai import OpenAIEmbeddings\n\nartists = db._execute(\"select * from Artist\")\nsongs = db._execute(\"select * from Track\")\nartist_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n).as_retriever()\nsong_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n).as_retriever()"]
|
||||
"source": [
|
||||
"from langchain_community.vectorstores import SKLearnVectorStore\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"\n",
|
||||
"artists = db._execute(\"select * from Artist\")\n",
|
||||
"songs = db._execute(\"select * from Track\")\n",
|
||||
"artist_retriever = SKLearnVectorStore.from_texts(\n",
|
||||
" [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n",
|
||||
").as_retriever()\n",
|
||||
"song_retriever = SKLearnVectorStore.from_texts(\n",
|
||||
" [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n",
|
||||
").as_retriever()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -182,7 +234,16 @@
|
||||
"id": "0a2a2b74",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["def get_albums_by_artist(artist):\n \"\"\"Get albums by an artist (or similar artists).\"\"\"\n docs = artist_retriever.get_relevant_documents(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"]
|
||||
"source": [
|
||||
"def get_albums_by_artist(artist):\n",
|
||||
" \"\"\"Get albums by an artist (or similar artists).\"\"\"\n",
|
||||
" docs = artist_retriever.get_relevant_documents(artist)\n",
|
||||
" artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n",
|
||||
" return db.run(\n",
|
||||
" f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n",
|
||||
" include_columns=True,\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -198,7 +259,16 @@
|
||||
"id": "da533f50",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["def get_tracks_by_artist(artist):\n \"\"\"Get songs by an artist (or similar artists).\"\"\"\n docs = artist_retriever.invoke(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"]
|
||||
"source": [
|
||||
"def get_tracks_by_artist(artist):\n",
|
||||
" \"\"\"Get songs by an artist (or similar artists).\"\"\"\n",
|
||||
" docs = artist_retriever.invoke(artist)\n",
|
||||
" artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n",
|
||||
" return db.run(\n",
|
||||
" f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n",
|
||||
" include_columns=True,\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -214,7 +284,11 @@
|
||||
"id": "b3c07010",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["def check_for_songs(song_title):\n \"\"\"Check if a song exists by its name.\"\"\"\n return song_retriever.invoke(song_title)"]
|
||||
"source": [
|
||||
"def check_for_songs(song_title):\n",
|
||||
" \"\"\"Check if a song exists by its name.\"\"\"\n",
|
||||
" return song_retriever.invoke(song_title)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -230,7 +304,23 @@
|
||||
"id": "72a14d5c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n\nYou only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n\nWhen looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\non similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n\n\ndef get_song_messages(messages):\n return [SystemMessage(content=song_system_message)] + messages\n\n\nsong_recc_chain = get_song_messages | model.bind_tools(\n [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n)"]
|
||||
"source": [
|
||||
"song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n",
|
||||
"\n",
|
||||
"You only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n",
|
||||
"\n",
|
||||
"When looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\n",
|
||||
"on similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_song_messages(messages):\n",
|
||||
" return [SystemMessage(content=song_system_message)] + messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"song_recc_chain = get_song_messages | model.bind_tools(\n",
|
||||
" [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -249,7 +339,10 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\nsong_recc_chain.invoke(msgs)"]
|
||||
"source": [
|
||||
"msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\n",
|
||||
"song_recc_chain.invoke(msgs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -267,7 +360,32 @@
|
||||
"id": "73e74268",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Router(BaseModel):\n \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n\n choice: str = Field(description=\"should be one of: music, customer\")\n\n\nsystem_message = \"\"\"Your job is to help as a customer service representative for a music store.\n\nYou should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n\n- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n\nIf the user is asking or wants to ask about updating or accessing their information, send them to that route.\nIf the user is asking or wants to ask about music, send them to that route.\nOtherwise, respond.\"\"\"\n\n\ndef get_messages(messages):\n return [SystemMessage(content=system_message)] + messages"]
|
||||
"source": [
|
||||
"from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n",
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Router(BaseModel):\n",
|
||||
" \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n",
|
||||
"\n",
|
||||
" choice: str = Field(description=\"should be one of: music, customer\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"system_message = \"\"\"Your job is to help as a customer service representative for a music store.\n",
|
||||
"\n",
|
||||
"You should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n",
|
||||
"\n",
|
||||
"- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n",
|
||||
"- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n",
|
||||
"\n",
|
||||
"If the user is asking or wants to ask about updating or accessing their information, send them to that route.\n",
|
||||
"If the user is asking or wants to ask about music, send them to that route.\n",
|
||||
"Otherwise, respond.\"\"\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_messages(messages):\n",
|
||||
" return [SystemMessage(content=system_message)] + messages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -275,7 +393,9 @@
|
||||
"id": "ddf27314",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["chain = get_messages | model.bind_tools([Router])"]
|
||||
"source": [
|
||||
"chain = get_messages | model.bind_tools([Router])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -294,7 +414,10 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\nchain.invoke(msgs)"]
|
||||
"source": [
|
||||
"msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\n",
|
||||
"chain.invoke(msgs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -313,7 +436,10 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\nchain.invoke(msgs)"]
|
||||
"source": [
|
||||
"msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\n",
|
||||
"chain.invoke(msgs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -321,7 +447,15 @@
|
||||
"id": "bd6ddd8b-7500-46a7-811d-3bcb937bda51",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.messages import AIMessage\n\n\ndef add_name(message, name):\n _dict = message.dict()\n _dict[\"name\"] = name\n return AIMessage(**_dict)"]
|
||||
"source": [
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def add_name(message, name):\n",
|
||||
" _dict = message.dict()\n",
|
||||
" _dict[\"name\"] = name\n",
|
||||
" return AIMessage(**_dict)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -329,7 +463,45 @@
|
||||
"id": "27494de5-8345-4c23-bc0e-81e0dd5d47d8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import json\n\nfrom langgraph.graph import END, START\n\n\ndef _get_last_ai_message(messages):\n for m in messages[::-1]:\n if isinstance(m, AIMessage):\n return m\n return None\n\n\ndef _is_tool_call(msg):\n return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n\n\ndef _route(messages):\n last_message = messages[-1]\n if isinstance(last_message, AIMessage):\n if not last_message.tool_calls:\n return END\n else:\n if last_message.name == \"general\":\n if len(last_message.tool_calls) > 1:\n raise ValueError(\"Too many tools\")\n return last_message.tool_calls[0][\"args\"][\"choice\"]\n else:\n return \"tools\"\n last_m = _get_last_ai_message(messages)\n if last_m is None:\n return \"general\"\n if last_m.name == \"music\":\n return \"music\"\n elif last_m.name == \"customer\":\n return \"customer\"\n else:\n return \"general\""]
|
||||
"source": [
|
||||
"import json\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, START\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _get_last_ai_message(messages):\n",
|
||||
" for m in messages[::-1]:\n",
|
||||
" if isinstance(m, AIMessage):\n",
|
||||
" return m\n",
|
||||
" return None\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _is_tool_call(msg):\n",
|
||||
" return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _route(messages):\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" if isinstance(last_message, AIMessage):\n",
|
||||
" if not last_message.tool_calls:\n",
|
||||
" return END\n",
|
||||
" else:\n",
|
||||
" if last_message.name == \"general\":\n",
|
||||
" if len(last_message.tool_calls) > 1:\n",
|
||||
" raise ValueError(\"Too many tools\")\n",
|
||||
" return last_message.tool_calls[0][\"args\"][\"choice\"]\n",
|
||||
" else:\n",
|
||||
" return \"tools\"\n",
|
||||
" last_m = _get_last_ai_message(messages)\n",
|
||||
" if last_m is None:\n",
|
||||
" return \"general\"\n",
|
||||
" if last_m.name == \"music\":\n",
|
||||
" return \"music\"\n",
|
||||
" elif last_m.name == \"customer\":\n",
|
||||
" return \"customer\"\n",
|
||||
" else:\n",
|
||||
" return \"general\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -337,7 +509,12 @@
|
||||
"id": "8aec704a-46fe-4fb3-bdee-11c3bbffc370",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.prebuilt import ToolNode\n\ntools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\ntool_node = ToolNode(tools)"]
|
||||
"source": [
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"tools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\n",
|
||||
"tool_node = ToolNode(tools)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -345,7 +522,16 @@
|
||||
"id": "4d5b75c6-73e0-4922-a765-a15be63f869e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["def _filter_out_routes(messages):\n ms = []\n for m in messages:\n if _is_tool_call(m):\n if m.name == \"general\":\n continue\n ms.append(m)\n return ms"]
|
||||
"source": [
|
||||
"def _filter_out_routes(messages):\n",
|
||||
" ms = []\n",
|
||||
" for m in messages:\n",
|
||||
" if _is_tool_call(m):\n",
|
||||
" if m.name == \"general\":\n",
|
||||
" continue\n",
|
||||
" ms.append(m)\n",
|
||||
" return ms"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -353,7 +539,13 @@
|
||||
"id": "fd4dbf98-dbb3-411a-bad6-2bb334072aaf",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from functools import partial\n\ngeneral_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\nmusic_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\ncustomer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"]
|
||||
"source": [
|
||||
"from functools import partial\n",
|
||||
"\n",
|
||||
"general_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\n",
|
||||
"music_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\n",
|
||||
"customer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -361,7 +553,33 @@
|
||||
"id": "dcade924",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nfrom langgraph.graph import MessageGraph\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = MessageGraph()\nnodes = {\n \"general\": \"general\",\n \"music\": \"music\",\n END: END,\n \"tools\": \"tools\",\n \"customer\": \"customer\",\n}\n# Define a new graph\nworkflow = MessageGraph()\nworkflow.add_node(\"general\", general_node)\nworkflow.add_node(\"music\", music_node)\nworkflow.add_node(\"customer\", customer_node)\nworkflow.add_node(\"tools\", tool_node)\nworkflow.add_conditional_edges(\"general\", _route, nodes)\nworkflow.add_conditional_edges(\"tools\", _route, nodes)\nworkflow.add_conditional_edges(\"music\", _route, nodes)\nworkflow.add_conditional_edges(\"customer\", _route, nodes)\nworkflow.add_conditional_edges(START, _route, nodes)\ngraph = workflow.compile()"]
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"from langgraph.graph import MessageGraph\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = MessageGraph()\n",
|
||||
"nodes = {\n",
|
||||
" \"general\": \"general\",\n",
|
||||
" \"music\": \"music\",\n",
|
||||
" END: END,\n",
|
||||
" \"tools\": \"tools\",\n",
|
||||
" \"customer\": \"customer\",\n",
|
||||
"}\n",
|
||||
"# Define a new graph\n",
|
||||
"workflow = MessageGraph()\n",
|
||||
"workflow.add_node(\"general\", general_node)\n",
|
||||
"workflow.add_node(\"music\", music_node)\n",
|
||||
"workflow.add_node(\"customer\", customer_node)\n",
|
||||
"workflow.add_node(\"tools\", tool_node)\n",
|
||||
"workflow.add_conditional_edges(\"general\", _route, nodes)\n",
|
||||
"workflow.add_conditional_edges(\"tools\", _route, nodes)\n",
|
||||
"workflow.add_conditional_edges(\"music\", _route, nodes)\n",
|
||||
"workflow.add_conditional_edges(\"customer\", _route, nodes)\n",
|
||||
"workflow.add_conditional_edges(START, _route, nodes)\n",
|
||||
"graph = workflow.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -370,7 +588,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"User (q/Q to quit): what music do you have?\n"
|
||||
@@ -395,7 +613,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdin",
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"User (q/Q to quit): how about shakira?\n"
|
||||
@@ -446,7 +664,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdin",
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"User (q/Q to quit): hm cool\n"
|
||||
@@ -483,7 +701,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdin",
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"User (q/Q to quit): q\n"
|
||||
@@ -497,7 +715,27 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["import uuid\n\nfrom langchain_core.messages import HumanMessage\n\nfrom langgraph.graph.graph import START\n\nhistory = []\nwhile True:\n user = input(\"User (q/Q to quit): \")\n if user in {\"q\", \"Q\"}:\n print(\"AI: Byebye\")\n break\n history.append(HumanMessage(content=user))\n async for output in graph.astream(history):\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"]
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"\n",
|
||||
"from langgraph.graph.graph import START\n",
|
||||
"\n",
|
||||
"history = []\n",
|
||||
"while True:\n",
|
||||
" user = input(\"User (q/Q to quit): \")\n",
|
||||
" if user in {\"q\", \"Q\"}:\n",
|
||||
" print(\"AI: Byebye\")\n",
|
||||
" break\n",
|
||||
" history.append(HumanMessage(content=user))\n",
|
||||
" async for output in graph.astream(history):\n",
|
||||
" for key, value in output.items():\n",
|
||||
" print(f\"Output from node '{key}':\")\n",
|
||||
" print(\"---\")\n",
|
||||
" print(value)\n",
|
||||
" print(\"\\n---\\n\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -176,10 +176,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import START, MessageGraph\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"workflow = MessageGraph()\n",
|
||||
"workflow.add_node(\"info\", chain)\n",
|
||||
"workflow.add_node(\"prompt\", prompt_gen_chain)\n",
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=memory)"]
|
||||
"source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = MemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,7 +39,10 @@
|
||||
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain_openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -55,7 +58,18 @@
|
||||
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import getpass\nimport os\n\n\ndef _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\")"]
|
||||
"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",
|
||||
@@ -71,7 +85,10 @@
|
||||
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
|
||||
"source": [
|
||||
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
|
||||
"_set_env(\"LANGCHAIN_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -89,7 +106,22 @@
|
||||
"id": "6098e5cb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
|
||||
"source": [
|
||||
"from typing import Annotated\n",
|
||||
"\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"\n",
|
||||
"# `add_messages`` essentially does this\n",
|
||||
"# (with more robust handling)\n",
|
||||
"# def add_messages(left: list, right: list):\n",
|
||||
"# return left + right\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, add_messages]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -109,7 +141,22 @@
|
||||
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]"]
|
||||
"source": [
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def search(query: str):\n",
|
||||
" \"\"\"Call to surf the web.\"\"\"\n",
|
||||
" # This is a placeholder for the actual implementation\n",
|
||||
" # Don't let the LLM know this though 😊\n",
|
||||
" return [\n",
|
||||
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"tools = [search]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -127,7 +174,11 @@
|
||||
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
|
||||
"source": [
|
||||
"from langgraph.prebuilt import ToolExecutor\n",
|
||||
"\n",
|
||||
"tool_executor = ToolExecutor(tools)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -148,7 +199,11 @@
|
||||
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(temperature=0)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -166,7 +221,9 @@
|
||||
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["model = model.bind_tools(tools)"]
|
||||
"source": [
|
||||
"model = model.bind_tools(tools)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -201,7 +258,53 @@
|
||||
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"]
|
||||
"source": [
|
||||
"from langchain_core.messages import ToolMessage\n",
|
||||
"\n",
|
||||
"from langgraph.prebuilt import ToolInvocation\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the function that determines whether to continue or not\n",
|
||||
"def should_continue(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" # If there is no function call, then we finish\n",
|
||||
" if not last_message.tool_calls:\n",
|
||||
" return \"end\"\n",
|
||||
" # Otherwise if there is, we continue\n",
|
||||
" else:\n",
|
||||
" return \"continue\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the function that calls the model\n",
|
||||
"def call_model(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" response = model.invoke(messages)\n",
|
||||
" # We return a list, because this will get added to the existing list\n",
|
||||
" return {\"messages\": [response]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the function to execute tools\n",
|
||||
"def call_tool(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" # Based on the continue condition\n",
|
||||
" # we know the last message involves a function call\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" # We construct an ToolInvocation from the function_call\n",
|
||||
" tool_call = last_message.tool_calls[0]\n",
|
||||
" action = ToolInvocation(\n",
|
||||
" tool=tool_call[\"name\"],\n",
|
||||
" tool_input=tool_call[\"args\"],\n",
|
||||
" )\n",
|
||||
" # We call the tool_executor and get back a response\n",
|
||||
" response = tool_executor.invoke(action)\n",
|
||||
" # We use the response to create a ToolMessage\n",
|
||||
" tool_message = ToolMessage(\n",
|
||||
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
|
||||
" )\n",
|
||||
" # We return a list, because this will get added to the existing list\n",
|
||||
" return {\"messages\": [tool_message]}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -219,7 +322,45 @@
|
||||
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
|
||||
"source": [
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"\n",
|
||||
"# Define a new graph\n",
|
||||
"workflow = StateGraph(State)\n",
|
||||
"\n",
|
||||
"# Define the two nodes we will cycle between\n",
|
||||
"workflow.add_node(\"agent\", call_model)\n",
|
||||
"workflow.add_node(\"action\", call_tool)\n",
|
||||
"\n",
|
||||
"# Set the entrypoint as `agent`\n",
|
||||
"# This means that this node is the first one called\n",
|
||||
"workflow.add_edge(START, \"agent\")\n",
|
||||
"\n",
|
||||
"# We now add a conditional edge\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" # First, we define the start node. We use `agent`.\n",
|
||||
" # This means these are the edges taken after the `agent` node is called.\n",
|
||||
" \"agent\",\n",
|
||||
" # Next, we pass in the function that will determine which node is called next.\n",
|
||||
" should_continue,\n",
|
||||
" # Finally we pass in a mapping.\n",
|
||||
" # The keys are strings, and the values are other nodes.\n",
|
||||
" # END is a special node marking that the graph should finish.\n",
|
||||
" # What will happen is we will call `should_continue`, and then the output of that\n",
|
||||
" # will be matched against the keys in this mapping.\n",
|
||||
" # Based on which one it matches, that node will then be called.\n",
|
||||
" {\n",
|
||||
" # If `tools`, then we call the tool node.\n",
|
||||
" \"continue\": \"action\",\n",
|
||||
" # Otherwise we finish.\n",
|
||||
" \"end\": END,\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# We now add a normal edge from `tools` to `agent`.\n",
|
||||
"# This means that after `tools` is called, `agent` node is called next.\n",
|
||||
"workflow.add_edge(\"action\", \"agent\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -237,7 +378,11 @@
|
||||
"id": "6845ed6a-d155-4105-9160-28849877248b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -255,7 +400,12 @@
|
||||
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
|
||||
"source": [
|
||||
"# Finally, we compile it!\n",
|
||||
"# This compiles it into a LangChain Runnable,\n",
|
||||
"# meaning you can use it as you would any other runnable\n",
|
||||
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -282,7 +432,11 @@
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"]
|
||||
"source": [
|
||||
"from IPython.display import Image, display\n",
|
||||
"\n",
|
||||
"display(Image(app.get_graph().draw_mermaid_png()))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -313,7 +467,14 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"2\"}}\ninputs = [HumanMessage(content=\"hi! I'm bob\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"\n",
|
||||
"thread = {\"configurable\": {\"thread_id\": \"2\"}}\n",
|
||||
"inputs = [HumanMessage(content=\"hi! I'm bob\")]\n",
|
||||
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -334,7 +495,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\n",
|
||||
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -358,7 +523,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\n",
|
||||
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -392,7 +561,10 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -427,7 +599,43 @@
|
||||
"id": "5454f436-d56e-4499-9381-06192aca1b56",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import json\nfrom typing import Optional\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to construct message asking for verification\ndef generate_verification_message(message: AIMessage) -> None:\n \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n serialized_tool_calls = json.dumps(\n message.tool_calls,\n indent=2,\n )\n return AIMessage(\n content=(\n \"I plan to invoke the following tools, do you approve?\\n\\n\"\n \"Type 'y' if you do, anything else to stop.\\n\\n\"\n f\"{serialized_tool_calls}\"\n ),\n id=message.id,\n )\n\n\n# Helper function to stream output from the graph\ndef stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n \"\"\"Stream app, catching tool calls.\"\"\"\n tool_call_message = None\n for event in app.stream(inputs, thread, stream_mode=\"values\"):\n message = event[\"messages\"][-1]\n if isinstance(message, AIMessage) and message.tool_calls:\n tool_call_message = message\n else:\n message.pretty_print()\n\n return tool_call_message"]
|
||||
"source": [
|
||||
"import json\n",
|
||||
"from typing import Optional\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Helper function to construct message asking for verification\n",
|
||||
"def generate_verification_message(message: AIMessage) -> None:\n",
|
||||
" \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n",
|
||||
" serialized_tool_calls = json.dumps(\n",
|
||||
" message.tool_calls,\n",
|
||||
" indent=2,\n",
|
||||
" )\n",
|
||||
" return AIMessage(\n",
|
||||
" content=(\n",
|
||||
" \"I plan to invoke the following tools, do you approve?\\n\\n\"\n",
|
||||
" \"Type 'y' if you do, anything else to stop.\\n\\n\"\n",
|
||||
" f\"{serialized_tool_calls}\"\n",
|
||||
" ),\n",
|
||||
" id=message.id,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Helper function to stream output from the graph\n",
|
||||
"def stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n",
|
||||
" \"\"\"Stream app, catching tool calls.\"\"\"\n",
|
||||
" tool_call_message = None\n",
|
||||
" for event in app.stream(inputs, thread, stream_mode=\"values\"):\n",
|
||||
" message = event[\"messages\"][-1]\n",
|
||||
" if isinstance(message, AIMessage) and message.tool_calls:\n",
|
||||
" tool_call_message = message\n",
|
||||
" else:\n",
|
||||
" message.pretty_print()\n",
|
||||
"\n",
|
||||
" return tool_call_message"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -514,7 +722,43 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["import uuid\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\n\ntool_call_message = stream_app_catch_tool_calls(\n {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n thread,\n)\n\nwhile tool_call_message:\n verification_message = generate_verification_message(tool_call_message)\n verification_message.pretty_print()\n input_message = HumanMessage(input())\n if input_message.content == \"exit\":\n break\n input_message.pretty_print()\n\n # First we update the state with the verification message and the input message.\n # note that `generate_verification_message` sets the message ID to be the same\n # as the ID from the original tool call message. Updating the state with this\n # message will overwrite the previous tool call.\n snapshot = app.get_state(thread)\n snapshot.values[\"messages\"] += [verification_message, input_message]\n\n if input_message.content == \"y\":\n tool_call_message.id = str(uuid.uuid4())\n # If verified, we append the tool call message to the state\n # and resume execution.\n snapshot.values[\"messages\"] += [tool_call_message]\n app.update_state(thread, snapshot.values, as_node=\"agent\")\n else:\n # Otherwise, resume execution from the input message.\n app.update_state(thread, snapshot.values, as_node=\"__start__\")\n\n tool_call_message = stream_app_catch_tool_calls(None, thread)"]
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
|
||||
"\n",
|
||||
"tool_call_message = stream_app_catch_tool_calls(\n",
|
||||
" {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n",
|
||||
" thread,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"while tool_call_message:\n",
|
||||
" verification_message = generate_verification_message(tool_call_message)\n",
|
||||
" verification_message.pretty_print()\n",
|
||||
" input_message = HumanMessage(input())\n",
|
||||
" if input_message.content == \"exit\":\n",
|
||||
" break\n",
|
||||
" input_message.pretty_print()\n",
|
||||
"\n",
|
||||
" # First we update the state with the verification message and the input message.\n",
|
||||
" # note that `generate_verification_message` sets the message ID to be the same\n",
|
||||
" # as the ID from the original tool call message. Updating the state with this\n",
|
||||
" # message will overwrite the previous tool call.\n",
|
||||
" snapshot = app.get_state(thread)\n",
|
||||
" snapshot.values[\"messages\"] += [verification_message, input_message]\n",
|
||||
"\n",
|
||||
" if input_message.content == \"y\":\n",
|
||||
" tool_call_message.id = str(uuid.uuid4())\n",
|
||||
" # If verified, we append the tool call message to the state\n",
|
||||
" # and resume execution.\n",
|
||||
" snapshot.values[\"messages\"] += [tool_call_message]\n",
|
||||
" app.update_state(thread, snapshot.values, as_node=\"agent\")\n",
|
||||
" else:\n",
|
||||
" # Otherwise, resume execution from the input message.\n",
|
||||
" app.update_state(thread, snapshot.values, as_node=\"__start__\")\n",
|
||||
"\n",
|
||||
" tool_call_message = stream_app_catch_tool_calls(None, thread)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -535,7 +779,34 @@
|
||||
"id": "03232f16-d6fe-46d0-afa0-a6f0d0bf16de",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["class State(TypedDict):\n messages: Annotated[list, add_messages]\n tool_call_message: Optional[AIMessage]\n\n\ndef call_model(state):\n messages = state[\"messages\"]\n if messages[-1].content == \"y\":\n return {\n \"messages\": [state[\"tool_call_message\"]],\n \"tool_call_message\": None,\n }\n else:\n response = model.invoke(messages)\n if response.tool_calls:\n verification_message = generate_verification_message(response)\n response.id = str(uuid.uuid4())\n return {\n \"messages\": [verification_message],\n \"tool_call_message\": response,\n }\n else:\n return {\n \"messages\": [response],\n \"tool_call_message\": None,\n }"]
|
||||
"source": [
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, add_messages]\n",
|
||||
" tool_call_message: Optional[AIMessage]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def call_model(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" if messages[-1].content == \"y\":\n",
|
||||
" return {\n",
|
||||
" \"messages\": [state[\"tool_call_message\"]],\n",
|
||||
" \"tool_call_message\": None,\n",
|
||||
" }\n",
|
||||
" else:\n",
|
||||
" response = model.invoke(messages)\n",
|
||||
" if response.tool_calls:\n",
|
||||
" verification_message = generate_verification_message(response)\n",
|
||||
" response.id = str(uuid.uuid4())\n",
|
||||
" return {\n",
|
||||
" \"messages\": [verification_message],\n",
|
||||
" \"tool_call_message\": response,\n",
|
||||
" }\n",
|
||||
" else:\n",
|
||||
" return {\n",
|
||||
" \"messages\": [response],\n",
|
||||
" \"tool_call_message\": None,\n",
|
||||
" }"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -551,7 +822,27 @@
|
||||
"id": "502dc688-c926-407e-8759-8c9e39eb4257",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["workflow = StateGraph(State)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\nworkflow.add_edge(START, \"agent\")\n\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n {\n \"continue\": \"action\",\n \"end\": END,\n },\n)\n\nworkflow.add_edge(\"action\", \"agent\")\n\napp = workflow.compile(checkpointer=memory)"]
|
||||
"source": [
|
||||
"workflow = StateGraph(State)\n",
|
||||
"\n",
|
||||
"workflow.add_node(\"agent\", call_model)\n",
|
||||
"workflow.add_node(\"action\", call_tool)\n",
|
||||
"\n",
|
||||
"workflow.add_edge(START, \"agent\")\n",
|
||||
"\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" \"agent\",\n",
|
||||
" should_continue,\n",
|
||||
" {\n",
|
||||
" \"continue\": \"action\",\n",
|
||||
" \"end\": END,\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"workflow.add_edge(\"action\", \"agent\")\n",
|
||||
"\n",
|
||||
"app = workflow.compile(checkpointer=memory)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -584,7 +875,13 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["thread = {\"configurable\": {\"thread_id\": \"4\"}}\n\ninputs = [HumanMessage(content=\"what's the weather in sf?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"thread = {\"configurable\": {\"thread_id\": \"4\"}}\n",
|
||||
"\n",
|
||||
"inputs = [HumanMessage(content=\"what's the weather in sf?\")]\n",
|
||||
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -617,7 +914,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\n",
|
||||
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -648,7 +949,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["inputs = [HumanMessage(content=\"y\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"inputs = [HumanMessage(content=\"y\")]\n",
|
||||
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"def answer_node(state: InputState):\n",
|
||||
" return {\"answer\": \"bye\"}\n",
|
||||
"\n",
|
||||
"check = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"graph = StateGraph(input=InputState, output=OutputState)\n",
|
||||
"graph.add_node(answer_node)\n",
|
||||
"graph.add_edge(START, \"answer_node\")\n",
|
||||
|
||||
+18
-18
@@ -848,7 +848,7 @@
|
||||
"\n",
|
||||
"We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But before we get too ahead of ourselves, let's add checkpointing to enable multi-turn conversations.\n",
|
||||
"\n",
|
||||
"To get started, create a `SqliteSaver` checkpointer."
|
||||
"To get started, create a `MemorySaver` checkpointer."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -858,9 +858,9 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")"
|
||||
"memory = MemorySaver()"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -868,7 +868,7 @@
|
||||
"id": "08d3d11a-1b42-4cbb-8e11-2a4294263d90",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**Notice** that we've specified `:memory` as the Sqlite DB path. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to connect to your own DB and/or use one of the other checkpointer classes.\n",
|
||||
"**Notice** we're using an in-memory checkpointer. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect to your own DB.\n",
|
||||
"\n",
|
||||
"Next define the graph. Now that you've already built your own `BasicToolNode`, we'll replace it with LangGraph's prebuilt `ToolNode` and `tools_condition`, since these do some nice things like parallel API execution. Apart from that, the following is all copied from Part 2."
|
||||
]
|
||||
@@ -1199,7 +1199,7 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
@@ -1277,12 +1277,12 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
@@ -1508,7 +1508,7 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
||||
"graph_builder.set_entry_point(\"chatbot\")\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = graph_builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" # This is new!\n",
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
@@ -1627,7 +1627,7 @@
|
||||
")\n",
|
||||
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
||||
"graph_builder.add_edge(START, \"chatbot\")\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = graph_builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" # This is new!\n",
|
||||
@@ -2092,7 +2092,7 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
@@ -2289,7 +2289,7 @@
|
||||
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
||||
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
|
||||
"graph_builder.add_edge(START, \"chatbot\")\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = graph_builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" # We interrupt before 'human' here instead.\n",
|
||||
@@ -2539,7 +2539,7 @@
|
||||
"from langchain_core.pydantic_v1 import BaseModel\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
@@ -2626,7 +2626,7 @@
|
||||
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
||||
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
|
||||
"graph_builder.set_entry_point(\"chatbot\")\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = graph_builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" interrupt_before=[\"human\"],\n",
|
||||
@@ -2665,11 +2665,11 @@
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
||||
"from langchain_core.messages import AIMessage, BaseMessage, ToolMessage\n",
|
||||
"from langchain_core.messages import AIMessage, ToolMessage\n",
|
||||
"from langchain_core.pydantic_v1 import BaseModel\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
@@ -2756,7 +2756,7 @@
|
||||
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
||||
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
|
||||
"graph_builder.add_edge(START, \"chatbot\")\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = graph_builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" interrupt_before=[\"human\"],\n",
|
||||
|
||||
@@ -105,10 +105,10 @@
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.messages import SystemMessage, RemoveMessage\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import MessagesState, StateGraph, START, END\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# We will add a `summary` attribute (in addition to `messages` key,\n",
|
||||
|
||||
@@ -112,11 +112,11 @@
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import MessagesState, StateGraph, START\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
|
||||
@@ -103,11 +103,11 @@
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import MessagesState, StateGraph, START\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
@@ -234,11 +234,11 @@
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import MessagesState, StateGraph, START\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"Example:\n",
|
||||
"```python\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"builder = StateGraph(....)\n",
|
||||
"# ... define the graph\n",
|
||||
"memory = AsyncSqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"...\n",
|
||||
"```\n",
|
||||
@@ -361,9 +361,9 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")"
|
||||
"memory = MemorySaver()"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+546
-1040
File diff suppressed because it is too large
Load Diff
@@ -246,7 +246,7 @@
|
||||
"id": "6845ed6a-d155-4105-9160-28849877248b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
|
||||
"source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import START, END, StateGraph\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"from IPython.display import Image, display\n",
|
||||
@@ -597,7 +597,7 @@
|
||||
"builder.add_edge(\"tools\", \"assistant\")\n",
|
||||
"\n",
|
||||
"# The checkpointer lets the graph persist its state\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"react_graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"# Show\n",
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"from IPython.display import Image, display\n",
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import END, START, StateGraph\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"\n",
|
||||
@@ -275,7 +275,7 @@
|
||||
"builder.add_edge(\"tools\", \"assistant\")\n",
|
||||
"\n",
|
||||
"# The checkpointer lets the graph persist its state\n",
|
||||
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"react_graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"# Show\n",
|
||||
|
||||
@@ -598,7 +598,7 @@
|
||||
"id": "e6e73e85-1232-4848-beba-3139ac7d0a64",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=checkpointer)"]
|
||||
"source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = MemorySaver()\ngraph = builder.compile(checkpointer=checkpointer)"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -809,7 +809,7 @@
|
||||
"id": "3c6456ba-363c-4133-8631-6dabb042b6ce",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["# This is all the same as before\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")"]
|
||||
"source": ["# This is all the same as before\nfrom langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = MemorySaver()"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
|
||||
@@ -4,6 +4,9 @@ Implementation of LangGraph CheckpointSaver that uses Postgres.
|
||||
|
||||
## Usage
|
||||
|
||||
> [!IMPORTANT]
|
||||
> When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
|
||||
@@ -12,6 +15,8 @@ read_config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
||||
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||||
# call .setup() the first time you're using the checkpointer
|
||||
checkpointer.setup()
|
||||
checkpoint = {
|
||||
"v": 1,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Iterator, List, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Connection, Cursor, Pipeline
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
@@ -23,8 +24,6 @@ from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
class PostgresSaver(BasePostgresSaver):
|
||||
lock: threading.Lock
|
||||
|
||||
is_setup: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conn: Connection,
|
||||
@@ -35,7 +34,6 @@ class PostgresSaver(BasePostgresSaver):
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = threading.Lock()
|
||||
self.is_setup = False
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
@@ -63,27 +61,27 @@ class PostgresSaver(BasePostgresSaver):
|
||||
def setup(self) -> None:
|
||||
"""Set up the checkpoint database asynchronously.
|
||||
|
||||
This method creates the necessary tables in the SQLite database if they don't
|
||||
already exist. It is called automatically when needed and should not be called
|
||||
directly by the user.
|
||||
This method creates the necessary tables in the Postgres database if they don't
|
||||
already exist and runs database migrations. It MUST be called directly by the user
|
||||
the first time checkpointer is used.
|
||||
"""
|
||||
if self.is_setup:
|
||||
return
|
||||
with self.lock:
|
||||
create_table_queries = [
|
||||
self.CREATE_CHECKPOINTS_SQL,
|
||||
self.CREATE_CHECKPOINT_BLOBS_SQL,
|
||||
self.CREATE_CHECKPOINT_WRITES_SQL,
|
||||
]
|
||||
with self.conn.cursor(binary=True) as cur:
|
||||
for query in create_table_queries:
|
||||
cur.execute(query)
|
||||
|
||||
try:
|
||||
version = cur.execute(
|
||||
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
||||
).fetchone()["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
if self.pipe:
|
||||
self.pipe.sync()
|
||||
|
||||
self.is_setup = True
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
@@ -92,7 +90,38 @@ class PostgresSaver(BasePostgresSaver):
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
self.setup()
|
||||
"""List checkpoints from the database.
|
||||
|
||||
This method retrieves a list of checkpoint tuples from the Postgres database based
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for listing the checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
|
||||
|
||||
Examples:
|
||||
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
||||
>>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
||||
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
||||
... # Run a graph, then list the checkpoints
|
||||
>>> config = {"configurable": {"thread_id": "1"}}
|
||||
>>> checkpoints = list(memory.list(config, limit=2))
|
||||
>>> print(checkpoints)
|
||||
[CheckpointTuple(...), CheckpointTuple(...)]
|
||||
|
||||
>>> config = {"configurable": {"thread_id": "1"}}
|
||||
>>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
|
||||
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
||||
... # Run a graph, then list the checkpoints
|
||||
>>> checkpoints = list(memory.list(config, before=before))
|
||||
>>> print(checkpoints)
|
||||
[CheckpointTuple(...), ...]
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
if limit:
|
||||
@@ -111,7 +140,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
**self._load_checkpoint(value["checkpoint"]),
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
@@ -124,7 +153,40 @@ class PostgresSaver(BasePostgresSaver):
|
||||
)
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
self.setup()
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
|
||||
Examples:
|
||||
|
||||
Basic:
|
||||
>>> config = {"configurable": {"thread_id": "1"}}
|
||||
>>> checkpoint_tuple = memory.get_tuple(config)
|
||||
>>> print(checkpoint_tuple)
|
||||
CheckpointTuple(...)
|
||||
|
||||
With timestamp:
|
||||
|
||||
>>> config = {
|
||||
... "configurable": {
|
||||
... "thread_id": "1",
|
||||
... "checkpoint_ns": "",
|
||||
... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
... }
|
||||
... }
|
||||
>>> checkpoint_tuple = memory.get_tuple(config)
|
||||
>>> print(checkpoint_tuple)
|
||||
CheckpointTuple(...)
|
||||
""" # noqa
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_id = get_checkpoint_id(config)
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
@@ -155,7 +217,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
**self._load_checkpoint(value["checkpoint"]),
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
@@ -175,6 +237,31 @@ class PostgresSaver(BasePostgresSaver):
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
"""Save a checkpoint to the database.
|
||||
|
||||
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
||||
with the provided config and its parent config (if any).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
||||
>>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
||||
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
||||
>>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
|
||||
>>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "data": {"key": "value"}}
|
||||
>>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
|
||||
>>> print(saved_config)
|
||||
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
|
||||
"""
|
||||
configurable = config["configurable"].copy()
|
||||
thread_id = configurable.pop("thread_id")
|
||||
checkpoint_ns = configurable.pop("checkpoint_ns")
|
||||
@@ -198,7 +285,6 @@ class PostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
copy.pop("channel_values"),
|
||||
copy["channel_versions"],
|
||||
new_versions,
|
||||
),
|
||||
)
|
||||
@@ -210,7 +296,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
Jsonb(metadata),
|
||||
self._dump_metadata(metadata),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -221,6 +307,15 @@ class PostgresSaver(BasePostgresSaver):
|
||||
writes: List[tuple[str, Any]],
|
||||
task_id: str,
|
||||
) -> None:
|
||||
"""Store intermediate writes linked to a checkpoint.
|
||||
|
||||
This method saves intermediate writes associated with a checkpoint to the Postgres database.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): Configuration of the related checkpoint.
|
||||
writes (List[Tuple[str, Any]]): List of writes to store.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
with self._cursor() as cur:
|
||||
cur.executemany(
|
||||
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
||||
@@ -235,7 +330,6 @@ class PostgresSaver(BasePostgresSaver):
|
||||
|
||||
@contextmanager
|
||||
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
|
||||
self.setup()
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
# in multiple threads/coroutines, but only one cursor can be
|
||||
@@ -244,7 +338,8 @@ class PostgresSaver(BasePostgresSaver):
|
||||
with self.conn.cursor(binary=True) as cur:
|
||||
yield cur
|
||||
finally:
|
||||
self.pipe.sync()
|
||||
if pipeline:
|
||||
self.pipe.sync()
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, AsyncIterator, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
@@ -21,8 +22,6 @@ from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
class AsyncPostgresSaver(BasePostgresSaver):
|
||||
lock: asyncio.Lock
|
||||
|
||||
is_setup: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conn: AsyncConnection,
|
||||
@@ -33,7 +32,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = asyncio.Lock()
|
||||
self.is_setup = False
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
@@ -61,27 +59,31 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
async def setup(self) -> None:
|
||||
"""Set up the checkpoint database asynchronously.
|
||||
|
||||
This method creates the necessary tables in the SQLite database if they don't
|
||||
already exist. It is called automatically when needed and should not be called
|
||||
directly by the user.
|
||||
This method creates the necessary tables in the Postgres database if they don't
|
||||
already exist and runs database migrations. It MUST be called directly by the user
|
||||
the first time checkpointer is used.
|
||||
"""
|
||||
if self.is_setup:
|
||||
return
|
||||
async with self.lock:
|
||||
create_table_queries = [
|
||||
self.CREATE_CHECKPOINTS_SQL,
|
||||
self.CREATE_CHECKPOINT_BLOBS_SQL,
|
||||
self.CREATE_CHECKPOINT_WRITES_SQL,
|
||||
]
|
||||
async with self.conn.cursor() as cur:
|
||||
for query in create_table_queries:
|
||||
await cur.execute(query)
|
||||
|
||||
async with self.conn.cursor(binary=True) as cur:
|
||||
try:
|
||||
version = (
|
||||
await cur.execute(
|
||||
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
||||
)
|
||||
).fetchone()["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(
|
||||
f"INSERT INTO checkpoint_migrations (v) VALUES ({v})"
|
||||
)
|
||||
if self.pipe:
|
||||
await self.pipe.sync()
|
||||
|
||||
self.is_setup = True
|
||||
|
||||
async def alist(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
@@ -90,7 +92,20 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> AsyncIterator[CheckpointTuple]:
|
||||
await self.setup()
|
||||
"""List checkpoints from the database asynchronously.
|
||||
|
||||
This method retrieves a list of checkpoint tuples from the Postgres database based
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
if limit:
|
||||
@@ -111,7 +126,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
self._load_blobs, value["channel_values"]
|
||||
),
|
||||
},
|
||||
value["metadata"],
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
@@ -124,7 +139,19 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
)
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
await self.setup()
|
||||
"""Get a checkpoint tuple from the database asynchronously.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_id = get_checkpoint_id(config)
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
@@ -157,7 +184,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
self._load_blobs, value["channel_values"]
|
||||
),
|
||||
},
|
||||
value["metadata"],
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
@@ -177,7 +204,20 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
await self.setup()
|
||||
"""Save a checkpoint to the database asynchronously.
|
||||
|
||||
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
||||
with the provided config and its parent config (if any).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
"""
|
||||
configurable = config["configurable"].copy()
|
||||
thread_id = configurable.pop("thread_id")
|
||||
checkpoint_ns = configurable.pop("checkpoint_ns")
|
||||
@@ -202,7 +242,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
copy.pop("channel_values"),
|
||||
copy["channel_versions"],
|
||||
new_versions,
|
||||
),
|
||||
)
|
||||
@@ -214,7 +253,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
Jsonb(metadata),
|
||||
self._dump_metadata(metadata),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -225,6 +264,15 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
writes: list[tuple[str, Any]],
|
||||
task_id: str,
|
||||
) -> None:
|
||||
"""Store intermediate writes linked to a checkpoint asynchronously.
|
||||
|
||||
This method saves intermediate writes associated with a checkpoint to the database.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): Configuration of the related checkpoint.
|
||||
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
async with self._cursor() as cur:
|
||||
await cur.executemany(
|
||||
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
||||
@@ -240,7 +288,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
|
||||
@asynccontextmanager
|
||||
async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
|
||||
await self.setup()
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
# in multiple threads/coroutines, but only one cursor can be
|
||||
@@ -249,7 +296,8 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
async with self.conn.cursor(binary=True) as cur:
|
||||
yield cur
|
||||
finally:
|
||||
await self.pipe.sync()
|
||||
if pipeline:
|
||||
await self.pipe.sync()
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
|
||||
@@ -11,10 +11,52 @@ from langgraph.checkpoint.base import (
|
||||
EmptyChannelError,
|
||||
get_checkpoint_id,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import ChannelProtocol
|
||||
|
||||
MetadataInput = Optional[dict[str, Any]]
|
||||
|
||||
"""
|
||||
To add a new migration, add a new string to the MIGRATIONS list.
|
||||
The position of the migration in the list is the version number.
|
||||
"""
|
||||
MIGRATIONS = [
|
||||
"""CREATE TABLE IF NOT EXISTS checkpoint_migrations (
|
||||
v INTEGER PRIMARY KEY
|
||||
);""",
|
||||
"""CREATE TABLE IF NOT EXISTS checkpoints (
|
||||
thread_id TEXT NOT NULL,
|
||||
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
||||
checkpoint_id TEXT NOT NULL,
|
||||
parent_checkpoint_id TEXT,
|
||||
type TEXT,
|
||||
checkpoint JSONB NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
||||
);""",
|
||||
"""CREATE TABLE IF NOT EXISTS checkpoint_blobs (
|
||||
thread_id TEXT NOT NULL,
|
||||
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
||||
channel TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
blob BYTEA,
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
|
||||
);""",
|
||||
"""CREATE TABLE IF NOT EXISTS checkpoint_writes (
|
||||
thread_id TEXT NOT NULL,
|
||||
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
||||
checkpoint_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
idx INTEGER NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
type TEXT,
|
||||
blob BYTEA NOT NULL,
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
||||
);""",
|
||||
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
|
||||
]
|
||||
|
||||
SELECT_SQL = """
|
||||
select
|
||||
thread_id,
|
||||
@@ -42,43 +84,6 @@ select
|
||||
) as pending_writes
|
||||
from checkpoints """
|
||||
|
||||
CREATE_CHECKPOINTS_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS checkpoints (
|
||||
thread_id TEXT NOT NULL,
|
||||
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
||||
checkpoint_id TEXT NOT NULL,
|
||||
parent_checkpoint_id TEXT,
|
||||
type TEXT,
|
||||
checkpoint JSONB NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
||||
);
|
||||
"""
|
||||
CREATE_CHECKPOINT_BLOBS_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS checkpoint_blobs (
|
||||
thread_id TEXT NOT NULL,
|
||||
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
||||
channel TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
blob BYTEA NOT NULL,
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
|
||||
);"""
|
||||
|
||||
CREATE_CHECKPOINT_WRITES_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS checkpoint_writes (
|
||||
thread_id TEXT NOT NULL,
|
||||
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
||||
checkpoint_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
idx INTEGER NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
type TEXT,
|
||||
blob BYTEA NOT NULL,
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
||||
);
|
||||
"""
|
||||
|
||||
UPSERT_CHECKPOINT_BLOBS_SQL = """
|
||||
INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
@@ -103,12 +108,11 @@ UPSERT_CHECKPOINT_WRITES_SQL = """
|
||||
|
||||
class BasePostgresSaver(BaseCheckpointSaver):
|
||||
SELECT_SQL = SELECT_SQL
|
||||
CREATE_CHECKPOINTS_SQL = CREATE_CHECKPOINTS_SQL
|
||||
CREATE_CHECKPOINT_BLOBS_SQL = CREATE_CHECKPOINT_BLOBS_SQL
|
||||
CREATE_CHECKPOINT_WRITES_SQL = CREATE_CHECKPOINT_WRITES_SQL
|
||||
MIGRATIONS = MIGRATIONS
|
||||
UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
|
||||
UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
|
||||
UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
|
||||
jsonplus_serde = JsonPlusSerializer()
|
||||
|
||||
def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint:
|
||||
if len(checkpoint["pending_sends"]) == 2 and all(
|
||||
@@ -137,6 +141,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
||||
return {
|
||||
k.decode(): self.serde.loads_typed((t.decode(), v))
|
||||
for k, t, v in blob_values
|
||||
if t.decode() != "empty"
|
||||
}
|
||||
|
||||
def _dump_blobs(
|
||||
@@ -145,24 +150,23 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
||||
checkpoint_ns: str,
|
||||
values: dict[str, Any],
|
||||
versions: dict[str, str],
|
||||
new_versions: Optional[dict[str, str]],
|
||||
) -> list[tuple[str, str, str, str, str, bytes]]:
|
||||
if not versions:
|
||||
return []
|
||||
|
||||
if new_versions:
|
||||
versions = new_versions
|
||||
|
||||
return [
|
||||
(
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
k,
|
||||
ver,
|
||||
*self.serde.dumps_typed(values[k]),
|
||||
*(
|
||||
self.serde.dumps_typed(values[k])
|
||||
if k in values
|
||||
else ("empty", None)
|
||||
),
|
||||
)
|
||||
for k, ver in versions.items()
|
||||
if k in values
|
||||
]
|
||||
|
||||
def _load_writes(
|
||||
@@ -202,6 +206,19 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
||||
for idx, (channel, value) in enumerate(writes)
|
||||
]
|
||||
|
||||
def _load_metadata(self, metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
return self.jsonplus_serde.loads(self.jsonplus_serde.dumps(metadata))
|
||||
|
||||
def _dump_metadata(self, metadata) -> str:
|
||||
serialized_metadata_type, serialized_metadata = self.jsonplus_serde.dumps_typed(
|
||||
metadata
|
||||
)
|
||||
if serialized_metadata_type != "json":
|
||||
raise TypeError(
|
||||
f"Failed to properly serialize metadata -- expected 'json', got '{serialized_metadata_type}'"
|
||||
)
|
||||
return serialized_metadata.decode()
|
||||
|
||||
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
|
||||
if current is None:
|
||||
current_v = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
postgres-test:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5441:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
@@ -13,4 +13,4 @@ services:
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_interval: 1s
|
||||
|
||||
@@ -3,7 +3,7 @@ from psycopg import AsyncConnection
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import dict_row
|
||||
|
||||
DEFAULT_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
||||
DEFAULT_URI = "postgres://postgres:postgres@localhost:5441/postgres?sslmode=disable"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
||||
@@ -13,7 +13,7 @@ from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
|
||||
class TestAsyncPostgresSaver:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
async def setup(self):
|
||||
# objects for test setup
|
||||
self.config_1: RunnableConfig = {
|
||||
"configurable": {
|
||||
@@ -55,6 +55,8 @@ class TestAsyncPostgresSaver:
|
||||
"score": None,
|
||||
}
|
||||
self.metadata_3: CheckpointMetadata = {}
|
||||
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
||||
await saver.setup()
|
||||
|
||||
async def test_asearch(self):
|
||||
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
||||
|
||||
@@ -55,6 +55,8 @@ class TestPostgresSaver:
|
||||
"score": None,
|
||||
}
|
||||
self.metadata_3: CheckpointMetadata = {}
|
||||
with PostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
||||
saver.setup()
|
||||
|
||||
def test_search(self):
|
||||
with PostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
||||
|
||||
@@ -176,7 +176,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
@@ -193,12 +193,12 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
>>> print(checkpoint_tuple)
|
||||
CheckpointTuple(...)
|
||||
|
||||
With timestamp:
|
||||
With checkpoint ID:
|
||||
|
||||
>>> config = {
|
||||
... "configurable": {
|
||||
... "thread_id": "1",
|
||||
... "checkpoint_ns": "",
|
||||
... "checkpoint_ns": "",
|
||||
... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
... }
|
||||
... }
|
||||
@@ -283,12 +283,12 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
"""List checkpoints from the database.
|
||||
|
||||
This method retrieves a list of checkpoint tuples from the SQLite database based
|
||||
on the provided config. The checkpoints are ordered by timestamp in descending order.
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for listing the checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
|
||||
|
||||
Yields:
|
||||
@@ -367,10 +367,11 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (Optional[dict[str, Any]]): Additional metadata to save with the checkpoint. Defaults to None.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
@@ -317,12 +317,12 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
"""List checkpoints from the database asynchronously.
|
||||
|
||||
This method retrieves a list of checkpoint tuples from the SQLite database based
|
||||
on the provided config. The checkpoints are ordered by timestamp in descending order.
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
||||
before (Optional[RunnableConfig]): List checkpoints created before this configuration.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
@@ -385,10 +385,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (dict): New versions as of this write
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
"""
|
||||
await self.setup()
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
|
||||
@@ -289,7 +289,7 @@ class BaseCheckpointSaver(ABC):
|
||||
config (RunnableConfig): Configuration for the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to store.
|
||||
metadata (CheckpointMetadata): Additional metadata for the checkpoint.
|
||||
new_versions (dict): New versions as of this write
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
@@ -381,7 +381,7 @@ class BaseCheckpointSaver(ABC):
|
||||
config (RunnableConfig): Configuration for the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to store.
|
||||
metadata (CheckpointMetadata): Additional metadata for the checkpoint.
|
||||
new_versions (dict): New versions as of this write
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -725,14 +725,14 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]:
|
||||
if len(meta) >= 1 and callable(meta[-1]):
|
||||
sig = signature(meta[0])
|
||||
params = list(sig.parameters.values())
|
||||
if len(params) == 2 and len(
|
||||
[
|
||||
p
|
||||
for p in params
|
||||
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
||||
]
|
||||
if len(params) == 2 and all(
|
||||
p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) for p in params
|
||||
):
|
||||
return BinaryOperatorAggregate(typ, meta[0])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid reducer signature. Expected (a, b) -> c. Got {sig}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ from langgraph.pregel.types import (
|
||||
StateSnapshot,
|
||||
StreamMode,
|
||||
)
|
||||
from langgraph.pregel.utils import get_new_channel_versions
|
||||
from langgraph.pregel.validate import validate_graph, validate_keys
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
|
||||
@@ -517,6 +518,9 @@ class Pregel(
|
||||
# get last checkpoint
|
||||
saved = self.checkpointer.get_tuple(config)
|
||||
checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
|
||||
checkpoint_previous_versions = (
|
||||
saved.checkpoint["channel_versions"] if saved else {}
|
||||
)
|
||||
step = saved.metadata.get("step", -1) if saved else -1
|
||||
# merge configurable fields with previous checkpoint config
|
||||
checkpoint_config = {
|
||||
@@ -606,6 +610,9 @@ class Pregel(
|
||||
checkpoint, channels, [task], self.checkpointer.get_next_version
|
||||
)
|
||||
|
||||
new_versions = get_new_channel_versions(
|
||||
checkpoint_previous_versions, checkpoint["channel_versions"]
|
||||
)
|
||||
return self.checkpointer.put(
|
||||
checkpoint_config,
|
||||
create_checkpoint(checkpoint, channels, step + 1),
|
||||
@@ -614,7 +621,7 @@ class Pregel(
|
||||
"step": step + 1,
|
||||
"writes": {as_node: values},
|
||||
},
|
||||
{},
|
||||
new_versions,
|
||||
)
|
||||
|
||||
async def aupdate_state(
|
||||
@@ -629,6 +636,9 @@ class Pregel(
|
||||
# get last checkpoint
|
||||
saved = await self.checkpointer.aget_tuple(config)
|
||||
checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
|
||||
checkpoint_previous_versions = (
|
||||
saved.checkpoint["channel_versions"] if saved else {}
|
||||
)
|
||||
step = saved.metadata.get("step", -1) if saved else -1
|
||||
# merge configurable fields with previous checkpoint config
|
||||
checkpoint_config = {
|
||||
@@ -716,6 +726,9 @@ class Pregel(
|
||||
checkpoint, channels, [task], self.checkpointer.get_next_version
|
||||
)
|
||||
|
||||
new_versions = get_new_channel_versions(
|
||||
checkpoint_previous_versions, checkpoint["channel_versions"]
|
||||
)
|
||||
return await self.checkpointer.aput(
|
||||
checkpoint_config,
|
||||
create_checkpoint(checkpoint, channels, step + 1),
|
||||
@@ -724,7 +737,7 @@ class Pregel(
|
||||
"step": step + 1,
|
||||
"writes": {as_node: values},
|
||||
},
|
||||
{},
|
||||
new_versions,
|
||||
)
|
||||
|
||||
def _defaults(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Iterator, Mapping, Optional, Sequence, TypeVar, Union
|
||||
|
||||
from langchain_core.runnables.utils import AddableDict
|
||||
@@ -102,36 +101,33 @@ def map_output_updates(
|
||||
output_tasks = [
|
||||
t for t in tasks if not t.config or TAG_HIDDEN not in t.config.get("tags")
|
||||
]
|
||||
if not output_tasks:
|
||||
return
|
||||
if isinstance(output_channels, str):
|
||||
if updated := [
|
||||
updated = [
|
||||
(task.name, value)
|
||||
for task in output_tasks
|
||||
for chan, value in task.writes
|
||||
if chan == output_channels
|
||||
]:
|
||||
grouped = defaultdict(list)
|
||||
for node, value in updated:
|
||||
grouped[node].append(value)
|
||||
for node, value in grouped.items():
|
||||
if len(value) == 1:
|
||||
grouped[node] = value[0]
|
||||
yield AddableUpdatesDict(grouped)
|
||||
]
|
||||
else:
|
||||
if updated := [
|
||||
updated = [
|
||||
(
|
||||
task.name,
|
||||
{chan: value for chan, value in task.writes if chan in output_channels},
|
||||
)
|
||||
for task in output_tasks
|
||||
if any(chan in output_channels for chan, _ in task.writes)
|
||||
]:
|
||||
grouped = defaultdict(list)
|
||||
for node, value in updated:
|
||||
grouped[node].append(value)
|
||||
for node, value in grouped.items():
|
||||
if len(value) == 1:
|
||||
grouped[node] = value[0]
|
||||
yield AddableUpdatesDict(grouped)
|
||||
]
|
||||
grouped = {t.name: [] for t in output_tasks}
|
||||
for node, value in updated:
|
||||
grouped[node].append(value)
|
||||
for node, value in grouped.items():
|
||||
if len(value) == 0:
|
||||
grouped[node] = None
|
||||
if len(value) == 1:
|
||||
grouped[node] = value[0]
|
||||
yield AddableUpdatesDict(grouped)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -61,6 +61,7 @@ from langgraph.pregel.executor import (
|
||||
)
|
||||
from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single
|
||||
from langgraph.pregel.types import PregelExecutableTask
|
||||
from langgraph.pregel.utils import get_new_channel_versions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langgraph.pregel import Pregel
|
||||
@@ -336,16 +337,9 @@ class PregelLoop:
|
||||
}
|
||||
|
||||
channel_versions = self.checkpoint["channel_versions"].copy()
|
||||
if self.checkpoint_previous_versions:
|
||||
new_versions = {
|
||||
k: v
|
||||
for k, v in channel_versions.items()
|
||||
if k not in self.checkpoint_previous_versions
|
||||
or k in self.checkpoint_previous_versions
|
||||
and v > self.checkpoint_previous_versions[k]
|
||||
}
|
||||
else:
|
||||
new_versions = channel_versions
|
||||
new_versions = get_new_channel_versions(
|
||||
self.checkpoint_previous_versions, channel_versions
|
||||
)
|
||||
|
||||
self.checkpoint_previous_versions = channel_versions
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from langgraph.checkpoint.base import ChannelVersions
|
||||
|
||||
|
||||
def get_new_channel_versions(
|
||||
previous_versions: ChannelVersions, current_versions: ChannelVersions
|
||||
) -> ChannelVersions:
|
||||
"""Get new channel versions."""
|
||||
if previous_versions:
|
||||
version_type = type(next(iter(current_versions.values()), None))
|
||||
null_version = version_type()
|
||||
new_versions = {
|
||||
k: v
|
||||
for k, v in current_versions.items()
|
||||
if v > previous_versions.get(k, null_version)
|
||||
}
|
||||
else:
|
||||
new_versions = current_versions
|
||||
|
||||
return new_versions
|
||||
Generated
+4
-4
@@ -1820,7 +1820,7 @@ types-requests = ">=2.31.0.2,<3.0.0.0"
|
||||
|
||||
[[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"
|
||||
@@ -1844,7 +1844,7 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langgraph-checkpoint = "^1.0.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = {version = ">=3.1.19", extras = ["binary"]}
|
||||
|
||||
@@ -1863,7 +1863,7 @@ develop = true
|
||||
|
||||
[package.dependencies]
|
||||
aiosqlite = "^0.20.0"
|
||||
langgraph-checkpoint = "^1.0.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -4282,4 +4282,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
content-hash = "61fe872e3b9dc213966f73958819fc42219c2aebe3bf9ef56605005c12d336c6"
|
||||
content-hash = "4ef9e25016072ce08554c8ff8d091104fb59da7cdab9a128312bd787e6f35146"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.1.19"
|
||||
version = "0.2.3"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,6 +10,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.27,<0.3"
|
||||
langgraph-checkpoint = "^1.0.2"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -1392,6 +1392,7 @@
|
||||
down --> __end__;
|
||||
side --> down;
|
||||
up --> down;
|
||||
up --> other;
|
||||
up --> side;
|
||||
|
||||
'''
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
postgres-test:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5442:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
@@ -13,4 +13,4 @@ services:
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_interval: 1s
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
from uuid import UUID
|
||||
import asyncio
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import AsyncIterator, Iterator, TypeVar
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
DEFAULT_POSTGRES_URI = (
|
||||
"postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
||||
)
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -19,23 +26,162 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
return mocker.patch("uuid.uuid4", side_effect=side_effect)
|
||||
|
||||
|
||||
"""
|
||||
pytest-asyncio doesn't support calling async fixtures with getfixturevalue
|
||||
so we need to use ThreadPoolExecutor to run the async fixture in a thread
|
||||
https://github.com/pytest-dev/pytest-asyncio/issues/112#issuecomment-462062890
|
||||
"""
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def close_loop(loop: asyncio.AbstractEventLoop) -> None:
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(loop.shutdown_default_executor())
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def agen_to_gen(agen: AsyncIterator[T]) -> Iterator[T]:
|
||||
with ThreadPoolExecutor(1) as bg:
|
||||
loop = asyncio.new_event_loop()
|
||||
bg.submit(asyncio.set_event_loop, loop).result()
|
||||
try:
|
||||
yield bg.submit(loop.run_until_complete, agen.__aenter__()).result()
|
||||
finally:
|
||||
bg.submit(
|
||||
loop.run_until_complete, agen.__aexit__(None, None, None)
|
||||
).result()
|
||||
bg.submit(close_loop, loop).result()
|
||||
|
||||
|
||||
# checkpointer fixtures
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def conn():
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
||||
) as conn:
|
||||
yield conn
|
||||
def checkpointer_memory():
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
async def clear_test_db(conn):
|
||||
"""Delete all tables before each test."""
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite_aio():
|
||||
with agen_to_gen(_checkpointer_sqlite_aio()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
await conn.execute("DELETE FROM checkpoints")
|
||||
await conn.execute("DELETE FROM checkpoint_blobs")
|
||||
await conn.execute("DELETE FROM checkpoint_writes")
|
||||
except UndefinedTable:
|
||||
pass
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
pytest.register_assert_rewrite("tests.memory_assert")
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
with agen_to_gen(_checkpointer_postgres_aio()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
with agen_to_gen(_checkpointer_postgres_aio_pipe()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
|
||||
def test_interruption_without_state_updates():
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite", "postgres", "postgres_pipe"],
|
||||
)
|
||||
def test_interruption_without_state_updates(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
) -> None:
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
|
||||
@@ -23,9 +31,8 @@ def test_interruption_without_state_updates():
|
||||
builder.add_edge("step_2", "step_3")
|
||||
builder.add_edge("step_3", END)
|
||||
|
||||
memory = MemorySaver()
|
||||
|
||||
graph = builder.compile(checkpointer=memory, interrupt_after="*")
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_after="*")
|
||||
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
@@ -40,7 +47,13 @@ def test_interruption_without_state_updates():
|
||||
assert graph.get_state(thread).next == ()
|
||||
|
||||
|
||||
async def test_interruption_without_state_updates_async():
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"],
|
||||
)
|
||||
async def test_interruption_without_state_updates_async(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
):
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
|
||||
@@ -59,9 +72,8 @@ async def test_interruption_without_state_updates_async():
|
||||
builder.add_edge("step_2", "step_3")
|
||||
builder.add_edge("step_3", END)
|
||||
|
||||
memory = MemorySaver()
|
||||
|
||||
graph = builder.compile(checkpointer=memory, interrupt_after="*")
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_after="*")
|
||||
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
@@ -18,11 +18,8 @@ from langchain_core.tools import BaseTool
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.prebuilt import ToolNode, ValidationNode, create_react_agent
|
||||
from langgraph.prebuilt.tool_node import InjectedState
|
||||
from tests.any_str import AnyStr
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
from tests.messages import _AnyIdHumanMessage
|
||||
|
||||
|
||||
@@ -54,18 +51,13 @@ class FakeToolCallingModel(BaseChatModel):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer",
|
||||
[
|
||||
MemorySaverAssertImmutable(),
|
||||
None,
|
||||
],
|
||||
ids=[
|
||||
"memory",
|
||||
"none",
|
||||
],
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite", "postgres", "postgres_pipe"],
|
||||
)
|
||||
def test_no_modifier(checkpointer: Optional[BaseCheckpointSaver]):
|
||||
def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> None:
|
||||
checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name)
|
||||
model = FakeToolCallingModel()
|
||||
|
||||
agent = create_react_agent(model, [], checkpointer=checkpointer)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
thread = {"configurable": {"thread_id": "123"}}
|
||||
@@ -76,30 +68,12 @@ def test_no_modifier(checkpointer: Optional[BaseCheckpointSaver]):
|
||||
if checkpointer:
|
||||
saved = checkpointer.get_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint == {
|
||||
"v": 1,
|
||||
"ts": AnyStr(),
|
||||
"id": AnyStr(),
|
||||
"channel_values": {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
},
|
||||
"channel_versions": {
|
||||
"__start__": 2,
|
||||
"messages": 3,
|
||||
"start:agent": 3,
|
||||
"agent": 3,
|
||||
},
|
||||
"versions_seen": {
|
||||
"__input__": {},
|
||||
"__start__": {"__start__": 1},
|
||||
"agent": {"start:agent": 2},
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {},
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"source": "loop",
|
||||
@@ -110,18 +84,16 @@ def test_no_modifier(checkpointer: Optional[BaseCheckpointSaver]):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer",
|
||||
[
|
||||
MemorySaverAssertImmutable(),
|
||||
None,
|
||||
],
|
||||
ids=[
|
||||
"memory",
|
||||
"none",
|
||||
],
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"],
|
||||
)
|
||||
async def test_no_modifier_async(checkpointer: Optional[BaseCheckpointSaver]):
|
||||
async def test_no_modifier_async(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
model = FakeToolCallingModel()
|
||||
|
||||
agent = create_react_agent(model, [], checkpointer=checkpointer)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
thread = {"configurable": {"thread_id": "123"}}
|
||||
@@ -132,30 +104,12 @@ async def test_no_modifier_async(checkpointer: Optional[BaseCheckpointSaver]):
|
||||
if checkpointer:
|
||||
saved = await checkpointer.aget_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint == {
|
||||
"v": 1,
|
||||
"ts": AnyStr(),
|
||||
"id": AnyStr(),
|
||||
"channel_values": {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
},
|
||||
"channel_versions": {
|
||||
"__start__": 2,
|
||||
"messages": 3,
|
||||
"start:agent": 3,
|
||||
"agent": 3,
|
||||
},
|
||||
"versions_seen": {
|
||||
"__input__": {},
|
||||
"__start__": {"__start__": 1},
|
||||
"agent": {"start:agent": 2},
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {},
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"source": "loop",
|
||||
|
||||
+1798
-1778
File diff suppressed because it is too large
Load Diff
+1879
-1969
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ export interface Config {
|
||||
/**
|
||||
* Timestamp of the state checkpoint
|
||||
*/
|
||||
thread_ts?: string;
|
||||
checkpoint_id?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -783,7 +783,8 @@ class ThreadsClient:
|
||||
'configurable':
|
||||
{
|
||||
'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2',
|
||||
'thread_ts': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1'
|
||||
'checkpoint_ns': '',
|
||||
'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1'
|
||||
}
|
||||
},
|
||||
'metadata':
|
||||
@@ -822,7 +823,8 @@ class ThreadsClient:
|
||||
'configurable':
|
||||
{
|
||||
'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2',
|
||||
'thread_ts': '1ef4a9b8-d80d-6fa7-8000-9300467fad0f'
|
||||
'checkpoint_ns': '',
|
||||
'checkpoint_id': '1ef4a9b8-d80d-6fa7-8000-9300467fad0f'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1184,7 +1186,7 @@ class RunsClient:
|
||||
'user_id': None,
|
||||
'graph_id': 'agent',
|
||||
'thread_id': 'my_thread_id',
|
||||
'thread_ts': None,
|
||||
'checkpoint_id': None,
|
||||
'model_name': "openai",
|
||||
'assistant_id': 'my_assistant_id'
|
||||
}
|
||||
|
||||
Generated
+113
-7
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user